@finatic/client 0.0.138 → 0.0.140

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.
Files changed (40) hide show
  1. package/README.md +278 -461
  2. package/dist/index.d.ts +59 -516
  3. package/dist/index.js +337 -456
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +338 -456
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/types/core/client/ApiClient.d.ts +12 -26
  8. package/dist/types/core/client/FinaticConnect.d.ts +20 -103
  9. package/dist/types/index.d.ts +1 -2
  10. package/dist/types/mocks/MockApiClient.d.ts +2 -4
  11. package/dist/types/mocks/utils.d.ts +0 -5
  12. package/dist/types/types/api/auth.d.ts +12 -30
  13. package/dist/types/types/api/broker.d.ts +1 -1
  14. package/dist/types/types/connect.d.ts +4 -1
  15. package/package.json +7 -3
  16. package/src/core/client/ApiClient.ts +1721 -0
  17. package/src/core/client/FinaticConnect.ts +1476 -0
  18. package/src/core/portal/PortalUI.ts +300 -0
  19. package/src/index.d.ts +23 -0
  20. package/src/index.ts +87 -0
  21. package/src/mocks/MockApiClient.ts +1032 -0
  22. package/src/mocks/MockDataProvider.ts +986 -0
  23. package/src/mocks/MockFactory.ts +97 -0
  24. package/src/mocks/utils.ts +133 -0
  25. package/src/themes/portalPresets.ts +1307 -0
  26. package/src/types/api/auth.ts +112 -0
  27. package/src/types/api/broker.ts +330 -0
  28. package/src/types/api/core.ts +53 -0
  29. package/src/types/api/errors.ts +35 -0
  30. package/src/types/api/orders.ts +45 -0
  31. package/src/types/api/portfolio.ts +59 -0
  32. package/src/types/common/pagination.ts +138 -0
  33. package/src/types/connect.ts +56 -0
  34. package/src/types/index.ts +25 -0
  35. package/src/types/portal.ts +214 -0
  36. package/src/types/ui/theme.ts +105 -0
  37. package/src/utils/brokerUtils.ts +85 -0
  38. package/src/utils/errors.ts +104 -0
  39. package/src/utils/events.ts +54 -0
  40. package/src/utils/themeUtils.ts +146 -0
