@finatic/client 0.0.139 → 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 (39) hide show
  1. package/README.md +278 -461
  2. package/dist/index.d.ts +55 -515
  3. package/dist/index.js +326 -456
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +327 -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/package.json +7 -3
  15. package/src/core/client/ApiClient.ts +1721 -0
  16. package/src/core/client/FinaticConnect.ts +1476 -0
  17. package/src/core/portal/PortalUI.ts +300 -0
  18. package/src/index.d.ts +23 -0
  19. package/src/index.ts +87 -0
  20. package/src/mocks/MockApiClient.ts +1032 -0
  21. package/src/mocks/MockDataProvider.ts +986 -0
  22. package/src/mocks/MockFactory.ts +97 -0
  23. package/src/mocks/utils.ts +133 -0
  24. package/src/themes/portalPresets.ts +1307 -0
  25. package/src/types/api/auth.ts +112 -0
  26. package/src/types/api/broker.ts +330 -0
  27. package/src/types/api/core.ts +53 -0
  28. package/src/types/api/errors.ts +35 -0
  29. package/src/types/api/orders.ts +45 -0
  30. package/src/types/api/portfolio.ts +59 -0
  31. package/src/types/common/pagination.ts +138 -0
  32. package/src/types/connect.ts +56 -0
  33. package/src/types/index.ts +25 -0
  34. package/src/types/portal.ts +214 -0
  35. package/src/types/ui/theme.ts +105 -0
  36. package/src/utils/brokerUtils.ts +85 -0
  37. package/src/utils/errors.ts +104 -0
  38. package/src/utils/events.ts +54 -0
  39. package/src/utils/themeUtils.ts +146 -0
