@useparagon/connect 1.0.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 +674 -0
- package/README.md +19 -0
- package/package.json +113 -0
- package/src/ConnectSDK.tsx +1065 -0
- package/src/entities/license.interface.ts +8 -0
|
@@ -0,0 +1,1065 @@
|
|
|
1
|
+
import jwtDecode from 'jwt-decode';
|
|
2
|
+
|
|
3
|
+
import { VisibleConnectAction } from './types/action';
|
|
4
|
+
import ActionCatalogConfigs from './helpers/actionCatalog';
|
|
5
|
+
import { IPersona, PersonaMeta } from './entities/persona.interface';
|
|
6
|
+
import {
|
|
7
|
+
CredentialStatus,
|
|
8
|
+
IConnectCredential,
|
|
9
|
+
} from './entities/connectCredential.interface';
|
|
10
|
+
import { ICredential } from './entities/credential.interface';
|
|
11
|
+
import {
|
|
12
|
+
IConnectIntegrationWithCredentialInfo,
|
|
13
|
+
IIntegrationMetadata,
|
|
14
|
+
getIntegrationTypeName,
|
|
15
|
+
isCustomIntegrationTypeName,
|
|
16
|
+
} from './entities/integration.interface'
|
|
17
|
+
import { IConnectSDKProject } from './entities/project.interface';
|
|
18
|
+
import { Action, overrideActionAlias } from './types/action';
|
|
19
|
+
import {
|
|
20
|
+
AuthenticatedConnectUser,
|
|
21
|
+
IConnectUser,
|
|
22
|
+
INFER_CONTENT_TYPE_FROM_CONNECT_OPTIONS,
|
|
23
|
+
ModalConfig,
|
|
24
|
+
} from './types/connect';
|
|
25
|
+
import { ConnectAddOn } from './types/stripe';
|
|
26
|
+
import { getAssetUrl } from './utils/connect';
|
|
27
|
+
import { hash } from './utils/crypto';
|
|
28
|
+
import { isValidUrl, sanitizeUrl } from './utils/http';
|
|
29
|
+
import { CacheThrottle } from './utils/throttle';
|
|
30
|
+
|
|
31
|
+
import { Props as ConnectModalProps } from './types/connectModal';
|
|
32
|
+
import { IConnectUserContext } from './helpers/ConnectUserContext';
|
|
33
|
+
import { shouldShowPreAuthInputs, startOAuthFlow } from './helpers/oauth';
|
|
34
|
+
import SDKEventEmitter from './SDKEventEmitter';
|
|
35
|
+
import { ConnectSdkEnvironments } from './server.types';
|
|
36
|
+
import {
|
|
37
|
+
AuthenticateOptions,
|
|
38
|
+
ConnectParams,
|
|
39
|
+
ConnectUser,
|
|
40
|
+
DocumentLoadingState,
|
|
41
|
+
EventInfo,
|
|
42
|
+
FunctionPropertyNames,
|
|
43
|
+
IConnectSDK,
|
|
44
|
+
InstallOptions,
|
|
45
|
+
IntegrationInstallEvent,
|
|
46
|
+
SDKFunctionErrorMessage,
|
|
47
|
+
SDKFunctionInvocationMessage,
|
|
48
|
+
SDKFunctionResponseMessage,
|
|
49
|
+
SDKReceivedMessage,
|
|
50
|
+
SDK_EVENT,
|
|
51
|
+
TriggerWorkflowRequest,
|
|
52
|
+
UIUpdateMessage,
|
|
53
|
+
UnwrapPromise,
|
|
54
|
+
UserProvidedIntegrationConfig,
|
|
55
|
+
} from './types/sdk';
|
|
56
|
+
|
|
57
|
+
const PARAGON_ROOT_IFRAME_ID = 'paragon-connect-frame';
|
|
58
|
+
const PARAGON_ROOT_IFRAME_CONTAINER_ID = `${PARAGON_ROOT_IFRAME_ID}-container`;
|
|
59
|
+
const PARAGON_STATE_STORAGE_KEY = 'paragon-connect-user-state';
|
|
60
|
+
export const PARAGON_OVERFLOW_EMPTY_VALUE = 'PARAGON_OVERFLOW_EMPTY_VALUE';
|
|
61
|
+
|
|
62
|
+
export default class ConnectSDK extends SDKEventEmitter implements IConnectSDK {
|
|
63
|
+
root: HTMLIFrameElement | undefined;
|
|
64
|
+
private rootLoaded: boolean = false;
|
|
65
|
+
private projectId: string | undefined;
|
|
66
|
+
private modalState: ConnectModalProps = {
|
|
67
|
+
integration: null,
|
|
68
|
+
onClose: this.onClose.bind(this),
|
|
69
|
+
overlayStyle: { overflow: 'auto' },
|
|
70
|
+
onOpen: this.onOpen.bind(this),
|
|
71
|
+
apiInstallationOptions: {
|
|
72
|
+
isApiInstallation: false,
|
|
73
|
+
showPortalAfterInstall: false,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
private userState: ConnectUser = {
|
|
77
|
+
authenticated: false,
|
|
78
|
+
};
|
|
79
|
+
private loadedConfigs: { [type: string]: ModalConfig } = {};
|
|
80
|
+
private loadedIntegrations: { [type: string]: IConnectIntegrationWithCredentialInfo } = {};
|
|
81
|
+
private endUserIntegrationConfig: { [type: string]: UserProvidedIntegrationConfig } = {};
|
|
82
|
+
private environments: ConnectSdkEnvironments;
|
|
83
|
+
private project: IConnectSDKProject;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* map b/w integration key name to integration metadata
|
|
87
|
+
*/
|
|
88
|
+
private metadata: Record<string, IIntegrationMetadata> = {};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* cache service
|
|
92
|
+
*/
|
|
93
|
+
private cachedApiResponse: CacheThrottle = new CacheThrottle({ ttl: 1000 * 5 });
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* in order to not call multiple same GET request at same time
|
|
97
|
+
* we are storing {key -> promise} map
|
|
98
|
+
*/
|
|
99
|
+
private keyToRequestPromiseMap: Record<string, Promise<unknown>> = {};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* this will store the original overflow style of body
|
|
103
|
+
* we are using PARAGON_OVERFLOW_EMPTY_VALUE as default
|
|
104
|
+
* so that we will be able to know whether style is added in body or not
|
|
105
|
+
*/
|
|
106
|
+
private originalBodyOverflow: string | null = PARAGON_OVERFLOW_EMPTY_VALUE;
|
|
107
|
+
|
|
108
|
+
constructor(environments?: ConnectSdkEnvironments) {
|
|
109
|
+
super();
|
|
110
|
+
|
|
111
|
+
const assetURL: string = getAssetUrl({
|
|
112
|
+
CDN_PUBLIC_URL: process.env.CDN_PUBLIC_URL || '',
|
|
113
|
+
DASHBOARD_PUBLIC_URL: process.env.DASHBOARD_PUBLIC_URL || '',
|
|
114
|
+
NODE_ENV: process.env.NODE_ENV || '',
|
|
115
|
+
PLATFORM_ENV: process.env.PLATFORM_ENV || '',
|
|
116
|
+
VERSION: process.env.VERSION || '',
|
|
117
|
+
});
|
|
118
|
+
this.environments = {
|
|
119
|
+
CDN_PUBLIC_URL: assetURL,
|
|
120
|
+
CONNECT_PUBLIC_URL: process.env.CONNECT_PUBLIC_URL as string,
|
|
121
|
+
DASHBOARD_PUBLIC_URL: process.env.DASHBOARD_PUBLIC_URL as string,
|
|
122
|
+
NODE_ENV: process.env.NODE_ENV as string,
|
|
123
|
+
PASSPORT_PUBLIC_URL: process.env.PASSPORT_PUBLIC_URL as string,
|
|
124
|
+
PLATFORM_ENV: process.env.PLATFORM_ENV as string,
|
|
125
|
+
VERSION: process.env.VERSION as string,
|
|
126
|
+
ZEUS_PUBLIC_URL: process.env.ZEUS_PUBLIC_URL as string,
|
|
127
|
+
...environments,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
if (!this.environments.CONNECT_PUBLIC_URL) {
|
|
131
|
+
throw new Error('Paragon SDK error! No `CONNECT_PUBLIC_URL` configured.');
|
|
132
|
+
} else if (!this.environments.DASHBOARD_PUBLIC_URL) {
|
|
133
|
+
throw new Error('Paragon SDK error! No `DASHBOARD_PUBLIC_URL` configured.');
|
|
134
|
+
} else if (!this.environments.PASSPORT_PUBLIC_URL) {
|
|
135
|
+
throw new Error('Paragon SDK error! No `PASSPORT_PUBLIC_URL` configured.');
|
|
136
|
+
} else if (!this.environments.ZEUS_PUBLIC_URL) {
|
|
137
|
+
throw new Error('Paragon SDK error! No `ZEUS_PUBLIC_URL` configured.');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.loadState();
|
|
141
|
+
|
|
142
|
+
window.addEventListener('message', this.eventMessageHandler.bind(this));
|
|
143
|
+
|
|
144
|
+
if (window.document.readyState === DocumentLoadingState.LOADING) {
|
|
145
|
+
// still loading, wait for the event
|
|
146
|
+
window.document.addEventListener('DOMContentLoaded', () => {
|
|
147
|
+
this.createReactRoot();
|
|
148
|
+
});
|
|
149
|
+
} else {
|
|
150
|
+
// DOM is ready!
|
|
151
|
+
this.createReactRoot();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* post message handler
|
|
157
|
+
* @param event
|
|
158
|
+
*/
|
|
159
|
+
private async eventMessageHandler(event: MessageEvent): Promise<void> {
|
|
160
|
+
switch (event.data.type) {
|
|
161
|
+
case 'oauth_success_callback':
|
|
162
|
+
await this._oauthCallback(event.data.credential);
|
|
163
|
+
break;
|
|
164
|
+
case 'oauth_error_callback':
|
|
165
|
+
await this._oauthErrorCallback(event.data.error);
|
|
166
|
+
break;
|
|
167
|
+
default:
|
|
168
|
+
await this.functionInvocationHandler(event);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private async functionInvocationHandler<T extends FunctionPropertyNames<ConnectSDK>>(
|
|
173
|
+
event: MessageEvent,
|
|
174
|
+
): Promise<void> {
|
|
175
|
+
if ((event.data as SDKReceivedMessage).messageType !== 'SDK_FUNCTION_INVOCATION') {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const { type, id, parameters } = event.data as SDKFunctionInvocationMessage<T>;
|
|
179
|
+
let reply: SDKFunctionResponseMessage<T> | SDKFunctionErrorMessage | undefined;
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
// @ts-ignore The spread isn't recognized here as valid args
|
|
183
|
+
const result = (await this[type](...parameters)) as UnwrapPromise<ReturnType<ConnectSDK[T]>>;
|
|
184
|
+
reply = { messageType: 'SDK_FUNCTION_RESPONSE', id, type, result };
|
|
185
|
+
} catch (err) {
|
|
186
|
+
reply = { messageType: 'SDK_FUNCTION_ERROR', error: true, message: err.message, id };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
(event.source as Window).postMessage(reply, this.environments.CONNECT_PUBLIC_URL);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private createReactRoot(): void {
|
|
193
|
+
this.root = document.createElement('iframe');
|
|
194
|
+
this.root.onload = () => {
|
|
195
|
+
this.rootLoaded = true;
|
|
196
|
+
// when root is loaded then render component
|
|
197
|
+
this.render();
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const isAtRoot: boolean = document.querySelector(`#${PARAGON_ROOT_IFRAME_CONTAINER_ID}`)
|
|
201
|
+
? true
|
|
202
|
+
: false;
|
|
203
|
+
this.root.id = PARAGON_ROOT_IFRAME_ID;
|
|
204
|
+
this.root.src = `${this.environments.CONNECT_PUBLIC_URL}/ui${
|
|
205
|
+
this.projectId ? `?projectId=${this.projectId}` : ''
|
|
206
|
+
}`;
|
|
207
|
+
this.root.style.position = isAtRoot ? 'absolute' : 'fixed';
|
|
208
|
+
this.root.style.top = '0';
|
|
209
|
+
this.root.style.left = '0';
|
|
210
|
+
this.root.style.width = isAtRoot ? '100%' : '100vw';
|
|
211
|
+
this.root.style.height = isAtRoot ? '100%' : '100vh';
|
|
212
|
+
this.root.style.zIndex = '2147483647';
|
|
213
|
+
this.root.style.display = 'none';
|
|
214
|
+
|
|
215
|
+
(document.querySelector(`#${PARAGON_ROOT_IFRAME_CONTAINER_ID}`) || document.body)?.appendChild(
|
|
216
|
+
this.root,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private validateAction(action: Action, validateIsEnabled: boolean = false): boolean {
|
|
221
|
+
if (!Object.values(Action).includes(action) && !isCustomIntegrationTypeName(action)) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`"${action}" is not a valid integration type. The integrations you have configured for this Paragon project are:
|
|
224
|
+
|
|
225
|
+
${Object.keys(this.loadedConfigs)
|
|
226
|
+
.map((validAction: string) => `- "${validAction}"`)
|
|
227
|
+
.join('\n')}
|
|
228
|
+
`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!this.loadedConfigs[action]) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`paragon.connect() was called with "${action}", but you do not have this integration set up in your Paragon project yet.
|
|
235
|
+
|
|
236
|
+
${Object.keys(this.loadedConfigs)
|
|
237
|
+
.map((validAction: string) => `- "${validAction}"`)
|
|
238
|
+
.join('\n')}`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!this.loadedIntegrations[action]?.isActive) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`paragon.connect() was called with "${action}", but this integration is not active in your Paragon project yet.`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (validateIsEnabled && this.userState.authenticated) {
|
|
249
|
+
return Boolean(this.userState.integrations[action]?.enabled);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private async bootstrapSDKState(userMeta?: PersonaMeta): Promise<void> {
|
|
256
|
+
if (!this.projectId || !this.userState.authenticated) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
this.project = (await this.sendConnectRequest<IConnectSDKProject>(
|
|
261
|
+
`/sdk/projects/${this.projectId}`,
|
|
262
|
+
{
|
|
263
|
+
headers: {
|
|
264
|
+
'X-Paragon-Cache-Metadata': '1',
|
|
265
|
+
...(userMeta
|
|
266
|
+
? {
|
|
267
|
+
'X-Paragon-User-Metadata': JSON.stringify(userMeta),
|
|
268
|
+
}
|
|
269
|
+
: {}),
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
false,
|
|
273
|
+
)) as IConnectSDKProject;
|
|
274
|
+
|
|
275
|
+
await this.updateLocalState();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
setModalState(statePartial: Partial<ConnectModalProps>): void {
|
|
279
|
+
this.modalState = {
|
|
280
|
+
config: undefined,
|
|
281
|
+
...this.modalState,
|
|
282
|
+
...statePartial,
|
|
283
|
+
};
|
|
284
|
+
this.render();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Loads previous SDK state saved in localStorage.
|
|
289
|
+
*
|
|
290
|
+
* **Warning:** This favors stored to current state. This should typically be called in
|
|
291
|
+
* unauthenticated contexts, as a fallback.
|
|
292
|
+
*/
|
|
293
|
+
private loadState(): void {
|
|
294
|
+
if (typeof window !== 'undefined') {
|
|
295
|
+
try {
|
|
296
|
+
const localState: {
|
|
297
|
+
userState: Record<string, unknown> | undefined;
|
|
298
|
+
projectId: string | undefined;
|
|
299
|
+
} = window.localStorage.getItem(PARAGON_STATE_STORAGE_KEY)
|
|
300
|
+
? JSON.parse(window.localStorage.getItem(PARAGON_STATE_STORAGE_KEY) as string)
|
|
301
|
+
: {};
|
|
302
|
+
|
|
303
|
+
if (localState.projectId) {
|
|
304
|
+
this.projectId = localState.projectId;
|
|
305
|
+
}
|
|
306
|
+
if (localState.userState) {
|
|
307
|
+
const { userState } = localState;
|
|
308
|
+
if ('authenticated' in userState && userState.authenticated && 'token' in userState) {
|
|
309
|
+
this.updateAuthenticatedUser(userState);
|
|
310
|
+
// Refresh user state automatically, if available
|
|
311
|
+
this.bootstrapSDKState().catch(() => {
|
|
312
|
+
// PARA-3267
|
|
313
|
+
this.clearState();
|
|
314
|
+
});
|
|
315
|
+
} else {
|
|
316
|
+
throw new Error(
|
|
317
|
+
'Malformatted or unauthenticated user was persisted into localStorage. Refusing to load.',
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
} catch {
|
|
322
|
+
this.clearState();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Saves the current SDK state into localStorage, if available.
|
|
329
|
+
*/
|
|
330
|
+
private saveState(): void {
|
|
331
|
+
if (typeof window !== 'undefined' && this.userState.authenticated) {
|
|
332
|
+
window.localStorage.setItem(
|
|
333
|
+
PARAGON_STATE_STORAGE_KEY,
|
|
334
|
+
JSON.stringify({ projectId: this.projectId, userState: this.userState }),
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Clears the current SDK state from localStorage, if available.
|
|
341
|
+
*/
|
|
342
|
+
private clearState(): void {
|
|
343
|
+
if (typeof window !== 'undefined') {
|
|
344
|
+
window.localStorage.removeItem(PARAGON_STATE_STORAGE_KEY);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private render(): void {
|
|
349
|
+
if (this.root && this.rootLoaded) {
|
|
350
|
+
const { onClose, onOpen, ...modalState } = this.modalState;
|
|
351
|
+
const update: UIUpdateMessage = {
|
|
352
|
+
messageType: 'UI_UPDATE',
|
|
353
|
+
nextContext: {
|
|
354
|
+
user: this.userState,
|
|
355
|
+
projectId: this.projectId,
|
|
356
|
+
environments: this.environments,
|
|
357
|
+
project: this.project,
|
|
358
|
+
endUserIntegrationConfig: this.modalState.integration
|
|
359
|
+
? this.endUserIntegrationConfig[this.modalState.integration.type]
|
|
360
|
+
: undefined,
|
|
361
|
+
},
|
|
362
|
+
nextModalState: modalState,
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
if (!this.root.contentWindow) {
|
|
366
|
+
throw new Error('Browser not supported');
|
|
367
|
+
}
|
|
368
|
+
this.root.contentWindow.postMessage(update, this.environments.CONNECT_PUBLIC_URL);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* this will update container style
|
|
374
|
+
* @param param0
|
|
375
|
+
* @returns
|
|
376
|
+
*/
|
|
377
|
+
updateContainerStyle({ isModalShown }: { isModalShown: boolean }) {
|
|
378
|
+
if (!this.root) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (isModalShown) {
|
|
382
|
+
this.root.style.display = 'block';
|
|
383
|
+
} else {
|
|
384
|
+
this.root.style.display = 'none';
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Authenticate your end user into the Paragon Connect SDK.
|
|
390
|
+
*
|
|
391
|
+
* @param projectId Your Paragon project ID.
|
|
392
|
+
* @param token A JWT signed by your App Server. The JWT should include a user ID and a
|
|
393
|
+
* session expiration time.
|
|
394
|
+
*/
|
|
395
|
+
async authenticate(
|
|
396
|
+
projectId: string,
|
|
397
|
+
token: string,
|
|
398
|
+
options?: AuthenticateOptions,
|
|
399
|
+
): Promise<void> {
|
|
400
|
+
if (!projectId || !token) {
|
|
401
|
+
throw new Error('projectId or token not specified to paragon.authenticate()');
|
|
402
|
+
}
|
|
403
|
+
let decodedJwt: { sub?: string; id?: string } = {};
|
|
404
|
+
try {
|
|
405
|
+
decodedJwt = jwtDecode<typeof decodedJwt>(token);
|
|
406
|
+
} catch (err) {
|
|
407
|
+
throw new Error('A well-formed JWT was not provided to paragon.authenticate()');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
this.projectId = projectId;
|
|
411
|
+
this.userState = {
|
|
412
|
+
authenticated: true,
|
|
413
|
+
token,
|
|
414
|
+
userId: (decodedJwt.sub || decodedJwt.id) as string,
|
|
415
|
+
integrations: {},
|
|
416
|
+
meta: options?.metadata ?? {},
|
|
417
|
+
};
|
|
418
|
+
try {
|
|
419
|
+
await this.bootstrapSDKState(options?.metadata);
|
|
420
|
+
} catch (err) {
|
|
421
|
+
console.warn('paragon.authenticate() could not login user', err);
|
|
422
|
+
this.logout();
|
|
423
|
+
throw new Error(`Failed to authenticate user with Paragon: ${err.message}`);
|
|
424
|
+
}
|
|
425
|
+
this.render();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Get the Paragon authentication and integration state of your end user.
|
|
430
|
+
*/
|
|
431
|
+
getUser(): ConnectUser {
|
|
432
|
+
return Object.fromEntries(
|
|
433
|
+
Object.entries(this.userState).filter(([key]: [string, unknown]) => key !== 'token'),
|
|
434
|
+
) as ConnectUser;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
updateAuthenticatedUser(statePartial: Partial<AuthenticatedConnectUser>): void {
|
|
438
|
+
if (this.userState.authenticated) {
|
|
439
|
+
this.userState = { ...this.userState, ...statePartial };
|
|
440
|
+
} else if (statePartial.authenticated) {
|
|
441
|
+
this.userState = statePartial as AuthenticatedConnectUser;
|
|
442
|
+
}
|
|
443
|
+
this.render();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Logout the currently authenticated end user from the Paragon SDK.
|
|
448
|
+
*/
|
|
449
|
+
logout(): void {
|
|
450
|
+
this.userState = { authenticated: false };
|
|
451
|
+
this.clearState();
|
|
452
|
+
this.render();
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Display the Paragon Connect modal
|
|
457
|
+
*/
|
|
458
|
+
async connect(
|
|
459
|
+
action: Action,
|
|
460
|
+
params: ConnectParams = { showPortalAfterInstall: true, isApiInstallation: false },
|
|
461
|
+
): Promise<void> {
|
|
462
|
+
const { mapObjectFields, overrideRedirectUrl, ...callbacks } = params;
|
|
463
|
+
|
|
464
|
+
return new Promise((resolve: () => void, reject: (err: Error) => void) => {
|
|
465
|
+
this.subscribeToIntegration(action, {
|
|
466
|
+
...callbacks,
|
|
467
|
+
onInstall: (event: IntegrationInstallEvent, user: AuthenticatedConnectUser) => {
|
|
468
|
+
resolve();
|
|
469
|
+
callbacks?.onInstall
|
|
470
|
+
? callbacks.onInstall(event, user)
|
|
471
|
+
: callbacks?.onSuccess?.(event, user);
|
|
472
|
+
},
|
|
473
|
+
onError: (err: Error) => {
|
|
474
|
+
reject(err);
|
|
475
|
+
callbacks?.onError?.(err);
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
if (
|
|
481
|
+
mapObjectFields &&
|
|
482
|
+
!this.project.accessibleFeatures.includes(ConnectAddOn.DynamicFieldMapper)
|
|
483
|
+
) {
|
|
484
|
+
throw new Error(
|
|
485
|
+
`Dynamic Field Mapping is available on our Enterprise plan. Contact sales@useparagon.com to learn more or upgrade.`,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
this.validateAction(action);
|
|
490
|
+
|
|
491
|
+
if (overrideRedirectUrl && !isValidUrl(overrideRedirectUrl)) {
|
|
492
|
+
throw new Error(`${overrideRedirectUrl} is not valid url.`);
|
|
493
|
+
}
|
|
494
|
+
this.endUserIntegrationConfig[action] = {
|
|
495
|
+
mapObjectFields,
|
|
496
|
+
overrideRedirectUrl: overrideRedirectUrl ? sanitizeUrl(overrideRedirectUrl) : undefined,
|
|
497
|
+
};
|
|
498
|
+
const integration: IConnectIntegrationWithCredentialInfo = Object.values(
|
|
499
|
+
this.loadedIntegrations,
|
|
500
|
+
).find(
|
|
501
|
+
(integration: IConnectIntegrationWithCredentialInfo) =>
|
|
502
|
+
getIntegrationTypeName(integration) === action,
|
|
503
|
+
) as IConnectIntegrationWithCredentialInfo;
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* in chrome getting issue for asynchronous popup opening
|
|
507
|
+
* see PARA-1505
|
|
508
|
+
*/
|
|
509
|
+
if (params.isApiInstallation && !shouldShowPreAuthInputs(integration)) {
|
|
510
|
+
void startOAuthFlow({
|
|
511
|
+
context: {
|
|
512
|
+
user: this.userState,
|
|
513
|
+
projectId: this.projectId,
|
|
514
|
+
environments: this.environments,
|
|
515
|
+
endUserIntegrationConfig: this.modalState.integration
|
|
516
|
+
? this.endUserIntegrationConfig[this.modalState.integration.type]
|
|
517
|
+
: undefined,
|
|
518
|
+
} as IConnectUserContext,
|
|
519
|
+
integration,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
this.setModalState({
|
|
524
|
+
integration,
|
|
525
|
+
config: this.loadedConfigs[action],
|
|
526
|
+
apiInstallationOptions: {
|
|
527
|
+
isApiInstallation: params.isApiInstallation,
|
|
528
|
+
showPortalAfterInstall: params.showPortalAfterInstall,
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
} catch (err) {
|
|
532
|
+
this.emitError(err, action);
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async _oauthCallback(credential: ICredential, credentialId?: string): Promise<void> {
|
|
538
|
+
const { integration } = this.modalState;
|
|
539
|
+
if (!integration) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const type = getIntegrationTypeName(integration) as string;
|
|
543
|
+
try {
|
|
544
|
+
const user = this.userState as AuthenticatedConnectUser;
|
|
545
|
+
const requestURI = credentialId
|
|
546
|
+
? `/sdk/credentials/${credentialId}/complete-setup`
|
|
547
|
+
: `/sdk/credentials`;
|
|
548
|
+
const oauthResponse = await this.sendConnectRequest<IConnectCredential>(requestURI, {
|
|
549
|
+
method: 'POST',
|
|
550
|
+
body: JSON.stringify({
|
|
551
|
+
integrationId: integration.id,
|
|
552
|
+
config: {},
|
|
553
|
+
payload: credential,
|
|
554
|
+
}),
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
if (!oauthResponse) {
|
|
558
|
+
throw new Error('Unable to save oauth credentials');
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const { id, config, providerId, status, providerData } = oauthResponse;
|
|
562
|
+
this.updateAuthenticatedUser({
|
|
563
|
+
integrations: {
|
|
564
|
+
...user.integrations,
|
|
565
|
+
[type]: {
|
|
566
|
+
...user.integrations[type],
|
|
567
|
+
...config,
|
|
568
|
+
credentialStatus: status,
|
|
569
|
+
enabled: status === CredentialStatus.VALID,
|
|
570
|
+
credentialId: id,
|
|
571
|
+
providerId,
|
|
572
|
+
providerData,
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
if (status === CredentialStatus.VALID) {
|
|
578
|
+
this.triggerSDKEvent({
|
|
579
|
+
type: SDK_EVENT.ON_INTEGRATION_INSTALL,
|
|
580
|
+
integrationId: integration.id,
|
|
581
|
+
integrationType: type,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
} catch (err) {
|
|
585
|
+
this.emitError(err, type);
|
|
586
|
+
await this._oauthErrorCallback(err.message);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// tslint:disable-next-line: function-name
|
|
591
|
+
async _oauthErrorCallback(errorMessage: string | object): Promise<void> {
|
|
592
|
+
let message: string | object = errorMessage;
|
|
593
|
+
//also log error in console
|
|
594
|
+
console.error('Failed to connect account to Paragon over OAuth', message);
|
|
595
|
+
|
|
596
|
+
if (typeof message === 'object') {
|
|
597
|
+
message = JSON.stringify(message);
|
|
598
|
+
}
|
|
599
|
+
// currently alerting generic error or error message for oauth failure
|
|
600
|
+
// TODO: to make generic oauth error modal to show errormessage property
|
|
601
|
+
window.alert(`Something went wrong connecting your account. Please try again or ${message}`);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Send a Connect API request. Automatically handles authorization and errors.
|
|
606
|
+
*
|
|
607
|
+
* @param path The path of the request, including the leading slash, i.e. `/sdk/actions`
|
|
608
|
+
* @param init Options for the request, excluding headers (automatically added).
|
|
609
|
+
* @param prefixWithProjectPath Defaults to true. Prepends /projects/${this.projectId}` to the
|
|
610
|
+
* path.
|
|
611
|
+
*/
|
|
612
|
+
async sendConnectRequest<TResponse>(
|
|
613
|
+
path: string,
|
|
614
|
+
init?: RequestInit & { cacheResult?: boolean },
|
|
615
|
+
prefixWithProjectPath: boolean = true,
|
|
616
|
+
): Promise<TResponse | undefined> {
|
|
617
|
+
if (!this.userState.authenticated || !this.projectId) {
|
|
618
|
+
throw new Error(
|
|
619
|
+
`Connect SDK attempted to make an API request, but no user was authenticated.
|
|
620
|
+
Call paragon.authenticate(<projectId>, <user token>) before using the SDK.`,
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
const url: string = `${this.environments.ZEUS_PUBLIC_URL}${
|
|
624
|
+
prefixWithProjectPath ? `/projects/${this.projectId}` : ''
|
|
625
|
+
}${path}`;
|
|
626
|
+
|
|
627
|
+
const cacheKey: string = hash(JSON.stringify({ url, payload: { ...init } }));
|
|
628
|
+
const cachedResult = await this.cachedApiResponse.get(cacheKey, true);
|
|
629
|
+
if (init?.cacheResult && cachedResult) {
|
|
630
|
+
return cachedResult;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const shouldCacheRequest: boolean = Boolean(!init || init.method === 'GET' || init.cacheResult);
|
|
634
|
+
|
|
635
|
+
if (shouldCacheRequest && typeof this.keyToRequestPromiseMap[cacheKey] === 'object') {
|
|
636
|
+
return this.keyToRequestPromiseMap[cacheKey] as Promise<TResponse | undefined>;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const accessToken: string = this.userState.token;
|
|
640
|
+
|
|
641
|
+
const onGoingRequest: Promise<TResponse | undefined> = new Promise((resolve, reject) => {
|
|
642
|
+
(async () => {
|
|
643
|
+
try {
|
|
644
|
+
const result = await this.sendRequest<TResponse>(
|
|
645
|
+
url,
|
|
646
|
+
{
|
|
647
|
+
...init,
|
|
648
|
+
headers: {
|
|
649
|
+
Accept: 'application/json',
|
|
650
|
+
'Content-Type': 'application/json',
|
|
651
|
+
...init?.headers,
|
|
652
|
+
Authorization: `Bearer ${accessToken}`,
|
|
653
|
+
},
|
|
654
|
+
},
|
|
655
|
+
shouldCacheRequest ? cacheKey : undefined,
|
|
656
|
+
);
|
|
657
|
+
resolve(result);
|
|
658
|
+
} catch (err) {
|
|
659
|
+
reject(err);
|
|
660
|
+
}
|
|
661
|
+
})();
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
if (shouldCacheRequest) {
|
|
665
|
+
this.keyToRequestPromiseMap[cacheKey] = onGoingRequest;
|
|
666
|
+
}
|
|
667
|
+
return onGoingRequest;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
private async sendRequest<TResponse>(
|
|
671
|
+
url: string,
|
|
672
|
+
init: RequestInit,
|
|
673
|
+
cacheKey?: string,
|
|
674
|
+
): Promise<TResponse | undefined> {
|
|
675
|
+
let response: Response | undefined;
|
|
676
|
+
let responseBody: TResponse | undefined;
|
|
677
|
+
let responseError: Error | undefined;
|
|
678
|
+
try {
|
|
679
|
+
response = await fetch(url, init);
|
|
680
|
+
} catch (error) {
|
|
681
|
+
responseError = error;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if ((response && !response.ok) || responseError) {
|
|
685
|
+
const requestReturnedAuthorizationError: boolean =
|
|
686
|
+
response?.status === 401 || response?.status === 403;
|
|
687
|
+
if (!url.includes('proxy') && requestReturnedAuthorizationError) {
|
|
688
|
+
this.logout();
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const errorMessage: string = (await response?.text()) || (responseError?.message as string);
|
|
692
|
+
|
|
693
|
+
if (cacheKey) {
|
|
694
|
+
await this.cachedApiResponse.del(cacheKey);
|
|
695
|
+
delete this.keyToRequestPromiseMap[cacheKey];
|
|
696
|
+
}
|
|
697
|
+
throw new Error(errorMessage);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
try {
|
|
701
|
+
responseBody = await response?.json();
|
|
702
|
+
} catch {
|
|
703
|
+
responseBody = undefined;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (cacheKey) {
|
|
707
|
+
await this.cachedApiResponse.set(cacheKey, responseBody);
|
|
708
|
+
delete this.keyToRequestPromiseMap[cacheKey];
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return responseBody;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
async sendProxyRequest<TResponse>(
|
|
715
|
+
action: VisibleConnectAction,
|
|
716
|
+
path: string,
|
|
717
|
+
init: {
|
|
718
|
+
method: RequestInit['method'];
|
|
719
|
+
body: RequestInit['body'] | object;
|
|
720
|
+
headers: RequestInit['headers'];
|
|
721
|
+
},
|
|
722
|
+
): Promise<TResponse | undefined> {
|
|
723
|
+
let baseProxyPath: string = `/sdk/proxy/${action}`;
|
|
724
|
+
if (isCustomIntegrationTypeName(action)) {
|
|
725
|
+
baseProxyPath = `/sdk/proxy/custom/${
|
|
726
|
+
Object.values(this.loadedIntegrations).find(
|
|
727
|
+
(integration: IConnectIntegrationWithCredentialInfo) => {
|
|
728
|
+
if (integration.customIntegration) {
|
|
729
|
+
return integration.customIntegration.slug === action;
|
|
730
|
+
}
|
|
731
|
+
return false;
|
|
732
|
+
},
|
|
733
|
+
)?.id
|
|
734
|
+
}`;
|
|
735
|
+
}
|
|
736
|
+
const pathWithLeadingSlash: string = path.startsWith('/') ? path : `/${path}`;
|
|
737
|
+
const response = await this.sendConnectRequest<{ output: TResponse }>(
|
|
738
|
+
`${baseProxyPath}${pathWithLeadingSlash}`,
|
|
739
|
+
{
|
|
740
|
+
method: init.method,
|
|
741
|
+
body: typeof init.body === 'object' ? JSON.stringify(init.body) : init.body,
|
|
742
|
+
headers: {
|
|
743
|
+
'Content-Type': INFER_CONTENT_TYPE_FROM_CONNECT_OPTIONS,
|
|
744
|
+
...init.headers,
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
);
|
|
748
|
+
return response?.output;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async event(name: string, payload: Record<string, unknown>): Promise<void> {
|
|
752
|
+
await this.sendConnectRequest(
|
|
753
|
+
`/v2/projects/${this.projectId}/sdk/events/trigger`,
|
|
754
|
+
{
|
|
755
|
+
method: 'POST',
|
|
756
|
+
body: JSON.stringify({
|
|
757
|
+
name,
|
|
758
|
+
payload,
|
|
759
|
+
}),
|
|
760
|
+
},
|
|
761
|
+
false,
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* @summary this will be called to close the modal
|
|
767
|
+
*/
|
|
768
|
+
onClose(): void {
|
|
769
|
+
const { integration, apiInstallationOptions } = this.modalState;
|
|
770
|
+
|
|
771
|
+
if (!integration) {
|
|
772
|
+
// This might happen if the "X" button is closed, because `onClose` will fire twice. We
|
|
773
|
+
// ignore it if this is the case.
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
this.triggerSDKEvent({
|
|
777
|
+
type: SDK_EVENT.ON_PORTAL_CLOSE,
|
|
778
|
+
integrationId: integration.id,
|
|
779
|
+
integrationType: getIntegrationTypeName(integration) as string,
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
if (this.originalBodyOverflow !== PARAGON_OVERFLOW_EMPTY_VALUE) {
|
|
783
|
+
window.document.body.style.overflow = this.originalBodyOverflow as string;
|
|
784
|
+
this.originalBodyOverflow = PARAGON_OVERFLOW_EMPTY_VALUE;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Not clearing integration state as its required in oauth_callback
|
|
788
|
+
if (!apiInstallationOptions?.isApiInstallation) {
|
|
789
|
+
this.setModalState({ integration: null });
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* @summary this will be called when modal will be visible
|
|
795
|
+
*/
|
|
796
|
+
onOpen(): void {
|
|
797
|
+
const { integration } = this.modalState;
|
|
798
|
+
if (!integration) {
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
this.triggerSDKEvent({
|
|
802
|
+
type: SDK_EVENT.ON_PORTAL_OPEN,
|
|
803
|
+
integrationId: integration.id,
|
|
804
|
+
integrationType: getIntegrationTypeName(integration) as string,
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
if (this.root && this.originalBodyOverflow === PARAGON_OVERFLOW_EMPTY_VALUE) {
|
|
808
|
+
const boundingRect = this.root.getBoundingClientRect();
|
|
809
|
+
// If the Connect Portal is bigger than the window
|
|
810
|
+
if (window.innerHeight <= boundingRect.height && window.innerWidth <= boundingRect.width) {
|
|
811
|
+
this.originalBodyOverflow = window.document.body.style.overflow;
|
|
812
|
+
window.document.body.style.overflow = 'hidden';
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* this is shared in ConnectUserContext
|
|
819
|
+
* so that event can be trigger from components
|
|
820
|
+
* @param eventInfo
|
|
821
|
+
*/
|
|
822
|
+
triggerSDKEvent(eventInfo: EventInfo): void {
|
|
823
|
+
this.triggerEvent(eventInfo, this.userState as AuthenticatedConnectUser);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* get single or all metadata info for integrations
|
|
828
|
+
* @param integrationKeyName
|
|
829
|
+
* @returns
|
|
830
|
+
*/
|
|
831
|
+
getIntegrationMetadata(
|
|
832
|
+
integrationKeyName?: string,
|
|
833
|
+
): IIntegrationMetadata | IIntegrationMetadata[] {
|
|
834
|
+
if (!integrationKeyName) {
|
|
835
|
+
return Object.values(this.metadata);
|
|
836
|
+
} else if (!this.metadata[integrationKeyName]) {
|
|
837
|
+
throw new Error(`${integrationKeyName} is not a valid integration name`);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return this.metadata[integrationKeyName];
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* trigger connect endpoint trigger workflow
|
|
845
|
+
* @param workflowId
|
|
846
|
+
* @param payload
|
|
847
|
+
*/
|
|
848
|
+
triggerWorkflow(
|
|
849
|
+
workflowId: string,
|
|
850
|
+
{ body = {}, query = {}, headers = {} }: TriggerWorkflowRequest = {
|
|
851
|
+
body: {},
|
|
852
|
+
headers: {},
|
|
853
|
+
query: {},
|
|
854
|
+
},
|
|
855
|
+
): Promise<object | undefined> {
|
|
856
|
+
if (!workflowId) {
|
|
857
|
+
throw new Error('workflowId is required.');
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const queryString: string = new URLSearchParams(query).toString();
|
|
861
|
+
|
|
862
|
+
return this.sendConnectRequest<object>(
|
|
863
|
+
`/sdk/triggers/${workflowId}?${queryString}`,
|
|
864
|
+
{
|
|
865
|
+
method: 'POST',
|
|
866
|
+
body: JSON.stringify(body),
|
|
867
|
+
headers,
|
|
868
|
+
},
|
|
869
|
+
true,
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* for programmatically installing an integration
|
|
875
|
+
*/
|
|
876
|
+
installIntegration(
|
|
877
|
+
action: Action,
|
|
878
|
+
params: Omit<InstallOptions, 'isApiInstallation'> = { showPortalAfterInstall: false },
|
|
879
|
+
): Promise<void> {
|
|
880
|
+
if (!this.userState.authenticated) {
|
|
881
|
+
throw new Error(
|
|
882
|
+
`User not authenticated, Call paragon.authenticate(<projectId>, <user token>) before using the SDK.`,
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
this.ensureHeadlessIsSupported();
|
|
886
|
+
|
|
887
|
+
const isAlreadyEnabled = this.validateAction(action, true);
|
|
888
|
+
if (isAlreadyEnabled) {
|
|
889
|
+
throw new Error(`Integration "${action}" is already installed.`);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// as we have added integration dependency in ConnectModal useEffect to trigger enableIntegration
|
|
893
|
+
// so this reset is needed incase previous one fails
|
|
894
|
+
this.setModalState({ integration: null });
|
|
895
|
+
|
|
896
|
+
return this.connect(action, { ...params, isApiInstallation: true });
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* gates headless feature to pro and enterprise users
|
|
901
|
+
*/
|
|
902
|
+
ensureHeadlessIsSupported(): void {
|
|
903
|
+
if (!this.project.accessibleFeatures.includes(ConnectAddOn.HeadlessConnectPortal)) {
|
|
904
|
+
throw new Error(
|
|
905
|
+
'Headless Connect Portal is available on our Pro plan and above. Contact sales@useparagon.com to learn more or upgrade.',
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
async uninstallIntegration(action: Action): Promise<void> {
|
|
911
|
+
if (!this.userState.authenticated) {
|
|
912
|
+
throw new Error(
|
|
913
|
+
`User not authenticated, Call paragon.authenticate(<projectId>, <user token>) before using the SDK.`,
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
this.ensureHeadlessIsSupported();
|
|
917
|
+
if (!this.validateAction(action, true)) {
|
|
918
|
+
throw new Error(`Integration "${action}" is not installed.`);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const integrationId = this.loadedIntegrations[action].id;
|
|
922
|
+
await this.sendConnectRequest(`/sdk/integrations/${integrationId}`, {
|
|
923
|
+
method: 'DELETE',
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
this.updateAuthenticatedUser({
|
|
927
|
+
integrations: {
|
|
928
|
+
...this.userState.integrations,
|
|
929
|
+
[action]: { enabled: false, configuredWorkflows: {} },
|
|
930
|
+
},
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
this.saveState();
|
|
934
|
+
|
|
935
|
+
this.triggerSDKEvent({
|
|
936
|
+
type: SDK_EVENT.ON_INTEGRATION_UNINSTALL,
|
|
937
|
+
integrationId,
|
|
938
|
+
integrationType: action,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
async disableWorkflow(workflowId: string): Promise<void> {
|
|
943
|
+
if (!this.userState.authenticated) {
|
|
944
|
+
throw new Error(
|
|
945
|
+
`User not authenticated, Call paragon.authenticate(<projectId>, <user token>) before using the SDK.`,
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
if (!this.isWorkflowEnabled(workflowId)) {
|
|
950
|
+
throw new Error(`Workflow ${workflowId} cannot be disabled for this user.`);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
await this.sendConnectRequest<IConnectCredential>(`/sdk/workflows/${workflowId}`, {
|
|
954
|
+
method: 'DELETE',
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
await this.updateLocalState();
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
private async updateLocalState(): Promise<void> {
|
|
961
|
+
const [userData, integrations] = await Promise.all([
|
|
962
|
+
this.fetchUserData(),
|
|
963
|
+
this.fetchIntegrations(),
|
|
964
|
+
]);
|
|
965
|
+
|
|
966
|
+
this.updateAuthenticatedUser({ integrations: userData.integrations, meta: userData.meta });
|
|
967
|
+
this.updateIntegrations(integrations);
|
|
968
|
+
this.saveState();
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* also returns false if unable to find workflow in configured workflow property of any integration
|
|
973
|
+
*/
|
|
974
|
+
private isWorkflowEnabled(workflowId: string): boolean {
|
|
975
|
+
const result = Object.values((this.userState as AuthenticatedConnectUser).integrations).find(
|
|
976
|
+
(integration) => integration?.configuredWorkflows?.[workflowId]?.enabled,
|
|
977
|
+
);
|
|
978
|
+
|
|
979
|
+
return result === undefined ? false : true;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* gets the user data from api `/sdk/me`
|
|
984
|
+
*/
|
|
985
|
+
private async fetchUserData(): Promise<IConnectUser> {
|
|
986
|
+
const userData = await this.sendConnectRequest<IConnectUser>('/sdk/me');
|
|
987
|
+
if (!userData) {
|
|
988
|
+
throw new Error('Unable to get user Data');
|
|
989
|
+
} else {
|
|
990
|
+
return userData;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Gets the integrations from api `/sdk/integrations`
|
|
996
|
+
* Required this another request for integrations, as `/sdk/me` provides only minimal data for integrations
|
|
997
|
+
* we also need integration config and other credentials info which is not included in `sdk/me`
|
|
998
|
+
*/
|
|
999
|
+
private async fetchIntegrations(): Promise<IConnectIntegrationWithCredentialInfo[]> {
|
|
1000
|
+
const integrations = await this.sendConnectRequest<IConnectIntegrationWithCredentialInfo[]>(
|
|
1001
|
+
'/sdk/integrations',
|
|
1002
|
+
);
|
|
1003
|
+
|
|
1004
|
+
if (!integrations) {
|
|
1005
|
+
throw new Error('Unable to fetch integrations');
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
return integrations;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Updates internal state where integration information may be used.
|
|
1013
|
+
*/
|
|
1014
|
+
private updateIntegrations(integrations: IConnectIntegrationWithCredentialInfo[]): void {
|
|
1015
|
+
integrations.forEach((integration: IConnectIntegrationWithCredentialInfo) => {
|
|
1016
|
+
if (integration.configs.length) {
|
|
1017
|
+
try {
|
|
1018
|
+
const type = getIntegrationTypeName(integration) as string;
|
|
1019
|
+
|
|
1020
|
+
// update loaded configs
|
|
1021
|
+
this.loadedConfigs[type] = integration.configs[0].values;
|
|
1022
|
+
|
|
1023
|
+
// update loaded integrations
|
|
1024
|
+
this.loadedIntegrations[type] = integration;
|
|
1025
|
+
|
|
1026
|
+
// update integration metadata
|
|
1027
|
+
if (Boolean(integration.isActive)) {
|
|
1028
|
+
this.metadata[type] = {
|
|
1029
|
+
type,
|
|
1030
|
+
name:
|
|
1031
|
+
integration.customIntegration?.name ?? ActionCatalogConfigs[integration.type].name,
|
|
1032
|
+
brandColor:
|
|
1033
|
+
integration.configs[0].values.accentColor ??
|
|
1034
|
+
ActionCatalogConfigs[integration.type].brandColor,
|
|
1035
|
+
icon:
|
|
1036
|
+
integration.customIntegration?.icon ??
|
|
1037
|
+
`${this.environments.CDN_PUBLIC_URL}/integrations/${
|
|
1038
|
+
overrideActionAlias[integration.type] || integration.type
|
|
1039
|
+
}.svg`,
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// update modal state (if being used)
|
|
1044
|
+
if (this.modalState.integration?.id === integration.id) {
|
|
1045
|
+
this.setModalState({ integration });
|
|
1046
|
+
}
|
|
1047
|
+
} catch (err) {
|
|
1048
|
+
console.warn(err);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
async setUserMetadata(meta: PersonaMeta): Promise<AuthenticatedConnectUser> {
|
|
1055
|
+
await this.sendConnectRequest<IPersona>('/sdk/me', {
|
|
1056
|
+
method: 'PATCH',
|
|
1057
|
+
body: JSON.stringify({ meta }),
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
await this.updateLocalState();
|
|
1061
|
+
this.render();
|
|
1062
|
+
|
|
1063
|
+
return this.userState as AuthenticatedConnectUser;
|
|
1064
|
+
}
|
|
1065
|
+
}
|