@@ -0,0 +1,1476 @@
1
+ import { EventEmitter } from '../../utils/events';
2
+ import { ApiClient } from './ApiClient';
3
+ import { PortalUI } from '../portal/PortalUI';
4
+ import { SessionError, AuthenticationError, CompanyAccessError, ApiError, ValidationError } from '../../utils/errors';
5
+ import { MockFactory } from '../../mocks/MockFactory';
6
+ import { PaginatedResult } from '../../types/common/pagination';
7
+ import { UserToken, SessionState } from '../../types/api/auth';
8
+ import { Order, OrderResponse, TradingContext } from '../../types/api/orders';
9
+ import {
10
+ BrokerDataOptions,
11
+ BrokerAccount,
12
+ BrokerOrder,
13
+ BrokerPosition,
14
+ BrokerBalance,
15
+ BrokerInfo,
16
+ BrokerOrderParams,
17
+ BrokerConnection,
18
+ OrdersFilter,
19
+ PositionsFilter,
20
+ AccountsFilter,
21
+ BalancesFilter,
22
+ BrokerDataOrder,
23
+ BrokerDataPosition,
24
+ BrokerDataAccount,
25
+ DisconnectCompanyResponse,
26
+ } from '../../types/api/broker';
27
+ import { FinaticConnectOptions, PortalOptions } from '../../types/connect';
28
+ import { appendThemeToURL } from '../../utils/themeUtils';
29
+ import { appendBrokerFilterToURL } from '../../utils/brokerUtils';
30
+ // Supabase import removed - SDK no longer depends on Supabase
31
+
32
+ interface DeviceInfo {
33
+ ip_address: string;
34
+ user_agent: string;
35
+ fingerprint: string;
36
+ }
37
+
38
+ export class FinaticConnect extends EventEmitter {
39
+ private static instance: FinaticConnect | null = null;
40
+ private apiClient: ApiClient | any; // Allow both ApiClient and MockApiClient
41
+ private portalUI: PortalUI;
42
+ private options: FinaticConnectOptions;
43
+ private userToken: UserToken | null = null;
44
+ private sessionId: string | null = null;
45
+ private companyId: string;
46
+ private baseUrl: string;
47
+ private readonly BROKER_LIST_CACHE_KEY = 'finatic_broker_list_cache';
48
+ private readonly BROKER_LIST_CACHE_VERSION = '1.0';
49
+ private readonly BROKER_LIST_CACHE_DURATION = 1000 * 60 * 60 * 24; // 24 hours in milliseconds
50
+ private readonly deviceInfo?: DeviceInfo;
51
+ private currentSessionState: string | null = null;
52
+
53
+ // Session keep-alive mechanism
54
+ private sessionKeepAliveInterval: ReturnType<typeof setInterval> | null = null;
55
+ private readonly SESSION_KEEP_ALIVE_INTERVAL = 1000 * 60 * 5; // 5 minutes
56
+ private readonly SESSION_VALIDATION_TIMEOUT = 1000 * 30; // 30 seconds
57
+ private readonly SESSION_REFRESH_BUFFER_HOURS = 16; // Refresh session at 16 hours
58
+ private sessionStartTime: number | null = null;
59
+
60
+ constructor(options: FinaticConnectOptions, deviceInfo?: DeviceInfo) {
61
+ super();
62
+ this.options = options;
63
+ this.baseUrl = options.baseUrl || 'https://api.finatic.dev';
64
+ this.apiClient = MockFactory.createApiClient(this.baseUrl, deviceInfo);
65
+ this.portalUI = new PortalUI(this.baseUrl);
66
+ this.deviceInfo = deviceInfo;
67
+
68
+ // Extract company ID from token
69
+ try {
70
+ // Validate token exists
71
+ if (!options.token) {
72
+ throw new Error('Token is required but not provided');
73
+ }
74
+
75
+ // Check if token is in JWT format (contains dots)
76
+ if (options.token.includes('.')) {
77
+ const tokenParts = options.token.split('.');
78
+ if (tokenParts.length === 3) {
79
+ const payload = JSON.parse(atob(tokenParts[1]));
80
+ this.companyId = payload.company_id;
81
+ } else {
82
+ throw new Error('Invalid JWT token format');
83
+ }
84
+ } else {
85
+ // Handle UUID format token
86
+ // For UUID tokens, we'll get the company_id from the session start response
87
+ this.companyId = ''; // Will be set after session start
88
+ }
89
+ } catch (error: unknown) {
90
+ if (error instanceof Error) {
91
+ throw new Error('Failed to parse token: ' + error.message);
92
+ } else {
93
+ throw new Error('Failed to parse token: Unknown error');
94
+ }
95
+ }
96
+
97
+ // Set up event listeners for callbacks
98
+ if (this.options.onSuccess) {
99
+ this.on('success', this.options.onSuccess);
100
+ }
101
+ if (this.options.onError) {
102
+ this.on('error', this.options.onError);
103
+ }
104
+ if (this.options.onClose) {
105
+ this.on('close', this.options.onClose);
106
+ }
107
+
108
+ // Register automatic session cleanup
109
+ this.registerSessionCleanup();
110
+ }
111
+
112
+ private async linkUserToSession(userId: string): Promise<boolean> {
113
+ try {
114
+ if (!this.sessionId) {
115
+ console.error('No session ID available for user linking');
116
+ return false;
117
+ }
118
+
119
+ // Call API endpoint to authenticate user with session
120
+ const response = await this.apiClient.request('/session/authenticate', {
121
+ method: 'POST',
122
+ body: {
123
+ session_id: this.sessionId,
124
+ user_id: userId,
125
+ },
126
+ });
127
+
128
+ if (response.error) {
129
+ console.error('Failed to link user to session:', response.error);
130
+ return false;
131
+ }
132
+
133
+ console.log('User linked to session successfully');
134
+ return true;
135
+ } catch (error) {
136
+ console.error('Error linking user to session:', error);
137
+ return false;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Store user ID for authentication state persistence
143
+ * @param userId - The user ID to store
144
+ */
145
+ private storeUserId(userId: string): void {
146
+ // Initialize userToken if it doesn't exist
147
+ if (!this.userToken) {
148
+ this.userToken = {
149
+ user_id: userId,
150
+ };
151
+ } else {
152
+ // Update existing userToken with new userId
153
+ this.userToken.user_id = userId;
154
+ }
155
+
156
+ // Set user ID in ApiClient for session context
157
+ this.apiClient.setSessionContext(this.sessionId || '', this.companyId, undefined);
158
+ }
159
+
160
+ /**
161
+ * Check if the user is fully authenticated (has userId in session context)
162
+ * @returns True if the user is fully authenticated and ready for API calls
163
+ */
164
+ public async isAuthenticated(): Promise<boolean> {
165
+ // Check internal session context only - no localStorage dependency
166
+ return this.userToken?.user_id !== undefined && this.userToken?.user_id !== null;
167
+ }
168
+
169
+
170
+ /**
171
+ * Get user's orders with pagination and optional filtering
172
+ * @param params - Query parameters including page, perPage, and filters
173
+ * @returns Promise with paginated result that supports navigation
174
+ */
175
+ public async getOrders(
176
+ page: number = 1,
177
+ perPage: number = 100,
178
+ options?: BrokerDataOptions,
179
+ filters?: OrdersFilter
180
+ ): Promise<PaginatedResult<BrokerDataOrder[]>> {
181
+ if (!(await this.isAuthenticated())) {
182
+ throw new AuthenticationError('User is not authenticated');
183
+ }
184
+
185
+ const result = await this.apiClient.getBrokerOrdersPage(page, perPage, filters);
186
+
187
+ // Add navigation methods to the result
188
+ const paginatedResult = result as any;
189
+ paginatedResult.next_page = async () => {
190
+ if (paginatedResult.hasNext) {
191
+ return this.apiClient.getBrokerOrdersPage(page + 1, perPage, filters);
192
+ }
193
+ throw new Error('No next page available');
194
+ };
195
+ paginatedResult.previous_page = async () => {
196
+ if (paginatedResult.has_previous) {
197
+ return this.apiClient.getBrokerOrdersPage(page - 1, perPage, filters);
198
+ }
199
+ throw new Error('No previous page available');
200
+ };
201
+
202
+ return paginatedResult;
203
+ }
204
+
205
+ /**
206
+ * Get user's positions with pagination and optional filtering
207
+ * @param params - Query parameters including page, perPage, and filters
208
+ * @returns Promise with paginated result that supports navigation
209
+ */
210
+ public async getPositions(
211
+ page: number = 1,
212
+ perPage: number = 100,
213
+ options?: BrokerDataOptions,
214
+ filters?: PositionsFilter
215
+ ): Promise<PaginatedResult<BrokerDataPosition[]>> {
216
+ if (!(await this.isAuthenticated())) {
217
+ throw new AuthenticationError('User is not authenticated');
218
+ }
219
+
220
+ const result = await this.apiClient.getBrokerPositionsPage(page, perPage, filters);
221
+
222
+ // Add navigation methods to the result
223
+ const paginatedResult = result as any;
224
+ paginatedResult.next_page = async () => {
225
+ if (paginatedResult.hasNext) {
226
+ return this.apiClient.getBrokerPositionsPage(page + 1, perPage, filters);
227
+ }
228
+ throw new Error('No next page available');
229
+ };
230
+ paginatedResult.previous_page = async () => {
231
+ if (paginatedResult.has_previous) {
232
+ return this.apiClient.getBrokerPositionsPage(page - 1, perPage, filters);
233
+ }
234
+ throw new Error('No previous page available');
235
+ };
236
+
237
+ return paginatedResult;
238
+ }
239
+
240
+ /**
241
+ * Get user's accounts with pagination and optional filtering
242
+ * @param params - Query parameters including page, perPage, and filters
243
+ * @returns Promise with paginated result that supports navigation
244
+ */
245
+ public async getAccounts(
246
+ page: number = 1,
247
+ perPage: number = 100,
248
+ options?: BrokerDataOptions,
249
+ filters?: AccountsFilter
250
+ ): Promise<PaginatedResult<BrokerDataAccount[]>> {
251
+ if (!(await this.isAuthenticated())) {
252
+ throw new AuthenticationError('User is not authenticated');
253
+ }
254
+
255
+ const result = await this.apiClient.getBrokerAccountsPage(page, perPage, filters);
256
+
257
+ // Add navigation methods to the result
258
+ const paginatedResult = result as any;
259
+ paginatedResult.next_page = async () => {
260
+ if (paginatedResult.hasNext) {
261
+ return this.apiClient.getBrokerAccountsPage(page + 1, perPage, filters);
262
+ }
263
+ throw new Error('No next page available');
264
+ };
265
+ paginatedResult.previous_page = async () => {
266
+ if (paginatedResult.has_previous) {
267
+ return this.apiClient.getBrokerAccountsPage(page - 1, perPage, filters);
268
+ }
269
+ throw new Error('No previous page available');
270
+ };
271
+
272
+ return paginatedResult;
273
+ }
274
+
275
+ /**
276
+ * Get user's balances with pagination and optional filtering
277
+ * @param params - Query parameters including page, perPage, and filters
278
+ * @returns Promise with paginated result that supports navigation
279
+ */
280
+ public async getBalances(
281
+ page: number = 1,
282
+ perPage: number = 100,
283
+ options?: BrokerDataOptions,
284
+ filters?: BalancesFilter
285
+ ): Promise<PaginatedResult<BrokerBalance[]>> {
286
+ if (!(await this.isAuthenticated())) {
287
+ throw new AuthenticationError('User is not authenticated');
288
+ }
289
+
290
+ const result = await this.apiClient.getBrokerBalancesPage(page, perPage, filters);
291
+
292
+ // Add navigation methods to the result
293
+ const paginatedResult = result as any;
294
+ paginatedResult.next_page = async () => {
295
+ if (paginatedResult.hasNext) {
296
+ return this.apiClient.getBrokerBalancesPage(page + 1, perPage, filters);
297
+ }
298
+ throw new Error('No next page available');
299
+ };
300
+ paginatedResult.previous_page = async () => {
301
+ if (paginatedResult.has_previous) {
302
+ return this.apiClient.getBrokerBalancesPage(page - 1, perPage, filters);
303
+ }
304
+ throw new Error('No previous page available');
305
+ };
306
+
307
+ return paginatedResult;
308
+ }
309
+
310
+ /**
311
+ * Initialize the Finatic Connect SDK
312
+ * @param token - The portal token from your backend
313
+ * @param userId - Optional: The user ID if you have it from a previous session
314
+ * @param options - Optional configuration including baseUrl
315
+ * @returns FinaticConnect instance
316
+ */
317
+ public static async init(
318
+ token: string,
319
+ userId?: string | null | undefined,
320
+ options?: { baseUrl?: string } | undefined
321
+ ): Promise<FinaticConnect> {
322
+ // Safari-specific fix: Clear instance if it exists but has no valid session
323
+ // This prevents stale instances from interfering with new requests
324
+ if (FinaticConnect.instance && !FinaticConnect.instance.sessionId) {
325
+ console.log('[FinaticConnect] Clearing stale instance for Safari compatibility');
326
+ FinaticConnect.instance = null;
327
+ }
328
+
329
+ if (!FinaticConnect.instance) {
330
+ const connectOptions: FinaticConnectOptions = {
331
+ token,
332
+ baseUrl: options?.baseUrl || 'https://api.finatic.dev',
333
+ onSuccess: undefined,
334
+ onError: undefined,
335
+ onClose: undefined,
336
+ };
337
+
338
+ // Generate device info
339
+ const deviceInfo: DeviceInfo = {
340
+ ip_address: '', // Will be set by the server
341
+ user_agent: navigator.userAgent,
342
+ fingerprint: btoa(
343
+ [
344
+ navigator.userAgent,
345
+ navigator.language,
346
+ new Date().getTimezoneOffset(),
347
+ screen.width,
348
+ screen.height,
349
+ navigator.hardwareConcurrency,
350
+ // @ts-expect-error - deviceMemory is not in the Navigator type but exists in modern browsers
351
+ navigator.deviceMemory || 'unknown',
352
+ ].join('|')
353
+ ),
354
+ };
355
+
356
+ FinaticConnect.instance = new FinaticConnect(connectOptions, deviceInfo);
357
+
358
+ // Start session and get session data
359
+ const normalizedUserId = userId || undefined; // Convert null to undefined
360
+ const startResponse = await FinaticConnect.instance.apiClient.startSession(
361
+ token,
362
+ normalizedUserId
363
+ );
364
+ FinaticConnect.instance.sessionId = startResponse.data.session_id;
365
+ FinaticConnect.instance.companyId = startResponse.data.company_id || '';
366
+
367
+ // Record session start time for automatic refresh
368
+ FinaticConnect.instance.sessionStartTime = Date.now();
369
+
370
+ // Set session context in API client
371
+ if (
372
+ FinaticConnect.instance.apiClient &&
373
+ typeof FinaticConnect.instance.apiClient.setSessionContext === 'function'
374
+ ) {
375
+ FinaticConnect.instance.apiClient.setSessionContext(
376
+ FinaticConnect.instance.sessionId,
377
+ FinaticConnect.instance.companyId,
378
+ startResponse.data.csrf_token // If available in response
379
+ );
380
+ }
381
+
382
+ // If userId is provided, try to link user to session
383
+ if (normalizedUserId) {
384
+ try {
385
+ // Try to link user to session via API
386
+ const linked = await FinaticConnect.instance.linkUserToSession(normalizedUserId);
387
+ if (linked) {
388
+ // Store user ID for authentication state
389
+ FinaticConnect.instance.storeUserId(normalizedUserId);
390
+ // Emit success event
391
+ FinaticConnect.instance.emit('success', normalizedUserId);
392
+ } else {
393
+ console.warn('Failed to link user to session during initialization');
394
+ }
395
+ } catch (error) {
396
+ FinaticConnect.instance.emit('error', error as Error);
397
+ throw error;
398
+ }
399
+ }
400
+ }
401
+ return FinaticConnect.instance;
402
+ }
403
+
404
+ /**
405
+ * Initialize the SDK with a user ID
406
+ * @param userId - The user ID from a previous session
407
+ */
408
+ public async setUserId(userId: string): Promise<void> {
409
+ await this.initializeWithUser(userId);
410
+ }
411
+
412
+ /**
413
+ * Get the user and tokens for a completed session
414
+ * @returns Promise with user information and tokens
415
+ */
416
+
417
+ private async initializeWithUser(userId: string): Promise<void> {
418
+ try {
419
+ if (!this.sessionId) {
420
+ throw new SessionError('Session not initialized');
421
+ }
422
+
423
+ // Try to link user to session
424
+ const linked = await this.linkUserToSession(userId);
425
+ if (!linked) {
426
+ console.warn('Failed to link user to session during initialization');
427
+ // Don't throw error, just continue without authentication
428
+ return;
429
+ }
430
+
431
+ // Store user ID for authentication state
432
+ this.storeUserId(userId);
433
+
434
+ this.emit('success', userId);
435
+ } catch (error) {
436
+ this.emit('error', error as Error);
437
+ throw error;
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Handle company access error by opening the portal
443
+ * @param error The company access error
444
+ * @param options Optional configuration for the portal
445
+ */
446
+ private async handleCompanyAccessError(
447
+ error: CompanyAccessError,
448
+ options?: {
449
+ onSuccess?: (userId: string) => void;
450
+ onError?: (error: Error) => void;
451
+ onClose?: () => void;
452
+ }
453
+ ): Promise<void> {
454
+ // Emit a specific event for company access errors
455
+ this.emit('companyAccessError', error);
456
+
457
+ // Open the portal to allow the user to connect a broker
458
+ await this.openPortal(options);
459
+ }
460
+
461
+ /**
462
+ * Open the portal for user authentication
463
+ * @param options Optional configuration for the portal
464
+ */
465
+ public async openPortal(options?: PortalOptions): Promise<void> {
466
+ try {
467
+ if (!this.sessionId) {
468
+ throw new SessionError('Session not initialized');
469
+ }
470
+
471
+ // Ensure session is active
472
+ const sessionState = this.apiClient.getCurrentSessionState();
473
+ if (sessionState !== SessionState.ACTIVE) {
474
+ // If not active, try to start a new session
475
+ const startResponse = await this.apiClient.startSession(this.options.token);
476
+ this.sessionId = startResponse.data.session_id;
477
+ this.companyId = startResponse.data.company_id || '';
478
+
479
+ // Set session context in API client
480
+ if (this.apiClient && typeof this.apiClient.setSessionContext === 'function') {
481
+ this.apiClient.setSessionContext(
482
+ this.sessionId,
483
+ this.companyId,
484
+ startResponse.data.csrf_token // If available in response
485
+ );
486
+ }
487
+
488
+ // Session is now active
489
+ this.currentSessionState = SessionState.ACTIVE;
490
+ }
491
+
492
+ // Get portal URL
493
+ const portalResponse = await this.apiClient.getPortalUrl(this.sessionId);
494
+ if (!portalResponse.data.portal_url) {
495
+ throw new Error('Failed to get portal URL');
496
+ }
497
+
498
+ // Apply theme to portal URL if provided
499
+ let themedPortalUrl = appendThemeToURL(portalResponse.data.portal_url, options?.theme);
500
+
501
+ // Apply broker filter to portal URL if provided
502
+ themedPortalUrl = appendBrokerFilterToURL(themedPortalUrl, options?.brokers);
503
+
504
+ // Apply email parameter to portal URL if provided
505
+ if (options?.email) {
506
+ const url = new URL(themedPortalUrl);
507
+ url.searchParams.set('email', options.email);
508
+ themedPortalUrl = url.toString();
509
+ }
510
+
511
+ // Add session ID to portal URL so the portal can use it
512
+ const url = new URL(themedPortalUrl);
513
+ if (this.sessionId) {
514
+ url.searchParams.set('session_id', this.sessionId);
515
+ }
516
+ if (this.companyId) {
517
+ url.searchParams.set('company_id', this.companyId);
518
+ }
519
+ themedPortalUrl = url.toString();
520
+
521
+ // Create portal UI if not exists
522
+ if (!this.portalUI) {
523
+ this.portalUI = new PortalUI(this.baseUrl);
524
+ }
525
+
526
+ // Show portal
527
+ this.portalUI.show(themedPortalUrl, this.sessionId || '', {
528
+ onSuccess: async (userId: string) => {
529
+ try {
530
+ if (!this.sessionId) {
531
+ throw new SessionError('Session not initialized');
532
+ }
533
+
534
+ // Store the userId for authentication state
535
+ this.storeUserId(userId);
536
+
537
+ // Try to link user to session via API
538
+ const linked = await this.linkUserToSession(userId);
539
+ if (!linked) {
540
+ console.warn('Failed to link user to session, but continuing with authentication');
541
+ }
542
+
543
+ // Emit portal success event
544
+ this.emit('portal:success', userId);
545
+
546
+ // Emit legacy success event
547
+ this.emit('success', userId);
548
+ options?.onSuccess?.(userId);
549
+ } catch (error) {
550
+ if (error instanceof CompanyAccessError) {
551
+ // Handle company access error by opening the portal
552
+ await this.handleCompanyAccessError(error, options);
553
+ } else {
554
+ this.emit('error', error as Error);
555
+ options?.onError?.(error as Error);
556
+ }
557
+ }
558
+ },
559
+ onError: (error: Error) => {
560
+ // Emit portal error event
561
+ this.emit('portal:error', error);
562
+
563
+ // Emit legacy error event
564
+ this.emit('error', error);
565
+ options?.onError?.(error);
566
+ },
567
+ onClose: () => {
568
+ // Emit portal close event
569
+ this.emit('portal:close');
570
+
571
+ // Emit legacy close event
572
+ this.emit('close');
573
+ options?.onClose?.();
574
+ },
575
+ onEvent: (type: string, data: any) => {
576
+ console.log('[FinaticConnect] Portal event received:', type, data);
577
+
578
+ // Emit generic event
579
+ this.emit('event', type, data);
580
+
581
+ // Call the event callback
582
+ options?.onEvent?.(type, data);
583
+ },
584
+ });
585
+ } catch (error) {
586
+ if (error instanceof CompanyAccessError) {
587
+ // Handle company access error by opening the portal
588
+ await this.handleCompanyAccessError(error, options);
589
+ } else {
590
+ this.emit('error', error as Error);
591
+ options?.onError?.(error as Error);
592
+ }
593
+ }
594
+ }
595
+
596
+ /**
597
+ * Close the Finatic Connect Portal
598
+ */
599
+ public closePortal(): void {
600
+ this.portalUI.hide();
601
+ this.emit('close');
602
+ }
603
+
604
+ /**
605
+ * Initialize a new session
606
+ * @param oneTimeToken - The one-time token from initSession
607
+ */
608
+ protected async startSession(oneTimeToken: string): Promise<void> {
609
+ try {
610
+ const response = await this.apiClient.startSession(oneTimeToken);
611
+ this.sessionId = response.data.session_id;
612
+ this.currentSessionState = response.data.state;
613
+
614
+ // Set session context in API client
615
+ if (this.apiClient && typeof this.apiClient.setSessionContext === 'function') {
616
+ this.apiClient.setSessionContext(
617
+ this.sessionId,
618
+ this.companyId,
619
+ response.data.csrf_token // If available in response
620
+ );
621
+ }
622
+
623
+ // For non-direct auth, we need to wait for the session to be ACTIVE
624
+ if (response.data.state === SessionState.PENDING) {
625
+ // Session is now active
626
+ this.currentSessionState = SessionState.ACTIVE;
627
+ }
628
+ } catch (error) {
629
+ if (error instanceof SessionError) {
630
+ throw new AuthenticationError('Failed to start session', error.details);
631
+ }
632
+ throw error;
633
+ }
634
+ }
635
+
636
+ /**
637
+ * Place a new order using the broker order API
638
+ * @param order - Order details with broker context
639
+ */
640
+ public async placeOrder(order: BrokerOrderParams, extras?: BrokerExtras): Promise<OrderResponse> {
641
+ if (!(await this.isAuthenticated())) {
642
+ throw new AuthenticationError('User is not authenticated. Please connect a broker first.');
643
+ }
644
+ if (!this.userToken?.user_id) {
645
+ throw new AuthenticationError('No user ID available. Please connect a broker first.');
646
+ }
647
+
648
+ try {
649
+ // Use the order parameter directly since it's already BrokerOrderParams
650
+ return await this.apiClient.placeBrokerOrder(
651
+ order,
652
+ extras || {},
653
+ order.connection_id
654
+ );
655
+ } catch (error) {
656
+ this.emit('error', error as Error);
657
+ throw error;
658
+ }
659
+ }
660
+
661
+ /**
662
+ * Cancel a broker order
663
+ * @param orderId - The order ID to cancel
664
+ * @param broker - Optional broker override
665
+ * @param connection_id - Optional connection ID for testing bypass
666
+ */
667
+ public async cancelOrder(
668
+ orderId: string,
669
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
670
+ connection_id?: string
671
+ ): Promise<OrderResponse> {
672
+ if (!(await this.isAuthenticated())) {
673
+ throw new AuthenticationError('User is not authenticated. Please connect a broker first.');
674
+ }
675
+ if (!this.userToken?.user_id) {
676
+ throw new AuthenticationError('No user ID available. Please connect a broker first.');
677
+ }
678
+
679
+ try {
680
+ return await this.apiClient.cancelBrokerOrder(orderId, broker, {}, connection_id);
681
+ } catch (error) {
682
+ this.emit('error', error as Error);
683
+ throw error;
684
+ }
685
+ }
686
+
687
+ /**
688
+ * Modify a broker order
689
+ * @param orderId - The order ID to modify
690
+ * @param modifications - The modifications to apply
691
+ * @param broker - Optional broker override
692
+ * @param connection_id - Optional connection ID for testing bypass
693
+ */
694
+ public async modifyOrder(
695
+ orderId: string,
696
+ modifications: Partial<{
697
+ symbol?: string;
698
+ quantity?: number;
699
+ price?: number;
700
+ stopPrice?: number;
701
+ timeInForce?: 'day' | 'gtc' | 'gtd' | 'ioc' | 'fok';
702
+ orderType?: 'Market' | 'Limit' | 'Stop' | 'StopLimit';
703
+ side?: 'Buy' | 'Sell';
704
+ order_id?: string;
705
+ }>,
706
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
707
+ connection_id?: string
708
+ ): Promise<OrderResponse> {
709
+ if (!(await this.isAuthenticated())) {
710
+ throw new AuthenticationError('User is not authenticated. Please connect a broker first.');
711
+ }
712
+ if (!this.userToken?.user_id) {
713
+ throw new AuthenticationError('No user ID available. Please connect a broker first.');
714
+ }
715
+
716
+ try {
717
+ // Convert modifications to broker format
718
+ const brokerModifications: Partial<BrokerOrderParams> = {};
719
+ if (modifications.symbol) brokerModifications.symbol = modifications.symbol;
720
+ if (modifications.quantity) brokerModifications.orderQty = modifications.quantity;
721
+ if (modifications.price) brokerModifications.price = modifications.price;
722
+ if (modifications.stopPrice) brokerModifications.stopPrice = modifications.stopPrice;
723
+ if (modifications.timeInForce)
724
+ brokerModifications.timeInForce = modifications.timeInForce as
725
+ | 'day'
726
+ | 'gtc'
727
+ | 'gtd'
728
+ | 'ioc'
729
+ | 'fok';
730
+ if (modifications.orderType) brokerModifications.orderType = modifications.orderType;
731
+ if (modifications.side) brokerModifications.action = modifications.side;
732
+ if (modifications.order_id) brokerModifications.order_id = modifications.order_id;
733
+
734
+ return await this.apiClient.modifyBrokerOrder(
735
+ orderId,
736
+ brokerModifications,
737
+ broker,
738
+ {},
739
+ connection_id
740
+ );
741
+ } catch (error) {
742
+ this.emit('error', error as Error);
743
+ throw error;
744
+ }
745
+ }
746
+
747
+
748
+ /**
749
+ * Place a stock market order (convenience method)
750
+ */
751
+ public async placeStockMarketOrder(
752
+ symbol: string,
753
+ quantity: number,
754
+ side: 'buy' | 'sell',
755
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
756
+ accountNumber?: string
757
+ ): Promise<OrderResponse> {
758
+ if (!(await this.isAuthenticated())) {
759
+ throw new AuthenticationError('User is not authenticated. Please connect a broker first.');
760
+ }
761
+ if (!this.userToken?.user_id) {
762
+ throw new AuthenticationError('No user ID available. Please connect a broker first.');
763
+ }
764
+
765
+ try {
766
+ return await this.apiClient.placeStockMarketOrder(
767
+ symbol,
768
+ quantity,
769
+ side === 'buy' ? 'Buy' : 'Sell',
770
+ broker,
771
+ accountNumber
772
+ );
773
+ } catch (error) {
774
+ this.emit('error', error as Error);
775
+ throw error;
776
+ }
777
+ }
778
+
779
+ /**
780
+ * Place a stock limit order (convenience method)
781
+ */
782
+ public async placeStockLimitOrder(
783
+ symbol: string,
784
+ quantity: number,
785
+ side: 'buy' | 'sell',
786
+ price: number,
787
+ timeInForce: 'day' | 'gtc' = 'gtc',
788
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
789
+ accountNumber?: string
790
+ ): Promise<OrderResponse> {
791
+ if (!(await this.isAuthenticated())) {
792
+ throw new AuthenticationError('User is not authenticated. Please connect a broker first.');
793
+ }
794
+ if (!this.userToken?.user_id) {
795
+ throw new AuthenticationError('No user ID available. Please connect a broker first.');
796
+ }
797
+
798
+ try {
799
+ return await this.apiClient.placeStockLimitOrder(
800
+ symbol,
801
+ quantity,
802
+ side === 'buy' ? 'Buy' : 'Sell',
803
+ price,
804
+ timeInForce,
805
+ broker,
806
+ accountNumber
807
+ );
808
+ } catch (error) {
809
+ this.emit('error', error as Error);
810
+ throw error;
811
+ }
812
+ }
813
+
814
+ /**
815
+ * Place a stock stop order (convenience method)
816
+ */
817
+ public async placeStockStopOrder(
818
+ symbol: string,
819
+ quantity: number,
820
+ side: 'buy' | 'sell',
821
+ stopPrice: number,
822
+ timeInForce: 'day' | 'gtc' = 'gtc',
823
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
824
+ accountNumber?: string
825
+ ): Promise<OrderResponse> {
826
+ if (!(await this.isAuthenticated())) {
827
+ throw new AuthenticationError('User is not authenticated. Please connect a broker first.');
828
+ }
829
+ if (!this.userToken?.user_id) {
830
+ throw new AuthenticationError('No user ID available. Please connect a broker first.');
831
+ }
832
+
833
+ try {
834
+ return await this.apiClient.placeStockStopOrder(
835
+ symbol,
836
+ quantity,
837
+ side === 'buy' ? 'Buy' : 'Sell',
838
+ stopPrice,
839
+ timeInForce,
840
+ broker,
841
+ accountNumber
842
+ );
843
+ } catch (error) {
844
+ this.emit('error', error as Error);
845
+ throw error;
846
+ }
847
+ }
848
+
849
+ /**
850
+ * Place a crypto market order (convenience method)
851
+ */
852
+ public async placeCryptoMarketOrder(
853
+ symbol: string,
854
+ quantity: number,
855
+ side: 'buy' | 'sell',
856
+ broker?: 'coinbase' | 'binance' | 'kraken',
857
+ accountNumber?: string
858
+ ): Promise<OrderResponse> {
859
+ if (!(await this.isAuthenticated())) {
860
+ throw new AuthenticationError('User is not authenticated. Please connect a broker first.');
861
+ }
862
+ if (!this.userToken?.user_id) {
863
+ throw new AuthenticationError('No user ID available. Please connect a broker first.');
864
+ }
865
+
866
+ try {
867
+ return await this.apiClient.placeCryptoMarketOrder(
868
+ symbol,
869
+ quantity,
870
+ side === 'buy' ? 'Buy' : 'Sell',
871
+ broker,
872
+ accountNumber
873
+ );
874
+ } catch (error) {
875
+ this.emit('error', error as Error);
876
+ throw error;
877
+ }
878
+ }
879
+
880
+ /**
881
+ * Place a crypto limit order (convenience method)
882
+ */
883
+ public async placeCryptoLimitOrder(
884
+ symbol: string,
885
+ quantity: number,
886
+ side: 'buy' | 'sell',
887
+ price: number,
888
+ timeInForce: 'day' | 'gtc' = 'gtc',
889
+ broker?: 'coinbase' | 'binance' | 'kraken',
890
+ accountNumber?: string
891
+ ): Promise<OrderResponse> {
892
+ if (!(await this.isAuthenticated())) {
893
+ throw new AuthenticationError('User is not authenticated. Please connect a broker first.');
894
+ }
895
+ if (!this.userToken?.user_id) {
896
+ throw new AuthenticationError('No user ID available. Please connect a broker first.');
897
+ }
898
+
899
+ try {
900
+ return await this.apiClient.placeCryptoLimitOrder(
901
+ symbol,
902
+ quantity,
903
+ side === 'buy' ? 'Buy' : 'Sell',
904
+ price,
905
+ timeInForce,
906
+ broker,
907
+ accountNumber
908
+ );
909
+ } catch (error) {
910
+ this.emit('error', error as Error);
911
+ throw error;
912
+ }
913
+ }
914
+
915
+ /**
916
+ * Place an options market order (convenience method)
917
+ */
918
+ public async placeOptionsMarketOrder(
919
+ symbol: string,
920
+ quantity: number,
921
+ side: 'buy' | 'sell',
922
+ broker?: 'tasty_trade' | 'robinhood' | 'ninja_trader',
923
+ accountNumber?: string
924
+ ): Promise<OrderResponse> {
925
+ if (!(await this.isAuthenticated())) {
926
+ throw new AuthenticationError('User is not authenticated. Please connect a broker first.');
927
+ }
928
+ if (!this.userToken?.user_id) {
929
+ throw new AuthenticationError('No user ID available. Please connect a broker first.');
930
+ }
931
+
932
+ try {
933
+ return await this.apiClient.placeOptionsMarketOrder(
934
+ symbol,
935
+ quantity,
936
+ side === 'buy' ? 'Buy' : 'Sell',
937
+ broker,
938
+ accountNumber
939
+ );
940
+ } catch (error) {
941
+ this.emit('error', error as Error);
942
+ throw error;
943
+ }
944
+ }
945
+
946
+ /**
947
+ * Place an options limit order (convenience method)
948
+ */
949
+ public async placeOptionsLimitOrder(
950
+ symbol: string,
951
+ quantity: number,
952
+ side: 'buy' | 'sell',
953
+ price: number,
954
+ timeInForce: 'day' | 'gtc' = 'gtc',
955
+ broker?: 'tasty_trade' | 'robinhood' | 'ninja_trader',
956
+ accountNumber?: string
957
+ ): Promise<OrderResponse> {
958
+ if (!(await this.isAuthenticated())) {
959
+ throw new AuthenticationError('User is not authenticated. Please connect a broker first.');
960
+ }
961
+ if (!this.userToken?.user_id) {
962
+ throw new AuthenticationError('No user ID available. Please connect a broker first.');
963
+ }
964
+
965
+ try {
966
+ return await this.apiClient.placeOptionsLimitOrder(
967
+ symbol,
968
+ quantity,
969
+ side === 'buy' ? 'Buy' : 'Sell',
970
+ price,
971
+ timeInForce,
972
+ broker,
973
+ accountNumber
974
+ );
975
+ } catch (error) {
976
+ this.emit('error', error as Error);
977
+ throw error;
978
+ }
979
+ }
980
+
981
+ /**
982
+ * Place a futures market order (convenience method)
983
+ */
984
+ public async placeFuturesMarketOrder(
985
+ symbol: string,
986
+ quantity: number,
987
+ side: 'buy' | 'sell',
988
+ broker?: 'ninja_trader' | 'tasty_trade',
989
+ accountNumber?: string
990
+ ): Promise<OrderResponse> {
991
+ if (!(await this.isAuthenticated())) {
992
+ throw new AuthenticationError('User is not authenticated. Please connect a broker first.');
993
+ }
994
+ if (!this.userToken?.user_id) {
995
+ throw new AuthenticationError('No user ID available. Please connect a broker first.');
996
+ }
997
+
998
+ try {
999
+ return await this.apiClient.placeFuturesMarketOrder(
1000
+ symbol,
1001
+ quantity,
1002
+ side === 'buy' ? 'Buy' : 'Sell',
1003
+ broker,
1004
+ accountNumber
1005
+ );
1006
+ } catch (error) {
1007
+ this.emit('error', error as Error);
1008
+ throw error;
1009
+ }
1010
+ }
1011
+
1012
+ /**
1013
+ * Place a futures limit order (convenience method)
1014
+ */
1015
+ public async placeFuturesLimitOrder(
1016
+ symbol: string,
1017
+ quantity: number,
1018
+ side: 'buy' | 'sell',
1019
+ price: number,
1020
+ timeInForce: 'day' | 'gtc' = 'gtc',
1021
+ broker?: 'ninja_trader' | 'tasty_trade',
1022
+ accountNumber?: string
1023
+ ): Promise<OrderResponse> {
1024
+ if (!(await this.isAuthenticated())) {
1025
+ throw new AuthenticationError('User is not authenticated. Please connect a broker first.');
1026
+ }
1027
+ if (!this.userToken?.user_id) {
1028
+ throw new AuthenticationError('No user ID available. Please connect a broker first.');
1029
+ }
1030
+
1031
+ try {
1032
+ return await this.apiClient.placeFuturesLimitOrder(
1033
+ symbol,
1034
+ quantity,
1035
+ side === 'buy' ? 'Buy' : 'Sell',
1036
+ price,
1037
+ timeInForce,
1038
+ broker,
1039
+ accountNumber
1040
+ );
1041
+ } catch (error) {
1042
+ this.emit('error', error as Error);
1043
+ throw error;
1044
+ }
1045
+ }
1046
+
1047
+ /**
1048
+ * Get the current user ID
1049
+ * @returns The current user ID or undefined if not authenticated
1050
+ * @throws AuthenticationError if user is not authenticated
1051
+ */
1052
+ public async getUserId(): Promise<string | null> {
1053
+ if (!(await this.isAuthenticated())) {
1054
+ return null;
1055
+ }
1056
+ if (!this.userToken?.user_id) {
1057
+ return null;
1058
+ }
1059
+ return this.userToken.user_id;
1060
+ }
1061
+
1062
+ /**
1063
+ * Get list of supported brokers
1064
+ * @returns Promise with array of broker information
1065
+ */
1066
+ public async getBrokerList(): Promise<BrokerInfo[]> {
1067
+ // if (!this.isAuthenticated()) {
1068
+ // throw new AuthenticationError('Not authenticated');
1069
+ // }
1070
+
1071
+ const response = await this.apiClient.getBrokerList();
1072
+ const baseUrl = this.baseUrl.replace('/api/v1', ''); // Remove /api/v1 to get the base URL
1073
+
1074
+ // Transform the broker list to include full logo URLs
1075
+ return response.response_data.map((broker: BrokerInfo) => ({
1076
+ ...broker,
1077
+ logo_path: broker.logo_path ? `${baseUrl}${broker.logo_path}` : '',
1078
+ }));
1079
+ }
1080
+
1081
+ /**
1082
+ * Get broker connections
1083
+ * @returns Promise with array of broker connections
1084
+ * @throws AuthenticationError if user is not authenticated
1085
+ */
1086
+ public async getBrokerConnections(): Promise<BrokerConnection[]> {
1087
+ if (!(await this.isAuthenticated())) {
1088
+ throw new AuthenticationError('User is not authenticated. Please connect a broker first.');
1089
+ }
1090
+ if (!this.userToken?.user_id) {
1091
+ throw new AuthenticationError('No user ID available. Please connect a broker first.');
1092
+ }
1093
+
1094
+ const response = await this.apiClient.getBrokerConnections();
1095
+ if (response.status_code !== 200) {
1096
+ throw new Error(response.message || 'Failed to retrieve broker connections');
1097
+ }
1098
+ return response.response_data;
1099
+ }
1100
+
1101
+ // Abstract convenience methods
1102
+ /**
1103
+ * Get only open positions
1104
+ * @returns Promise with array of open positions
1105
+ */
1106
+ public async getOpenPositions(): Promise<BrokerDataPosition[]> {
1107
+ return this.getAllPositions({ position_status: 'open' });
1108
+ }
1109
+
1110
+ /**
1111
+ * Get only filled orders
1112
+ * @returns Promise with array of filled orders
1113
+ */
1114
+ public async getFilledOrders(): Promise<BrokerDataOrder[]> {
1115
+ return this.getAllOrders({ status: 'filled' });
1116
+ }
1117
+
1118
+ /**
1119
+ * Get only pending orders
1120
+ * @returns Promise with array of pending orders
1121
+ */
1122
+ public async getPendingOrders(): Promise<BrokerDataOrder[]> {
1123
+ return this.getAllOrders({ status: 'pending' });
1124
+ }
1125
+
1126
+ /**
1127
+ * Get only active accounts
1128
+ * @returns Promise with array of active accounts
1129
+ */
1130
+ public async getActiveAccounts(): Promise<BrokerDataAccount[]> {
1131
+ return this.getAllAccounts({ status: 'active' });
1132
+ }
1133
+
1134
+ /**
1135
+ * Get orders for a specific symbol
1136
+ * @param symbol - The symbol to filter by
1137
+ * @returns Promise with array of orders for the symbol
1138
+ */
1139
+ public async getOrdersBySymbol(symbol: string): Promise<BrokerDataOrder[]> {
1140
+ return this.getAllOrders({ symbol });
1141
+ }
1142
+
1143
+ /**
1144
+ * Get positions for a specific symbol
1145
+ * @param symbol - The symbol to filter by
1146
+ * @returns Promise with array of positions for the symbol
1147
+ */
1148
+ public async getPositionsBySymbol(symbol: string): Promise<BrokerDataPosition[]> {
1149
+ return this.getAllPositions({ symbol });
1150
+ }
1151
+
1152
+ /**
1153
+ * Get orders for a specific broker
1154
+ * @param brokerId - The broker ID to filter by
1155
+ * @returns Promise with array of orders for the broker
1156
+ */
1157
+ public async getOrdersByBroker(brokerId: string): Promise<BrokerDataOrder[]> {
1158
+ return this.getAllOrders({ broker_id: brokerId });
1159
+ }
1160
+
1161
+ /**
1162
+ * Get positions for a specific broker
1163
+ * @param brokerId - The broker ID to filter by
1164
+ * @returns Promise with array of positions for the broker
1165
+ */
1166
+ public async getPositionsByBroker(brokerId: string): Promise<BrokerDataPosition[]> {
1167
+ return this.getAllPositions({ broker_id: brokerId });
1168
+ }
1169
+
1170
+ // Pagination methods
1171
+
1172
+
1173
+
1174
+
1175
+
1176
+
1177
+
1178
+ /**
1179
+ * Get all orders across all pages (convenience method)
1180
+ * @param filter - Optional filter parameters
1181
+ * @returns Promise with all orders
1182
+ */
1183
+ public async getAllOrders(filter?: OrdersFilter): Promise<BrokerDataOrder[]> {
1184
+ if (!(await this.isAuthenticated())) {
1185
+ throw new AuthenticationError('User is not authenticated');
1186
+ }
1187
+
1188
+ const allData: BrokerDataOrder[] = [];
1189
+ let currentResult = await this.apiClient.getBrokerOrdersPage(1, 100, filter);
1190
+
1191
+ while (currentResult) {
1192
+ allData.push(...currentResult.data);
1193
+ if (!currentResult.hasNext) break;
1194
+ const nextResult = await currentResult.nextPage();
1195
+ if (!nextResult) break;
1196
+ currentResult = nextResult;
1197
+ }
1198
+
1199
+ return allData;
1200
+ }
1201
+
1202
+ /**
1203
+ * Get all positions across all pages (convenience method)
1204
+ * @param filter - Optional filter parameters
1205
+ * @returns Promise with all positions
1206
+ */
1207
+ public async getAllPositions(filter?: PositionsFilter): Promise<BrokerDataPosition[]> {
1208
+ if (!(await this.isAuthenticated())) {
1209
+ throw new AuthenticationError('User is not authenticated');
1210
+ }
1211
+
1212
+ const allData: BrokerDataPosition[] = [];
1213
+ let currentResult = await this.apiClient.getBrokerPositionsPage(1, 100, filter);
1214
+
1215
+ while (currentResult) {
1216
+ allData.push(...currentResult.data);
1217
+ if (!currentResult.hasNext) break;
1218
+ const nextResult = await currentResult.nextPage();
1219
+ if (!nextResult) break;
1220
+ currentResult = nextResult;
1221
+ }
1222
+
1223
+ return allData;
1224
+ }
1225
+
1226
+ /**
1227
+ * Get all accounts across all pages (convenience method)
1228
+ * @param filter - Optional filter parameters
1229
+ * @returns Promise with all accounts
1230
+ */
1231
+ public async getAllAccounts(filter?: AccountsFilter): Promise<BrokerDataAccount[]> {
1232
+ if (!(await this.isAuthenticated())) {
1233
+ throw new AuthenticationError('User is not authenticated');
1234
+ }
1235
+
1236
+ const allData: BrokerDataAccount[] = [];
1237
+ let currentResult = await this.apiClient.getBrokerAccountsPage(1, 100, filter);
1238
+
1239
+ while (currentResult) {
1240
+ allData.push(...currentResult.data);
1241
+ if (!currentResult.hasNext) break;
1242
+ const nextResult = await currentResult.nextPage();
1243
+ if (!nextResult) break;
1244
+ currentResult = nextResult;
1245
+ }
1246
+
1247
+ return allData;
1248
+ }
1249
+
1250
+ public async getAllBalances(filter?: BalancesFilter): Promise<BrokerBalance[]> {
1251
+ if (!(await this.isAuthenticated())) {
1252
+ throw new AuthenticationError('User is not authenticated');
1253
+ }
1254
+
1255
+ const allData: BrokerBalance[] = [];
1256
+ let currentResult = await this.apiClient.getBrokerBalancesPage(1, 100, filter);
1257
+
1258
+ while (currentResult) {
1259
+ allData.push(...currentResult.data);
1260
+ if (!currentResult.hasNext) break;
1261
+ const nextResult = await currentResult.nextPage();
1262
+ if (!nextResult) break;
1263
+ currentResult = nextResult;
1264
+ }
1265
+
1266
+ return allData;
1267
+ }
1268
+
1269
+ /**
1270
+ * Register session management (but don't auto-cleanup for 24-hour sessions)
1271
+ */
1272
+ private registerSessionCleanup(): void {
1273
+ // Only cleanup on actual page unload (not visibility changes)
1274
+ // This prevents sessions from being closed when users switch tabs or apps
1275
+ window.addEventListener('beforeunload', this.handleSessionCleanup.bind(this));
1276
+
1277
+ // Handle visibility changes for keep-alive management (but don't complete sessions)
1278
+ document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
1279
+
1280
+ // Start session keep-alive mechanism
1281
+ this.startSessionKeepAlive();
1282
+ }
1283
+
1284
+ /**
1285
+ * Start the session keep-alive mechanism
1286
+ */
1287
+ private startSessionKeepAlive(): void {
1288
+ if (this.sessionKeepAliveInterval) {
1289
+ clearInterval(this.sessionKeepAliveInterval);
1290
+ }
1291
+
1292
+ this.sessionKeepAliveInterval = setInterval(() => {
1293
+ this.validateSessionKeepAlive();
1294
+ }, this.SESSION_KEEP_ALIVE_INTERVAL);
1295
+
1296
+ console.log('[FinaticConnect] Session keep-alive started (5-minute intervals)');
1297
+ }
1298
+
1299
+ /**
1300
+ * Stop the session keep-alive mechanism
1301
+ */
1302
+ private stopSessionKeepAlive(): void {
1303
+ if (this.sessionKeepAliveInterval) {
1304
+ clearInterval(this.sessionKeepAliveInterval);
1305
+ this.sessionKeepAliveInterval = null;
1306
+ console.log('[FinaticConnect] Session keep-alive stopped');
1307
+ }
1308
+ }
1309
+
1310
+ /**
1311
+ * Validate session for keep-alive purposes and handle automatic refresh
1312
+ */
1313
+ private async validateSessionKeepAlive(): Promise<void> {
1314
+ if (!this.sessionId || !(await this.isAuthenticated())) {
1315
+ console.log('[FinaticConnect] Session keep-alive skipped - no active session');
1316
+ return;
1317
+ }
1318
+
1319
+ try {
1320
+ console.log('[FinaticConnect] Validating session for keep-alive...');
1321
+
1322
+ // Check if we need to refresh the session (at 16 hours)
1323
+ if (this.shouldRefreshSession()) {
1324
+ await this.refreshSessionAutomatically();
1325
+ return;
1326
+ }
1327
+
1328
+ // Session keep-alive - assume session is active if we have a session ID
1329
+ console.log('[FinaticConnect] Session keep-alive successful');
1330
+ this.currentSessionState = 'active';
1331
+ } catch (error) {
1332
+ console.warn('[FinaticConnect] Session keep-alive error:', error);
1333
+ // Don't throw errors during keep-alive - just log them
1334
+ }
1335
+ }
1336
+
1337
+ /**
1338
+ * Check if the session should be refreshed (after 16 hours)
1339
+ */
1340
+ private shouldRefreshSession(): boolean {
1341
+ if (!this.sessionStartTime) {
1342
+ return false;
1343
+ }
1344
+
1345
+ const sessionAgeHours = (Date.now() - this.sessionStartTime) / (1000 * 60 * 60);
1346
+ const hoursUntilRefresh = this.SESSION_REFRESH_BUFFER_HOURS - sessionAgeHours;
1347
+
1348
+ if (hoursUntilRefresh <= 0) {
1349
+ console.log(
1350
+ `[FinaticConnect] Session is ${sessionAgeHours.toFixed(1)} hours old - triggering refresh`
1351
+ );
1352
+ return true;
1353
+ }
1354
+
1355
+ // Log when refresh will occur (every 5 minutes during keep-alive)
1356
+ if (hoursUntilRefresh <= 1) {
1357
+ console.log(`[FinaticConnect] Session will refresh in ${hoursUntilRefresh.toFixed(1)} hours`);
1358
+ }
1359
+
1360
+ return false;
1361
+ }
1362
+
1363
+ /**
1364
+ * Automatically refresh the session to extend its lifetime
1365
+ */
1366
+ private async refreshSessionAutomatically(): Promise<void> {
1367
+ if (!this.sessionId) {
1368
+ console.warn('[FinaticConnect] Cannot refresh session - no session ID');
1369
+ return;
1370
+ }
1371
+
1372
+ try {
1373
+ console.log('[FinaticConnect] Automatically refreshing session (16+ hours old)...');
1374
+ const response = await this.apiClient.refreshSession();
1375
+
1376
+ if (response.success) {
1377
+ console.log('[FinaticConnect] Session automatically refreshed successfully');
1378
+ console.log('[FinaticConnect] New session expires at:', response.response_data.expires_at);
1379
+ this.currentSessionState = response.response_data.status;
1380
+
1381
+ // Update session start time to prevent immediate re-refresh
1382
+ this.sessionStartTime = Date.now();
1383
+ } else {
1384
+ console.warn('[FinaticConnect] Automatic session refresh failed');
1385
+ }
1386
+ } catch (error) {
1387
+ console.warn('[FinaticConnect] Automatic session refresh error:', error);
1388
+ // Don't throw errors during automatic refresh - just log them
1389
+ }
1390
+ }
1391
+
1392
+ /**
1393
+ * Handle session cleanup when page is unloading
1394
+ */
1395
+ private async handleSessionCleanup(): Promise<void> {
1396
+ this.stopSessionKeepAlive();
1397
+ if (this.sessionId) {
1398
+ await this.completeSession(this.sessionId);
1399
+ }
1400
+ }
1401
+
1402
+ /**
1403
+ * Handle visibility change (mobile browsers)
1404
+ * Note: We don't complete sessions on visibility change for 24-hour sessions
1405
+ */
1406
+ private async handleVisibilityChange(): Promise<void> {
1407
+ // For 24-hour sessions, we don't want to complete sessions on visibility changes
1408
+ // This prevents sessions from being closed when users switch tabs or apps
1409
+ console.log('[FinaticConnect] Page visibility changed to:', document.visibilityState);
1410
+
1411
+ // Only pause keep-alive when hidden, but don't complete the session
1412
+ if (document.visibilityState === 'hidden') {
1413
+ this.stopSessionKeepAlive();
1414
+ } else if (document.visibilityState === 'visible') {
1415
+ // Restart keep-alive when page becomes visible again
1416
+ this.startSessionKeepAlive();
1417
+ }
1418
+ }
1419
+
1420
+ /**
1421
+ * Complete the session by calling the API
1422
+ * @param sessionId - The session ID to complete
1423
+ */
1424
+ private async completeSession(sessionId: string): Promise<void> {
1425
+ try {
1426
+ // Check if we're in mock mode (check if apiClient is a mock client)
1427
+ const isMockMode =
1428
+ this.apiClient &&
1429
+ typeof this.apiClient.isMockClient === 'function' &&
1430
+ this.apiClient.isMockClient();
1431
+
1432
+ if (isMockMode) {
1433
+ // Mock the completion response
1434
+ console.log('[FinaticConnect] Mock session completion for session:', sessionId);
1435
+ return;
1436
+ }
1437
+
1438
+ // Real API call
1439
+ const response = await fetch(`${this.baseUrl}/portal/${sessionId}/complete`, {
1440
+ method: 'POST',
1441
+ headers: {
1442
+ 'Content-Type': 'application/json',
1443
+ },
1444
+ });
1445
+
1446
+ if (response.ok) {
1447
+ console.log('[FinaticConnect] Session completed successfully');
1448
+ } else {
1449
+ console.warn('[FinaticConnect] Failed to complete session:', response.status);
1450
+ }
1451
+ } catch (error) {
1452
+ // Silent failure - don't throw errors during cleanup
1453
+ console.warn('[FinaticConnect] Session cleanup failed:', error);
1454
+ }
1455
+ }
1456
+
1457
+ /**
1458
+ * Disconnect a company from a broker connection
1459
+ * @param connectionId - The connection ID to disconnect
1460
+ * @returns Promise with disconnect response
1461
+ * @throws AuthenticationError if user is not authenticated
1462
+ */
1463
+ public async disconnectCompany(connectionId: string): Promise<DisconnectCompanyResponse> {
1464
+ if (!(await this.isAuthenticated())) {
1465
+ throw new AuthenticationError('User is not authenticated. Please connect a broker first.');
1466
+ }
1467
+ if (!this.userToken?.user_id) {
1468
+ throw new AuthenticationError('No user ID available. Please connect a broker first.');
1469
+ }
1470
+
1471
+ return this.apiClient.disconnectCompany(connectionId);
1472
+ }
1473
+
1474
+ // Duplicate getBalances method removed - using the paginated version above
1475
+
1476
+ }