@@ -0,0 +1,300 @@
1
+ export class PortalUI {
2
+ private iframe: HTMLIFrameElement | null = null;
3
+ private container: HTMLDivElement | null = null;
4
+ private messageHandler: ((event: MessageEvent) => void) | null = null;
5
+ private sessionId: string | null = null;
6
+ private portalOrigin: string | null = null;
7
+ private options?: {
8
+ onSuccess?: (userId: string, tokens?: { access_token?: string; refresh_token?: string }) => void;
9
+ onError?: (error: Error) => void;
10
+ onClose?: () => void;
11
+ onEvent?: (type: string, data: any) => void;
12
+ };
13
+ private originalBodyStyle: string | null = null;
14
+
15
+ constructor(portalUrl: string) {
16
+ this.createContainer();
17
+ }
18
+
19
+ private createContainer(): void {
20
+ console.debug('[PortalUI] Creating container and iframe');
21
+ this.container = document.createElement('div');
22
+ this.container.style.cssText = `
23
+ position: fixed;
24
+ top: 0;
25
+ left: 0;
26
+ width: 100%;
27
+ height: 100%;
28
+ background: rgba(0, 0, 0, 0.5);
29
+ display: none;
30
+ z-index: 9999;
31
+ `;
32
+
33
+ this.iframe = document.createElement('iframe');
34
+ // Let the portal handle its own styling - only set essential attributes
35
+ this.iframe.style.cssText = `
36
+ position: absolute;
37
+ top: 50%;
38
+ left: 50%;
39
+ transform: translate(-50%, -50%);
40
+ width: 90%;
41
+ max-width: 500px;
42
+ height: 90%;
43
+ max-height: 600px;
44
+ border: none;
45
+ border-radius: 24px;
46
+ `;
47
+
48
+ // Set security headers
49
+ this.iframe.setAttribute('sandbox', 'allow-scripts allow-forms allow-popups allow-same-origin');
50
+ this.iframe.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin');
51
+ this.iframe.setAttribute(
52
+ 'allow',
53
+ 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'
54
+ );
55
+
56
+ // Add CSP meta tag to allow Google Fonts and other required resources
57
+ const meta = document.createElement('meta');
58
+ meta.setAttribute('http-equiv', 'Content-Security-Policy');
59
+ meta.setAttribute(
60
+ 'content',
61
+ `
62
+ default-src 'self' https:;
63
+ style-src 'self' 'unsafe-inline' https: https://fonts.googleapis.com https://fonts.gstatic.com https://cdn.jsdelivr.net;
64
+ font-src 'self' https://fonts.gstatic.com;
65
+ img-src 'self' data: https:;
66
+ script-src 'self' 'unsafe-inline' 'unsafe-eval' https:;
67
+ connect-src 'self' https:;
68
+ frame-src 'self' https:;
69
+ `
70
+ .replace(/\s+/g, ' ')
71
+ .trim()
72
+ );
73
+ this.iframe.contentDocument?.head.appendChild(meta);
74
+
75
+ this.container.appendChild(this.iframe);
76
+ document.body.appendChild(this.container);
77
+ console.debug('[PortalUI] Container and iframe created successfully');
78
+ }
79
+
80
+ /**
81
+ * Lock background scrolling by setting overflow: hidden on body
82
+ */
83
+ private lockScroll(): void {
84
+ if (typeof document !== 'undefined' && document.body) {
85
+ // Store original body style to restore later
86
+ this.originalBodyStyle = document.body.style.cssText;
87
+
88
+ // Add overflow: hidden to prevent scrolling
89
+ document.body.style.overflow = 'hidden';
90
+ document.body.style.position = 'fixed';
91
+ document.body.style.width = '100%';
92
+ document.body.style.top = `-${window.scrollY}px`;
93
+
94
+ console.debug('[PortalUI] Background scroll locked');
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Unlock background scrolling by restoring original body style
100
+ */
101
+ private unlockScroll(): void {
102
+ if (typeof document !== 'undefined' && document.body && this.originalBodyStyle !== null) {
103
+ // Restore original body style
104
+ document.body.style.cssText = this.originalBodyStyle;
105
+
106
+ // Restore scroll position
107
+ const scrollY = document.body.style.top;
108
+ document.body.style.position = '';
109
+ document.body.style.top = '';
110
+ window.scrollTo(0, parseInt(scrollY || '0') * -1);
111
+
112
+ this.originalBodyStyle = null;
113
+ console.debug('[PortalUI] Background scroll unlocked');
114
+ }
115
+ }
116
+
117
+ public show(
118
+ url: string,
119
+ sessionId: string,
120
+ options: {
121
+ onSuccess?: (userId: string) => void;
122
+ onError?: (error: Error) => void;
123
+ onClose?: () => void;
124
+ onEvent?: (type: string, data: any) => void;
125
+ } = {}
126
+ ): void {
127
+ if (!this.iframe || !this.container) {
128
+ this.createContainer();
129
+ }
130
+
131
+ // Set portalOrigin to the actual portal URL's origin
132
+ try {
133
+ this.portalOrigin = new URL(url).origin;
134
+ } catch (e) {
135
+ this.portalOrigin = null;
136
+ }
137
+
138
+ this.sessionId = sessionId;
139
+ this.options = options;
140
+ this.container!.style.display = 'block';
141
+ this.iframe!.src = url;
142
+
143
+ // Lock background scrolling
144
+ this.lockScroll();
145
+
146
+ // Set up message handler
147
+ this.messageHandler = this.handleMessage.bind(this);
148
+ window.addEventListener('message', this.messageHandler);
149
+ }
150
+
151
+ public hide(): void {
152
+ if (this.container) {
153
+ this.container.style.display = 'none';
154
+ }
155
+ if (this.iframe) {
156
+ this.iframe.src = '';
157
+ }
158
+ if (this.messageHandler) {
159
+ window.removeEventListener('message', this.messageHandler);
160
+ this.messageHandler = null;
161
+ }
162
+ this.sessionId = null;
163
+
164
+ // Unlock background scrolling
165
+ this.unlockScroll();
166
+ }
167
+
168
+ private handleMessage(event: MessageEvent): void {
169
+ // Verify origin matches the portal URL
170
+ if (!this.portalOrigin || event.origin !== this.portalOrigin) {
171
+ console.warn(
172
+ '[PortalUI] Received message from unauthorized origin:',
173
+ event.origin,
174
+ 'Expected:',
175
+ this.portalOrigin
176
+ );
177
+ return;
178
+ }
179
+
180
+ const { type, userId, access_token, refresh_token, error, height, data } = event.data;
181
+ console.log('[PortalUI] Received message:', event.data);
182
+
183
+ switch (type) {
184
+ case 'portal-success': {
185
+ // Handle both direct userId and data.userId formats
186
+ const successUserId = userId || (data && data.userId);
187
+ const tokens = {
188
+ access_token: access_token || (data && data.access_token),
189
+ refresh_token: refresh_token || (data && data.refresh_token)
190
+ };
191
+ this.handlePortalSuccess(successUserId, tokens);
192
+ break;
193
+ }
194
+
195
+ case 'portal-error': {
196
+ // Handle both direct error and data.message formats
197
+ const errorMessage = error || (data && data.message);
198
+ this.handlePortalError(errorMessage);
199
+ break;
200
+ }
201
+
202
+ case 'portal-close':
203
+ this.handlePortalClose();
204
+ break;
205
+
206
+ case 'event':
207
+ this.handleGenericEvent(data);
208
+ break;
209
+
210
+ case 'resize':
211
+ this.handleResize(height);
212
+ break;
213
+
214
+ // Legacy support for old message types
215
+ case 'success':
216
+ this.handleSuccess(userId);
217
+ break;
218
+
219
+ case 'error':
220
+ this.handleError(error);
221
+ break;
222
+
223
+ case 'close':
224
+ this.handleClose();
225
+ break;
226
+
227
+ default:
228
+ console.warn('[PortalUI] Received unhandled message type:', type);
229
+ }
230
+ }
231
+
232
+ private handlePortalSuccess(userId: string, tokens?: { access_token?: string; refresh_token?: string }): void {
233
+ if (!userId) {
234
+ console.error('[PortalUI] Missing userId in portal-success message');
235
+ return;
236
+ }
237
+
238
+ console.log('[PortalUI] Portal success - User connected:', userId);
239
+ if (tokens?.access_token && tokens?.refresh_token) {
240
+ console.log('[PortalUI] Tokens received for user:', userId);
241
+ }
242
+
243
+ // Pass userId to parent (SDK will handle tokens internally)
244
+ this.options?.onSuccess?.(userId, tokens);
245
+ }
246
+
247
+ private handlePortalError(error: string): void {
248
+ console.error('[PortalUI] Portal error:', error);
249
+ this.options?.onError?.(new Error(error || 'Unknown portal error'));
250
+ }
251
+
252
+ private handlePortalClose(): void {
253
+ console.log('[PortalUI] Portal closed by user');
254
+ this.options?.onClose?.();
255
+ this.hide();
256
+ }
257
+
258
+ private handleGenericEvent(data: any): void {
259
+ if (!data || !data.type) {
260
+ console.warn('[PortalUI] Invalid event data:', data);
261
+ return;
262
+ }
263
+
264
+ console.log('[PortalUI] Generic event received:', data.type, data.data);
265
+
266
+ // Emit the event to be handled by the SDK
267
+ // This will be implemented in FinaticConnect
268
+ if (this.options?.onEvent) {
269
+ this.options.onEvent(data.type, data.data);
270
+ }
271
+ }
272
+
273
+ private handleSuccess(userId: string): void {
274
+ if (!userId) {
275
+ console.error('[PortalUI] Missing required fields in success message');
276
+ return;
277
+ }
278
+
279
+ // Pass userId to parent
280
+ this.options?.onSuccess?.(userId);
281
+ }
282
+
283
+ private handleError(error: string): void {
284
+ console.error('[PortalUI] Received error:', error);
285
+ this.options?.onError?.(new Error(error || 'Unknown error'));
286
+ }
287
+
288
+ private handleClose(): void {
289
+ console.log('[PortalUI] Received close message');
290
+ this.options?.onClose?.();
291
+ this.hide();
292
+ }
293
+
294
+ private handleResize(height: number): void {
295
+ if (height && this.iframe) {
296
+ console.log('[PortalUI] Received resize message:', height);
297
+ this.iframe.style.height = `${height}px`;
298
+ }
299
+ }
300
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ // Error types
2
+ export interface TradeAccessDeniedError {
3
+ success: false;
4
+ response_data: null;
5
+ message: string;
6
+ status_code: 403;
7
+ category: 'TRADE_ACCESS_DENIED';
8
+ }
9
+
10
+ export interface OrderNotFoundError {
11
+ success: false;
12
+ response_data: null;
13
+ message: string;
14
+ status_code: 404;
15
+ category: 'ORDER_NOT_FOUND';
16
+ }
17
+
18
+ export interface ValidationError {
19
+ success: false;
20
+ response_data: null;
21
+ message: string;
22
+ status_code: 400;
23
+ }
package/src/index.ts ADDED
@@ -0,0 +1,87 @@
1
+ // Types (explicit re-exports to avoid conflicts)
2
+ export type {
3
+ // Core API types
4
+ ApiConfig,
5
+ ApiResponse,
6
+ Order,
7
+ OptionsOrder,
8
+ BrokerAccount,
9
+ PortfolioSnapshot,
10
+ PerformanceMetrics,
11
+ UserToken,
12
+ Holding,
13
+ Portfolio,
14
+ PortalResponse,
15
+ SessionInitResponse,
16
+ SessionResponse,
17
+ OtpRequestResponse,
18
+ OtpVerifyResponse,
19
+ PortalUrlResponse,
20
+ SessionValidationResponse,
21
+ SessionAuthenticateResponse,
22
+ RequestHeaders,
23
+ SessionStatus,
24
+ BrokerOrderParams,
25
+ BrokerExtras,
26
+ CryptoOrderOptions,
27
+ OptionsOrderOptions,
28
+ OrderResponse,
29
+ TradingContext,
30
+ DeviceInfo,
31
+ BrokerOrder,
32
+ BrokerPosition,
33
+ BrokerBalance,
34
+ BrokerDataOptions,
35
+ BrokerInfo,
36
+ TokenInfo,
37
+ RefreshTokenRequest,
38
+ RefreshTokenResponse,
39
+ AccountsFilter,
40
+ OrdersFilter,
41
+ PositionsFilter,
42
+ BalancesFilter,
43
+ BrokerDataOrder,
44
+ BrokerDataPosition,
45
+ BrokerDataAccount,
46
+ FilteredOrdersResponse,
47
+ FilteredPositionsResponse,
48
+ FilteredAccountsResponse,
49
+ FilteredBalancesResponse,
50
+ BrokerConnection,
51
+ } from './types';
52
+
53
+ export type { FinaticConnectOptions, FinaticUserToken, PortalMessage } from './types/connect';
54
+ export type {
55
+ PortalProps,
56
+ PortalTheme,
57
+ PortalThemeConfig,
58
+ PortalThemePreset,
59
+ } from './types/portal';
60
+
61
+ // Error types
62
+ export type {
63
+ TradeAccessDeniedError,
64
+ OrderNotFoundError,
65
+ ValidationError,
66
+ } from './types/api/errors';
67
+
68
+ // Main SDK classes
69
+ export { ApiClient } from './core/client/ApiClient';
70
+ export { FinaticConnect } from './core/client/FinaticConnect';
71
+
72
+ // Supabase configuration
73
+ // Supabase export removed - SDK no longer depends on Supabase
74
+
75
+ // Core types and classes
76
+ export { PaginatedResult } from './types/common/pagination';
77
+
78
+ // Utilities
79
+ export * from './utils/errors';
80
+ export * from './utils/events';
81
+ export * from './utils/themeUtils';
82
+
83
+ // Theme presets
84
+ export { portalThemePresets } from './themes/portalPresets';
85
+
86
+ // Mocks (optional, for testing) - removed from main export to reduce bundle size
87
+ // export { MockFactory } from './mocks/MockFactory';