@cas-parser/connect 1.1.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.
@@ -0,0 +1,1769 @@
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react'), require('react-dom/client')) :
3
+ typeof define === 'function' && define.amd ? define(['exports', 'react', 'react-dom/client'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.PortfolioConnect = {}, global.React, global.ReactDOM));
5
+ })(this, (function (exports, React, ReactDOM) { 'use strict';
6
+
7
+ const Modal = ({ isOpen, onClose, onBack, showBackButton = false, children, width = 400, isSandbox = false, }) => {
8
+ const overlayRef = React.useRef(null);
9
+ React.useEffect(() => {
10
+ if (isOpen) {
11
+ document.body.style.overflow = 'hidden';
12
+ }
13
+ else {
14
+ document.body.style.overflow = '';
15
+ }
16
+ return () => {
17
+ document.body.style.overflow = '';
18
+ };
19
+ }, [isOpen]);
20
+ if (!isOpen)
21
+ return null;
22
+ const handleOverlayClick = (e) => {
23
+ if (e.target === overlayRef.current) {
24
+ onClose();
25
+ }
26
+ };
27
+ const handleHeaderButtonClick = () => {
28
+ if (showBackButton && onBack) {
29
+ onBack();
30
+ }
31
+ else {
32
+ onClose();
33
+ }
34
+ };
35
+ return (React.createElement("div", { ref: overlayRef, onClick: handleOverlayClick, style: {
36
+ position: 'fixed',
37
+ top: 0,
38
+ left: 0,
39
+ right: 0,
40
+ bottom: 0,
41
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
42
+ display: 'flex',
43
+ alignItems: 'center',
44
+ justifyContent: 'center',
45
+ zIndex: 10000,
46
+ padding: 16,
47
+ } },
48
+ React.createElement("div", { style: {
49
+ backgroundColor: '#fff',
50
+ borderRadius: 16,
51
+ width: '100%',
52
+ maxWidth: width,
53
+ maxHeight: '90vh',
54
+ overflow: 'auto',
55
+ position: 'relative',
56
+ boxShadow: '0 25px 80px rgba(0, 0, 0, 0.25)',
57
+ } },
58
+ isSandbox && (React.createElement("div", { style: {
59
+ backgroundColor: '#fef3c7',
60
+ borderBottom: '1px solid #fcd34d',
61
+ padding: '6px 16px',
62
+ fontSize: 12,
63
+ fontWeight: 600,
64
+ color: '#92400e',
65
+ textAlign: 'center',
66
+ borderTopLeftRadius: 16,
67
+ borderTopRightRadius: 16,
68
+ } }, "\uD83E\uDDEA Sandbox Mode \u2014 Test data only")),
69
+ React.createElement("button", { onClick: handleHeaderButtonClick, style: {
70
+ position: 'absolute',
71
+ top: isSandbox ? 40 : 12,
72
+ left: showBackButton ? 12 : 'auto',
73
+ right: showBackButton ? 'auto' : 12,
74
+ background: 'none',
75
+ border: 'none',
76
+ fontSize: showBackButton ? 20 : 28,
77
+ cursor: 'pointer',
78
+ color: '#6b7280',
79
+ lineHeight: 1,
80
+ padding: 8,
81
+ zIndex: 1,
82
+ display: 'flex',
83
+ alignItems: 'center',
84
+ justifyContent: 'center',
85
+ borderRadius: 8,
86
+ transition: 'background-color 0.15s',
87
+ }, onMouseOver: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseOut: (e) => e.currentTarget.style.backgroundColor = 'transparent', "aria-label": showBackButton ? 'Back' : 'Close' }, showBackButton ? '←' : '×'),
88
+ children)));
89
+ };
90
+
91
+ // Search by sender emails - official CAS statement sources
92
+ const CAS_SENDERS = {
93
+ 'CAMS_KFINTECH': ['donotreply@camsonline.com', 'samfs@kfintech.com'],
94
+ 'CDSL': ['ecas@cdslstatement.com'],
95
+ 'NSDL': ['nsdl-cas@nsdl.co.in'],
96
+ };
97
+ // Build search query for sender emails
98
+ const buildSearchQuery = (type) => {
99
+ const senders = type ? CAS_SENDERS[type] : Object.values(CAS_SENDERS).flat();
100
+ const fromQuery = senders.map(s => `from:${s}`).join(' OR ');
101
+ return `(${fromQuery}) has:attachment`;
102
+ };
103
+ // Official brand logos as inline SVG
104
+ const GmailLogo = () => (React.createElement("svg", { width: "20", height: "16", viewBox: "0 0 24 18", fill: "none" },
105
+ React.createElement("path", { d: "M1.636 18H5.455V8.682L0 4.91V16.364C0 17.267.733 18 1.636 18z", fill: "#4285F4" }),
106
+ React.createElement("path", { d: "M18.545 18h3.819c.904 0 1.636-.732 1.636-1.636V4.91l-5.455 3.772V18z", fill: "#34A853" }),
107
+ React.createElement("path", { d: "M18.545 1.636V8.682L24 4.91v-1.91c0-2.022-2.309-3.175-3.927-1.963l-1.528 1.109z", fill: "#FBBC04" }),
108
+ React.createElement("path", { d: "M5.455 8.682V1.636l6.545 4.91 6.545-4.91v7.046L12 13.592 5.455 8.682z", fill: "#EA4335" }),
109
+ React.createElement("path", { d: "M0 2.999v1.91l5.455 3.773V1.636L3.927.527C2.31-.685 0 .468 0 2.999z", fill: "#C5221F" })));
110
+ const OutlookLogo = () => (React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 32 32", fill: "none" },
111
+ React.createElement("path", { d: "M19.484 7.937v5.477l1.916 1.205a.489.489 0 00.21.063.489.489 0 00.229-.063l8.088-5.234a1.455 1.455 0 00-.741-1.448H19.484z", fill: "#0072C6" }),
112
+ React.createElement("path", { d: "M19.484 15.457l1.747 1.2a.522.522 0 00.543 0c-.3.181 8.073-5.378 8.073-5.378v10.795a1.474 1.474 0 01-1.474 1.474h-8.889V15.457z", fill: "#0072C6" }),
113
+ React.createElement("path", { d: "M10.44 12.932a1.927 1.927 0 00-1.384-.543 1.9 1.9 0 00-1.387.543 1.927 1.927 0 00-.543 1.384v3.086a1.927 1.927 0 00.543 1.384 1.864 1.864 0 001.384.543 1.927 1.927 0 001.387-.543 1.9 1.9 0 00.543-1.384v-3.086a1.927 1.927 0 00-.543-1.384z", fill: "#0072C6" }),
114
+ React.createElement("path", { d: "M18.044 5.116H8.044a1.474 1.474 0 00-1.474 1.474v1.209l6.3 4.5 5.174-3.15V6.59a1.474 1.474 0 00-1.474-1.474H8.044z", fill: "#0072C6" }),
115
+ React.createElement("path", { d: "M1.474 9.789h16.842v14.737H1.474A1.474 1.474 0 010 23.053V11.263a1.474 1.474 0 011.474-1.474z", fill: "#0072C6" }),
116
+ React.createElement("ellipse", { cx: "9.263", cy: "16.842", rx: "3.789", ry: "4.421", stroke: "#fff", strokeWidth: "1.5", fill: "none" })));
117
+ const YahooLogo = () => (React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 32 32", fill: "none" },
118
+ React.createElement("path", { d: "M16 2C8.268 2 2 8.268 2 16s6.268 14 14 14 14-6.268 14-14S23.732 2 16 2z", fill: "#5C0DAC" }),
119
+ React.createElement("path", { d: "M22.287 10.5h-2.844l-3.118 6.066L13.178 10.5h-2.89l4.622 8.37v4.63h2.666v-4.63l4.711-8.37z", fill: "#fff" })));
120
+ // Generate provider URLs based on selected CAS type
121
+ const getProviders = (selectedType) => {
122
+ const query = buildSearchQuery(selectedType);
123
+ const encodedQuery = encodeURIComponent(query);
124
+ return [
125
+ {
126
+ name: 'Gmail',
127
+ Logo: GmailLogo,
128
+ url: `https://mail.google.com/mail/u/0/#search/${encodedQuery}`
129
+ },
130
+ {
131
+ name: 'Outlook',
132
+ Logo: OutlookLogo,
133
+ url: `https://outlook.live.com/mail/0/deeplink?version=20200615005&search=${encodedQuery}`
134
+ },
135
+ {
136
+ name: 'Yahoo',
137
+ Logo: YahooLogo,
138
+ url: `https://mail.yahoo.com/d/search/keyword=${encodedQuery}`
139
+ }
140
+ ];
141
+ };
142
+ const ShortcutLinks = ({ selectedType, onSearchClick }) => {
143
+ const providers = getProviders(selectedType);
144
+ return (React.createElement("div", { style: { marginBottom: 16 } },
145
+ React.createElement("div", { style: {
146
+ fontSize: 12,
147
+ color: '#6b7280',
148
+ marginBottom: 10,
149
+ fontWeight: 500
150
+ } }, "Find statement in your inbox"),
151
+ React.createElement("div", { style: {
152
+ display: 'grid',
153
+ gridTemplateColumns: 'repeat(3, 1fr)',
154
+ gap: 10
155
+ } }, providers.map((provider) => (React.createElement("a", { key: provider.name, href: provider.url, target: "_blank", rel: "noopener noreferrer", onClick: () => onSearchClick?.(provider.name), style: {
156
+ display: 'flex',
157
+ flexDirection: 'column',
158
+ alignItems: 'center',
159
+ justifyContent: 'center',
160
+ padding: 10,
161
+ backgroundColor: '#f9fafb',
162
+ borderRadius: 8,
163
+ textDecoration: 'none',
164
+ border: '1px solid #e5e7eb',
165
+ transition: 'all 0.15s',
166
+ cursor: 'pointer',
167
+ }, onMouseEnter: (e) => {
168
+ e.currentTarget.style.backgroundColor = '#eff6ff';
169
+ e.currentTarget.style.borderColor = '#2563eb';
170
+ }, onMouseLeave: (e) => {
171
+ e.currentTarget.style.backgroundColor = '#f9fafb';
172
+ e.currentTarget.style.borderColor = '#e5e7eb';
173
+ } },
174
+ React.createElement(provider.Logo, null),
175
+ React.createElement("span", { style: { fontSize: 11, color: '#374151', fontWeight: 500, marginTop: 4 } }, provider.name)))))));
176
+ };
177
+
178
+ const UploadZone = ({ onFileSelect, isProcessing, progress }) => {
179
+ const [file, setFile] = React.useState(null);
180
+ const [password, setPassword] = React.useState('');
181
+ const [isDragOver, setIsDragOver] = React.useState(false);
182
+ const inputRef = React.useRef(null);
183
+ const handleDrop = React.useCallback((e) => {
184
+ e.preventDefault();
185
+ setIsDragOver(false);
186
+ const droppedFile = e.dataTransfer.files[0];
187
+ if (droppedFile?.type === 'application/pdf') {
188
+ setFile(droppedFile);
189
+ }
190
+ }, []);
191
+ const handleFileChange = (e) => {
192
+ const selectedFile = e.target.files?.[0];
193
+ if (selectedFile) {
194
+ setFile(selectedFile);
195
+ }
196
+ };
197
+ const handleSubmit = () => {
198
+ if (file) {
199
+ onFileSelect(file, password);
200
+ }
201
+ };
202
+ if (isProcessing) {
203
+ return (React.createElement("div", { style: { textAlign: 'center', padding: '50px 20px' } },
204
+ React.createElement("div", { style: {
205
+ width: 56,
206
+ height: 56,
207
+ backgroundColor: '#eff6ff',
208
+ borderRadius: '50%',
209
+ display: 'flex',
210
+ alignItems: 'center',
211
+ justifyContent: 'center',
212
+ margin: '0 auto 20px',
213
+ } },
214
+ React.createElement("svg", { width: "28", height: "28", viewBox: "0 0 24 24", fill: "none", stroke: "#2563eb", strokeWidth: "2" },
215
+ React.createElement("path", { d: "M21 12V7H5a2 2 0 0 1 0-4h14v4" }),
216
+ React.createElement("path", { d: "M3 5v14a2 2 0 0 0 2 2h16v-5" }),
217
+ React.createElement("path", { d: "M18 12a2 2 0 0 0 0 4h4v-4Z" }))),
218
+ React.createElement("h3", { style: { margin: '0 0 8px', fontSize: 18, fontWeight: 600, color: '#1f2937' } }, "Reading Your Investments"),
219
+ React.createElement("p", { style: { color: '#6b7280', marginBottom: 24, fontSize: 14 } }, "Extracting holdings, transactions & values..."),
220
+ React.createElement("div", { style: {
221
+ height: 6,
222
+ backgroundColor: '#e5e7eb',
223
+ borderRadius: 3,
224
+ overflow: 'hidden',
225
+ maxWidth: 240,
226
+ margin: '0 auto',
227
+ } },
228
+ React.createElement("div", { style: {
229
+ height: '100%',
230
+ width: `${progress}%`,
231
+ backgroundColor: '#2563eb',
232
+ transition: 'width 0.3s ease',
233
+ } })),
234
+ React.createElement("div", { style: { fontSize: 12, color: '#9ca3af', marginTop: 10 } }, progress < 50 ? 'Uploading...' : progress < 90 ? 'Processing...' : 'Almost done!')));
235
+ }
236
+ if (!file) {
237
+ return (React.createElement("div", null,
238
+ React.createElement("h3", { style: { fontSize: 18, fontWeight: 600, margin: '0 0 16px', color: '#1f2937' } }, "Upload Your Statement"),
239
+ React.createElement("div", { onDragOver: (e) => { e.preventDefault(); setIsDragOver(true); }, onDragLeave: () => setIsDragOver(false), onDrop: handleDrop, onClick: () => inputRef.current?.click(), style: {
240
+ border: `2px dashed ${isDragOver ? '#2563eb' : '#d1d5db'}`,
241
+ borderRadius: 12,
242
+ padding: '36px 20px',
243
+ textAlign: 'center',
244
+ cursor: 'pointer',
245
+ backgroundColor: isDragOver ? '#eff6ff' : '#f9fafb',
246
+ transition: 'all 0.2s',
247
+ } },
248
+ React.createElement("input", { ref: inputRef, type: "file", accept: ".pdf", onChange: handleFileChange, style: { display: 'none' } }),
249
+ React.createElement("div", { style: { marginBottom: 12 } },
250
+ React.createElement("svg", { width: "40", height: "40", viewBox: "0 0 24 24", fill: "none", stroke: "#9ca3af", strokeWidth: "1.5" },
251
+ React.createElement("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
252
+ React.createElement("polyline", { points: "14 2 14 8 20 8" }),
253
+ React.createElement("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
254
+ React.createElement("line", { x1: "16", y1: "17", x2: "8", y2: "17" }),
255
+ React.createElement("polyline", { points: "10 9 9 9 8 9" }))),
256
+ React.createElement("p", { style: { margin: 0, fontSize: 15, color: '#374151', fontWeight: 500 } }, "Drop your CAS PDF here"),
257
+ React.createElement("p", { style: { margin: '6px 0 0', fontSize: 13, color: '#9ca3af' } }, "or click to browse")),
258
+ React.createElement("div", { style: {
259
+ marginTop: 12,
260
+ fontSize: 11,
261
+ color: '#9ca3af',
262
+ textAlign: 'center'
263
+ } }, "Supported: CAMS, KFintech, CDSL, NSDL statements")));
264
+ }
265
+ return (React.createElement("div", null,
266
+ React.createElement("h3", { style: { fontSize: 18, fontWeight: 600, margin: '0 0 16px', color: '#1f2937' } }, "Enter Password"),
267
+ React.createElement("div", { style: {
268
+ display: 'flex',
269
+ alignItems: 'center',
270
+ padding: 14,
271
+ border: '1px solid #e5e7eb',
272
+ borderRadius: 10,
273
+ backgroundColor: '#f9fafb',
274
+ marginBottom: 20,
275
+ } },
276
+ React.createElement("div", { style: {
277
+ width: 40,
278
+ height: 40,
279
+ backgroundColor: '#dbeafe',
280
+ borderRadius: 8,
281
+ display: 'flex',
282
+ alignItems: 'center',
283
+ justifyContent: 'center',
284
+ marginRight: 12,
285
+ } },
286
+ React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "#2563eb", strokeWidth: "2" },
287
+ React.createElement("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
288
+ React.createElement("polyline", { points: "14 2 14 8 20 8" }))),
289
+ React.createElement("div", { style: { flex: 1, overflow: 'hidden' } },
290
+ React.createElement("div", { style: {
291
+ fontWeight: 500,
292
+ whiteSpace: 'nowrap',
293
+ overflow: 'hidden',
294
+ textOverflow: 'ellipsis',
295
+ fontSize: 14,
296
+ color: '#1f2937',
297
+ } }, file.name),
298
+ React.createElement("div", { style: { fontSize: 12, color: '#9ca3af' } },
299
+ (file.size / 1024 / 1024).toFixed(2),
300
+ " MB")),
301
+ React.createElement("button", { onClick: () => setFile(null), style: {
302
+ background: 'none',
303
+ border: 'none',
304
+ color: '#6b7280',
305
+ cursor: 'pointer',
306
+ fontSize: 13,
307
+ textDecoration: 'underline',
308
+ } }, "Change")),
309
+ React.createElement("div", { style: { marginBottom: 20 } },
310
+ React.createElement("label", { style: {
311
+ display: 'block',
312
+ marginBottom: 6,
313
+ fontWeight: 500,
314
+ fontSize: 13,
315
+ color: '#374151',
316
+ } }, "PDF Password"),
317
+ React.createElement("input", { type: "password", placeholder: "Usually your PAN (e.g. ABCDE1234F)", value: password, onChange: (e) => setPassword(e.target.value), style: {
318
+ width: '100%',
319
+ padding: '12px 14px',
320
+ fontSize: 16,
321
+ border: '1px solid #d1d5db',
322
+ borderRadius: 8,
323
+ boxSizing: 'border-box',
324
+ } }),
325
+ React.createElement("div", { style: {
326
+ fontSize: 11,
327
+ color: '#9ca3af',
328
+ marginTop: 6,
329
+ display: 'flex',
330
+ gap: 16,
331
+ } },
332
+ React.createElement("span", null, "Common passwords:"),
333
+ React.createElement("span", null, "PAN in CAPS"),
334
+ React.createElement("span", null, "DOB (DDMMYYYY)"))),
335
+ React.createElement("button", { onClick: handleSubmit, style: {
336
+ width: '100%',
337
+ padding: '16px 24px',
338
+ fontSize: 16,
339
+ fontWeight: 600,
340
+ backgroundColor: '#2563eb',
341
+ color: '#fff',
342
+ border: 'none',
343
+ borderRadius: 8,
344
+ cursor: 'pointer',
345
+ transition: 'background-color 0.2s',
346
+ } }, "Import Investments")));
347
+ };
348
+
349
+ const FetchMode = ({ prefill, onSubmit, onBack, isLoading }) => {
350
+ const [pan, setPan] = React.useState(prefill?.pan || '');
351
+ const [email, setEmail] = React.useState(prefill?.email || '');
352
+ const [error, setError] = React.useState('');
353
+ React.useEffect(() => {
354
+ if (prefill) {
355
+ if (prefill.pan)
356
+ setPan(prefill.pan);
357
+ if (prefill.email)
358
+ setEmail(prefill.email);
359
+ }
360
+ }, [prefill]);
361
+ const validatePan = (value) => /^[A-Z]{5}[0-9]{4}[A-Z]{1}$/.test(value);
362
+ const validateEmail = (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
363
+ const handleSubmit = async (e) => {
364
+ e.preventDefault();
365
+ setError('');
366
+ const upperPan = pan.toUpperCase();
367
+ if (!validatePan(upperPan)) {
368
+ setError('Invalid PAN format (e.g. ABCDE1234F)');
369
+ return;
370
+ }
371
+ if (!validateEmail(email)) {
372
+ setError('Invalid email format');
373
+ return;
374
+ }
375
+ try {
376
+ await onSubmit(upperPan, email);
377
+ }
378
+ catch (err) {
379
+ setError(err.message || 'Failed to submit request');
380
+ }
381
+ };
382
+ if (isLoading) {
383
+ return (React.createElement("div", { style: {
384
+ textAlign: 'center',
385
+ padding: '60px 20px',
386
+ display: 'flex',
387
+ flexDirection: 'column',
388
+ alignItems: 'center',
389
+ } },
390
+ React.createElement("div", { style: {
391
+ width: 48,
392
+ height: 48,
393
+ border: '3px solid #e5e7eb',
394
+ borderTopColor: '#2563eb',
395
+ borderRadius: '50%',
396
+ animation: 'spin 1s linear infinite',
397
+ marginBottom: 24,
398
+ } }),
399
+ React.createElement("style", null, `@keyframes spin { to { transform: rotate(360deg); } }`),
400
+ React.createElement("h3", { style: { margin: 0, fontSize: 18, fontWeight: 600, color: '#1f2937' } }, "Sending Request..."),
401
+ React.createElement("p", { style: { color: '#6b7280', marginTop: 8, fontSize: 14 } }, "You'll receive your statement via email shortly")));
402
+ }
403
+ return (React.createElement("div", null,
404
+ React.createElement("h2", { style: { fontSize: 20, fontWeight: 700, margin: '0 0 6px', color: '#1f2937' } }, "Request CAS via Email"),
405
+ React.createElement("p", { style: { fontSize: 14, color: '#6b7280', margin: '0 0 20px', lineHeight: 1.5 } }, "KFintech will send your statement in 2-5 minutes"),
406
+ React.createElement("div", { style: {
407
+ backgroundColor: '#f9fafb',
408
+ border: '1px solid #e5e7eb',
409
+ borderRadius: 8,
410
+ padding: 12,
411
+ marginBottom: 20,
412
+ fontSize: 12,
413
+ color: '#6b7280',
414
+ } },
415
+ React.createElement("strong", { style: { color: '#374151' } }, "Note:"),
416
+ " Use the email registered with your mutual fund investments"),
417
+ React.createElement("form", { onSubmit: handleSubmit },
418
+ React.createElement("div", { style: { marginBottom: 16 } },
419
+ React.createElement("label", { style: {
420
+ display: 'block',
421
+ fontSize: 13,
422
+ fontWeight: 500,
423
+ color: '#374151',
424
+ marginBottom: 6,
425
+ } }, "PAN Number"),
426
+ React.createElement("input", { type: "text", value: pan, onChange: (e) => setPan(e.target.value.toUpperCase()), placeholder: "ABCDE1234F", maxLength: 10, style: {
427
+ width: '100%',
428
+ height: 46,
429
+ padding: '0 14px',
430
+ fontSize: 16,
431
+ border: '1px solid #d1d5db',
432
+ borderRadius: 8,
433
+ boxSizing: 'border-box',
434
+ textTransform: 'uppercase',
435
+ } })),
436
+ React.createElement("div", { style: { marginBottom: 20 } },
437
+ React.createElement("label", { style: {
438
+ display: 'block',
439
+ fontSize: 13,
440
+ fontWeight: 500,
441
+ color: '#374151',
442
+ marginBottom: 6,
443
+ } }, "Email Address"),
444
+ React.createElement("input", { type: "email", value: email, onChange: (e) => setEmail(e.target.value), placeholder: "you@example.com", style: {
445
+ width: '100%',
446
+ height: 46,
447
+ padding: '0 14px',
448
+ fontSize: 16,
449
+ border: '1px solid #d1d5db',
450
+ borderRadius: 8,
451
+ boxSizing: 'border-box',
452
+ } }),
453
+ React.createElement("div", { style: { fontSize: 11, color: '#9ca3af', marginTop: 4 } }, "Statement will be sent to this email")),
454
+ error && (React.createElement("div", { style: {
455
+ color: '#dc2626',
456
+ fontSize: 13,
457
+ marginBottom: 16,
458
+ padding: '10px 12px',
459
+ backgroundColor: '#fef2f2',
460
+ border: '1px solid #fecaca',
461
+ borderRadius: 8,
462
+ } }, error)),
463
+ React.createElement("button", { type: "submit", style: {
464
+ width: '100%',
465
+ height: 50,
466
+ fontSize: 16,
467
+ fontWeight: 600,
468
+ backgroundColor: '#2563eb',
469
+ color: '#fff',
470
+ border: 'none',
471
+ borderRadius: 8,
472
+ cursor: 'pointer',
473
+ transition: 'background-color 0.2s',
474
+ } }, "Request Statement"),
475
+ React.createElement("div", { style: { textAlign: 'center', marginTop: 16 } },
476
+ React.createElement("button", { type: "button", onClick: onBack, style: {
477
+ background: 'none',
478
+ border: 'none',
479
+ color: '#6b7280',
480
+ fontSize: 13,
481
+ cursor: 'pointer',
482
+ textDecoration: 'underline',
483
+ } }, "\u2190 I already have my file")))));
484
+ };
485
+
486
+ const CDSLFetchMode = ({ prefill, onRequestOtp, onVerifyOtp, onBack, }) => {
487
+ const [step, setStep] = React.useState('INPUT');
488
+ const [pan, setPan] = React.useState(prefill?.pan || '');
489
+ const [boId, setBoId] = React.useState(prefill?.boId || '');
490
+ const [dob, setDob] = React.useState(prefill?.dob || '');
491
+ const [otp, setOtp] = React.useState('');
492
+ const [sessionId, setSessionId] = React.useState('');
493
+ const [error, setError] = React.useState('');
494
+ const [isLoading, setIsLoading] = React.useState(false);
495
+ React.useEffect(() => {
496
+ if (prefill) {
497
+ if (prefill.pan)
498
+ setPan(prefill.pan);
499
+ if (prefill.boId)
500
+ setBoId(prefill.boId);
501
+ if (prefill.dob)
502
+ setDob(prefill.dob);
503
+ }
504
+ }, [prefill]);
505
+ const validatePan = (value) => /^[A-Z]{5}[0-9]{4}[A-Z]{1}$/.test(value);
506
+ const validateBoId = (value) => /^\d{16}$/.test(value);
507
+ const validateDob = (value) => /^\d{4}-\d{2}-\d{2}$/.test(value);
508
+ const handleRequestOtp = async (e) => {
509
+ e.preventDefault();
510
+ setError('');
511
+ const upperPan = pan.toUpperCase();
512
+ if (!validatePan(upperPan)) {
513
+ setError('Invalid PAN format (e.g. ABCDE1234F)');
514
+ return;
515
+ }
516
+ if (!validateBoId(boId)) {
517
+ setError('BO ID must be 16 digits');
518
+ return;
519
+ }
520
+ if (!validateDob(dob)) {
521
+ setError('Date of birth must be YYYY-MM-DD');
522
+ return;
523
+ }
524
+ setIsLoading(true);
525
+ try {
526
+ const sid = await onRequestOtp(upperPan, boId, dob);
527
+ setSessionId(sid);
528
+ setStep('OTP');
529
+ }
530
+ catch (err) {
531
+ setError(err.message || 'Failed to request OTP');
532
+ }
533
+ finally {
534
+ setIsLoading(false);
535
+ }
536
+ };
537
+ const handleVerifyOtp = async (e) => {
538
+ e.preventDefault();
539
+ setError('');
540
+ if (otp.length !== 4) {
541
+ setError('Please enter the 4-digit OTP');
542
+ return;
543
+ }
544
+ setIsLoading(true);
545
+ setStep('LOADING');
546
+ try {
547
+ await onVerifyOtp(sessionId, otp, pan.toUpperCase()); // Pass PAN as password for parsing
548
+ setStep('SUCCESS');
549
+ }
550
+ catch (err) {
551
+ setError(err.message || 'Something went wrong. Please try again.');
552
+ setStep('ERROR');
553
+ setIsLoading(false);
554
+ }
555
+ };
556
+ // Error state with retry
557
+ if (step === 'ERROR') {
558
+ return (React.createElement("div", { style: {
559
+ textAlign: 'center',
560
+ padding: '40px 20px',
561
+ } },
562
+ React.createElement("div", { style: {
563
+ width: 56,
564
+ height: 56,
565
+ backgroundColor: '#fef2f2',
566
+ borderRadius: '50%',
567
+ display: 'flex',
568
+ alignItems: 'center',
569
+ justifyContent: 'center',
570
+ margin: '0 auto 20px',
571
+ } },
572
+ React.createElement("svg", { width: "28", height: "28", viewBox: "0 0 24 24", fill: "none", stroke: "#dc2626", strokeWidth: "2" },
573
+ React.createElement("circle", { cx: "12", cy: "12", r: "10" }),
574
+ React.createElement("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
575
+ React.createElement("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" }))),
576
+ React.createElement("h3", { style: { margin: '0 0 8px', fontSize: 18, fontWeight: 600, color: '#1f2937' } }, "Import Failed"),
577
+ React.createElement("p", { style: { color: '#6b7280', fontSize: 14, margin: '0 0 24px', lineHeight: 1.5 } }, error || 'Something went wrong while fetching your portfolio.'),
578
+ React.createElement("div", { style: { display: 'flex', flexDirection: 'column', gap: 12 } },
579
+ React.createElement("button", { onClick: () => { setStep('OTP'); setError(''); setOtp(''); }, style: {
580
+ width: '100%',
581
+ height: 46,
582
+ fontSize: 15,
583
+ fontWeight: 600,
584
+ backgroundColor: '#2563eb',
585
+ color: '#fff',
586
+ border: 'none',
587
+ borderRadius: 8,
588
+ cursor: 'pointer',
589
+ } }, "Try Again"),
590
+ React.createElement("button", { onClick: () => { setStep('INPUT'); setError(''); setOtp(''); }, style: {
591
+ width: '100%',
592
+ height: 46,
593
+ fontSize: 15,
594
+ fontWeight: 500,
595
+ backgroundColor: '#fff',
596
+ color: '#6b7280',
597
+ border: '1px solid #e5e7eb',
598
+ borderRadius: 8,
599
+ cursor: 'pointer',
600
+ } }, "Start Over"))));
601
+ }
602
+ // Loading state
603
+ if (step === 'LOADING') {
604
+ return (React.createElement("div", { style: {
605
+ textAlign: 'center',
606
+ padding: '60px 20px',
607
+ display: 'flex',
608
+ flexDirection: 'column',
609
+ alignItems: 'center',
610
+ } },
611
+ React.createElement("div", { style: {
612
+ width: 48,
613
+ height: 48,
614
+ border: '3px solid #e5e7eb',
615
+ borderTopColor: '#2563eb',
616
+ borderRadius: '50%',
617
+ animation: 'spin 1s linear infinite',
618
+ marginBottom: 24,
619
+ } }),
620
+ React.createElement("style", null, `@keyframes spin { to { transform: rotate(360deg); } }`),
621
+ React.createElement("h3", { style: { margin: 0, fontSize: 18, fontWeight: 600, color: '#1f2937' } }, "Importing Your Portfolio"),
622
+ React.createElement("p", { style: { color: '#6b7280', marginTop: 8, fontSize: 14 } }, "Securely fetching your holdings...")));
623
+ }
624
+ // Success state
625
+ if (step === 'SUCCESS') {
626
+ return (React.createElement("div", { style: {
627
+ textAlign: 'center',
628
+ padding: '40px 20px',
629
+ } },
630
+ React.createElement("div", { style: {
631
+ width: 56,
632
+ height: 56,
633
+ backgroundColor: '#dcfce7',
634
+ borderRadius: '50%',
635
+ display: 'flex',
636
+ alignItems: 'center',
637
+ justifyContent: 'center',
638
+ margin: '0 auto 20px',
639
+ } },
640
+ React.createElement("svg", { width: "28", height: "28", viewBox: "0 0 24 24", fill: "none", stroke: "#16a34a", strokeWidth: "2.5" },
641
+ React.createElement("polyline", { points: "20 6 9 17 4 12" }))),
642
+ React.createElement("h3", { style: { margin: '0 0 8px', fontSize: 20, fontWeight: 600, color: '#1f2937' } }, "Portfolio Imported!"),
643
+ React.createElement("p", { style: { color: '#6b7280', fontSize: 14, margin: 0 } }, "Your demat holdings are now available")));
644
+ }
645
+ // OTP input step
646
+ if (step === 'OTP') {
647
+ return (React.createElement("div", null,
648
+ React.createElement("h2", { style: { fontSize: 20, fontWeight: 700, margin: '0 0 6px', color: '#1f2937' } }, "Verify OTP"),
649
+ React.createElement("p", { style: { fontSize: 14, color: '#6b7280', margin: '0 0 20px', lineHeight: 1.5 } }, "CDSL has sent an OTP to your registered mobile"),
650
+ React.createElement("form", { onSubmit: handleVerifyOtp },
651
+ React.createElement("div", { style: { marginBottom: 20 } },
652
+ React.createElement("label", { style: {
653
+ display: 'block',
654
+ fontSize: 13,
655
+ fontWeight: 500,
656
+ color: '#374151',
657
+ marginBottom: 6,
658
+ } }, "OTP Code"),
659
+ React.createElement("input", { type: "text", value: otp, onChange: (e) => setOtp(e.target.value.replace(/\D/g, '').slice(0, 4)), placeholder: "\u2022\u2022\u2022\u2022", maxLength: 4, autoFocus: true, style: {
660
+ width: '100%',
661
+ height: 50,
662
+ padding: '0 14px',
663
+ fontSize: 20,
664
+ fontWeight: 600,
665
+ letterSpacing: 8,
666
+ textAlign: 'center',
667
+ border: '1px solid #d1d5db',
668
+ borderRadius: 8,
669
+ boxSizing: 'border-box',
670
+ } })),
671
+ error && (React.createElement("div", { style: {
672
+ color: '#dc2626',
673
+ fontSize: 13,
674
+ marginBottom: 16,
675
+ padding: '10px 12px',
676
+ backgroundColor: '#fef2f2',
677
+ border: '1px solid #fecaca',
678
+ borderRadius: 8,
679
+ } }, error)),
680
+ React.createElement("button", { type: "submit", disabled: isLoading, style: {
681
+ width: '100%',
682
+ height: 50,
683
+ fontSize: 16,
684
+ fontWeight: 600,
685
+ backgroundColor: isLoading ? '#93c5fd' : '#2563eb',
686
+ color: '#fff',
687
+ border: 'none',
688
+ borderRadius: 8,
689
+ cursor: isLoading ? 'not-allowed' : 'pointer',
690
+ } }, isLoading ? 'Verifying...' : 'Import Portfolio'),
691
+ React.createElement("div", { style: { textAlign: 'center', marginTop: 16 } },
692
+ React.createElement("button", { type: "button", onClick: () => { setStep('INPUT'); setError(''); }, style: {
693
+ background: 'none',
694
+ border: 'none',
695
+ color: '#6b7280',
696
+ fontSize: 13,
697
+ cursor: 'pointer',
698
+ textDecoration: 'underline',
699
+ } }, "\u2190 Go back")))));
700
+ }
701
+ // Input step (default)
702
+ return (React.createElement("div", null,
703
+ React.createElement("h2", { style: { fontSize: 20, fontWeight: 700, margin: '0 0 6px', color: '#1f2937' } }, "Import from CDSL"),
704
+ React.createElement("p", { style: { fontSize: 14, color: '#6b7280', margin: '0 0 20px', lineHeight: 1.5 } }, "Securely fetch your demat holdings"),
705
+ React.createElement("div", { style: {
706
+ backgroundColor: '#f9fafb',
707
+ border: '1px solid #e5e7eb',
708
+ borderRadius: 8,
709
+ padding: 12,
710
+ marginBottom: 20,
711
+ fontSize: 12,
712
+ color: '#6b7280',
713
+ } },
714
+ React.createElement("strong", { style: { color: '#374151' } }, "Where to find BO ID:"),
715
+ " Check your broker app under Demat Details, or any CDSL statement"),
716
+ React.createElement("form", { onSubmit: handleRequestOtp },
717
+ React.createElement("div", { style: { marginBottom: 16 } },
718
+ React.createElement("label", { style: {
719
+ display: 'block',
720
+ fontSize: 13,
721
+ fontWeight: 500,
722
+ color: '#374151',
723
+ marginBottom: 6,
724
+ } }, "PAN Number"),
725
+ React.createElement("input", { type: "text", value: pan, onChange: (e) => setPan(e.target.value.toUpperCase()), placeholder: "ABCDE1234F", maxLength: 10, style: {
726
+ width: '100%',
727
+ height: 46,
728
+ padding: '0 14px',
729
+ fontSize: 16,
730
+ border: '1px solid #d1d5db',
731
+ borderRadius: 8,
732
+ boxSizing: 'border-box',
733
+ textTransform: 'uppercase',
734
+ } })),
735
+ React.createElement("div", { style: { marginBottom: 16 } },
736
+ React.createElement("label", { style: {
737
+ display: 'block',
738
+ fontSize: 13,
739
+ fontWeight: 500,
740
+ color: '#374151',
741
+ marginBottom: 6,
742
+ } }, "BO ID (16 digits)"),
743
+ React.createElement("input", { type: "text", value: boId, onChange: (e) => setBoId(e.target.value.replace(/\D/g, '').slice(0, 16)), placeholder: "1234567890123456", maxLength: 16, style: {
744
+ width: '100%',
745
+ height: 46,
746
+ padding: '0 14px',
747
+ fontSize: 16,
748
+ border: '1px solid #d1d5db',
749
+ borderRadius: 8,
750
+ boxSizing: 'border-box',
751
+ } })),
752
+ React.createElement("div", { style: { marginBottom: 20 } },
753
+ React.createElement("label", { style: {
754
+ display: 'block',
755
+ fontSize: 13,
756
+ fontWeight: 500,
757
+ color: '#374151',
758
+ marginBottom: 6,
759
+ } }, "Date of Birth"),
760
+ React.createElement("input", { type: "date", value: dob, onChange: (e) => setDob(e.target.value), style: {
761
+ width: '100%',
762
+ height: 46,
763
+ padding: '0 14px',
764
+ fontSize: 16,
765
+ border: '1px solid #d1d5db',
766
+ borderRadius: 8,
767
+ boxSizing: 'border-box',
768
+ } })),
769
+ error && (React.createElement("div", { style: {
770
+ color: '#dc2626',
771
+ fontSize: 13,
772
+ marginBottom: 16,
773
+ padding: '10px 12px',
774
+ backgroundColor: '#fef2f2',
775
+ border: '1px solid #fecaca',
776
+ borderRadius: 8,
777
+ } }, error)),
778
+ React.createElement("button", { type: "submit", disabled: isLoading, style: {
779
+ width: '100%',
780
+ height: 50,
781
+ fontSize: 16,
782
+ fontWeight: 600,
783
+ backgroundColor: isLoading ? '#93c5fd' : '#2563eb',
784
+ color: '#fff',
785
+ border: 'none',
786
+ borderRadius: 8,
787
+ cursor: isLoading ? 'not-allowed' : 'pointer',
788
+ } }, isLoading ? 'Requesting OTP...' : 'Request OTP'),
789
+ React.createElement("div", { style: { textAlign: 'center', marginTop: 16 } },
790
+ React.createElement("button", { type: "button", onClick: onBack, style: {
791
+ background: 'none',
792
+ border: 'none',
793
+ color: '#6b7280',
794
+ fontSize: 13,
795
+ cursor: 'pointer',
796
+ textDecoration: 'underline',
797
+ } }, "\u2190 I already have my file")))));
798
+ };
799
+
800
+ const DEFAULT_API_URL = 'https://portfolio-parser.api.casparser.in';
801
+ const DEFAULT_AUTH_URL = 'https://client-apis.casparser.in';
802
+ /**
803
+ * Creates an API client for making requests to CASParser API.
804
+ *
805
+ * @param accessToken - Your access token (get one at docs.casparser.in)
806
+ * @param baseUrl - Optional custom API base URL
807
+ * @returns API client instance
808
+ */
809
+ function createApiClient(accessToken, baseUrl) {
810
+ return {
811
+ baseUrl: baseUrl || DEFAULT_API_URL,
812
+ apiKey: accessToken,
813
+ };
814
+ }
815
+ /**
816
+ * Request a short-lived access token from the API key.
817
+ * Use this on your backend to generate tokens for frontend use.
818
+ *
819
+ * @param apiKey - Your API key (do NOT expose this on frontend)
820
+ * @param expiryMinutes - Token validity in minutes (max 60, default 60)
821
+ * @param authUrl - Optional custom auth URL
822
+ * @returns Access token response with SK_ prefixed token
823
+ */
824
+ async function requestAccessToken(apiKey, expiryMinutes = 60, authUrl) {
825
+ const url = authUrl || DEFAULT_AUTH_URL;
826
+ const response = await fetch(`${url}/v1/access-token`, {
827
+ method: 'POST',
828
+ headers: {
829
+ 'Content-Type': 'application/json',
830
+ 'x-api-key': apiKey,
831
+ },
832
+ body: JSON.stringify({
833
+ expiry_minutes: Math.min(expiryMinutes, 60),
834
+ }),
835
+ });
836
+ if (!response.ok) {
837
+ const errorData = await response.json().catch(() => ({}));
838
+ throw new Error(errorData.detail || errorData.message || 'Failed to get access token');
839
+ }
840
+ return response.json();
841
+ }
842
+ /**
843
+ * Check if a token is an access token (SK_ prefix)
844
+ */
845
+ function isAccessToken(token) {
846
+ return token?.startsWith('SK_') || false;
847
+ }
848
+ async function parseStatement(client, file, password, onProgress) {
849
+ const formData = new FormData();
850
+ formData.append('pdf_file', file);
851
+ if (password) {
852
+ formData.append('password', password);
853
+ }
854
+ // Simulate progress since fetch doesn't support upload progress natively
855
+ if (onProgress) {
856
+ onProgress(30);
857
+ }
858
+ const response = await fetch(`${client.baseUrl}/v4/smart/parse`, {
859
+ method: 'POST',
860
+ headers: {
861
+ 'x-api-key': client.apiKey,
862
+ 'Authorization': `Token ${client.apiKey}`,
863
+ },
864
+ body: formData,
865
+ });
866
+ if (onProgress) {
867
+ onProgress(80);
868
+ }
869
+ if (!response.ok) {
870
+ const errorData = await response.json().catch(() => ({}));
871
+ throw { response: { data: errorData }, message: errorData.msg || 'Request failed' };
872
+ }
873
+ return response.json();
874
+ }
875
+ /**
876
+ * Parse a statement from a URL (used for CDSL fetch results)
877
+ */
878
+ async function parseStatementFromUrl(client, pdfUrl, password, onProgress) {
879
+ if (onProgress) {
880
+ onProgress(30);
881
+ }
882
+ const body = { pdf_url: pdfUrl };
883
+ if (password) {
884
+ body.password = password;
885
+ }
886
+ const response = await fetch(`${client.baseUrl}/v4/smart/parse`, {
887
+ method: 'POST',
888
+ headers: {
889
+ 'Content-Type': 'application/json',
890
+ 'x-api-key': client.apiKey,
891
+ 'Authorization': `Token ${client.apiKey}`,
892
+ },
893
+ body: JSON.stringify(body),
894
+ });
895
+ if (onProgress) {
896
+ onProgress(80);
897
+ }
898
+ if (!response.ok) {
899
+ const errorData = await response.json().catch(() => ({}));
900
+ throw { response: { data: errorData }, message: errorData.msg || 'Request failed' };
901
+ }
902
+ return response.json();
903
+ }
904
+ async function generateCAS(client, email, pan, options) {
905
+ // Format dates as YYYY-MM-DD per API spec
906
+ const today = new Date();
907
+ const toDate = options?.toDate || today.toISOString().split('T')[0];
908
+ const fromDate = options?.fromDate || '2000-01-01';
909
+ const password = options?.password || 'Abcdefghi12$';
910
+ // Using the /v4/generate endpoint from portfolio-tracking
911
+ const response = await fetch(`${client.baseUrl}/v4/generate`, {
912
+ method: 'POST',
913
+ headers: {
914
+ 'Content-Type': 'application/json',
915
+ 'x-api-key': client.apiKey,
916
+ 'Authorization': `Token ${client.apiKey}`,
917
+ },
918
+ body: JSON.stringify({
919
+ email,
920
+ pan_no: pan,
921
+ from_date: fromDate,
922
+ to_date: toDate,
923
+ password,
924
+ cas_authority: 'kfintech',
925
+ }),
926
+ });
927
+ if (!response.ok) {
928
+ const errorData = await response.json().catch(() => ({}));
929
+ throw { response: { data: errorData }, message: errorData.msg || errorData.message || 'Request failed' };
930
+ }
931
+ return response.json();
932
+ }
933
+ async function cdslFetchRequestOtp(client, pan, boId, dob) {
934
+ const response = await fetch(`${client.baseUrl}/v4/cdsl/fetch`, {
935
+ method: 'POST',
936
+ headers: {
937
+ 'Content-Type': 'application/json',
938
+ 'x-api-key': client.apiKey,
939
+ 'Authorization': `Token ${client.apiKey}`,
940
+ },
941
+ body: JSON.stringify({
942
+ pan: pan.toUpperCase(),
943
+ bo_id: boId,
944
+ dob,
945
+ }),
946
+ });
947
+ if (!response.ok) {
948
+ const errorData = await response.json().catch(() => ({}));
949
+ throw { response: { data: errorData }, message: errorData.msg || 'Failed to request OTP' };
950
+ }
951
+ return response.json();
952
+ }
953
+ async function cdslFetchVerifyOtp(client, sessionId, otp, numPeriods = 6) {
954
+ const response = await fetch(`${client.baseUrl}/v4/cdsl/fetch/${sessionId}/verify`, {
955
+ method: 'POST',
956
+ headers: {
957
+ 'Content-Type': 'application/json',
958
+ 'x-api-key': client.apiKey,
959
+ 'Authorization': `Token ${client.apiKey}`,
960
+ },
961
+ body: JSON.stringify({
962
+ otp,
963
+ num_periods: numPeriods,
964
+ }),
965
+ });
966
+ if (!response.ok) {
967
+ const errorData = await response.json().catch(() => ({}));
968
+ throw { response: { data: errorData }, message: errorData.msg || 'Failed to verify OTP' };
969
+ }
970
+ return response.json();
971
+ }
972
+
973
+ const DEFAULT_LOGO_URL = 'https://casparser.in/assets/images/section-navigation01-image01.png';
974
+ const BROKER_LOGO_BASE = 'https://assets.smallcase.com/smallcase/assets/brokerLogo/small';
975
+ const CAS_PORTALS = {
976
+ 'CAMS_KFINTECH': { name: 'KFintech', url: 'https://mfs.kfintech.com/investor/General/ConsolidatedAccountStatement' },
977
+ 'CDSL': { name: 'CDSL', url: 'https://www.cdslindia.com/cas/logincas.aspx' },
978
+ 'NSDL': { name: 'NSDL', url: 'https://nsdlcas.nsdl.com/' },
979
+ };
980
+ // Default broker list with logos (can be overridden via config.brokers)
981
+ const DEFAULT_BROKERS = ([
982
+ { name: 'Zerodha', depository: 'CDSL', logo: `${BROKER_LOGO_BASE}/kite.svg` },
983
+ { name: 'Groww', depository: 'CDSL', logo: `${BROKER_LOGO_BASE}/groww.svg` },
984
+ { name: 'Angel One', depository: 'CDSL', logo: `${BROKER_LOGO_BASE}/angelbroking.svg` },
985
+ { name: 'Upstox', depository: 'CDSL', logo: `${BROKER_LOGO_BASE}/upstox.svg` },
986
+ { name: '5paisa', depository: 'CDSL', logo: `${BROKER_LOGO_BASE}/fivepaisa.svg` },
987
+ { name: 'Dhan', depository: 'CDSL', logo: `${BROKER_LOGO_BASE}/dhan.svg` },
988
+ { name: 'Alice Blue', depository: 'CDSL', logo: `${BROKER_LOGO_BASE}/aliceblue.svg` },
989
+ { name: 'Fisdom', depository: 'CDSL', logo: `${BROKER_LOGO_BASE}/fisdom.svg` },
990
+ { name: 'FundzBazar', depository: 'CDSL', logo: `${BROKER_LOGO_BASE}/fundzbazar.svg` },
991
+ { name: 'HDFC SKY', depository: 'CDSL', logo: `${BROKER_LOGO_BASE}/hdfcsky.svg` },
992
+ { name: 'ICICI Direct', depository: 'NSDL', logo: `${BROKER_LOGO_BASE}/icici.svg` },
993
+ { name: 'HDFC Securities', depository: 'NSDL', logo: `${BROKER_LOGO_BASE}/hdfc.svg` },
994
+ { name: 'Kotak Securities', depository: 'NSDL', logo: `${BROKER_LOGO_BASE}/kotak.svg` },
995
+ { name: 'SBI Securities', depository: 'NSDL', logo: `${BROKER_LOGO_BASE}/sbi.svg` },
996
+ { name: 'Motilal Oswal', depository: 'NSDL', logo: `${BROKER_LOGO_BASE}/motilal.svg` },
997
+ { name: 'IIFL Capital', depository: 'NSDL', logo: `${BROKER_LOGO_BASE}/iifl.svg` },
998
+ { name: 'Nuvama Wealth', depository: 'NSDL', logo: `${BROKER_LOGO_BASE}/edelweiss.svg` },
999
+ { name: 'Trustline', depository: 'NSDL', logo: `${BROKER_LOGO_BASE}/trustline.svg` },
1000
+ { name: 'AxisDirect', depository: 'NSDL', logo: `${BROKER_LOGO_BASE}/axis.svg` },
1001
+ ]).sort((a, b) => a.name.localeCompare(b.name));
1002
+ const PortfolioConnectWidget = ({ isOpen, onClose, apiKey, apiBaseUrl, config = {}, onSuccess, onError, onEvent, }) => {
1003
+ const [mode, setMode] = React.useState('LOCATE');
1004
+ const [selectedType, setSelectedType] = React.useState('CAMS_KFINTECH');
1005
+ const [isProcessing, setIsProcessing] = React.useState(false);
1006
+ const [progress, setProgress] = React.useState(0);
1007
+ const [fetchLoading, setFetchLoading] = React.useState(false);
1008
+ const [lastError, setLastError] = React.useState(null);
1009
+ const [selectedBroker, setSelectedBroker] = React.useState(null);
1010
+ // Use ref to avoid stale closure issues and prevent infinite loops
1011
+ const onEventRef = React.useRef(onEvent);
1012
+ onEventRef.current = onEvent;
1013
+ // Track progress interval for cleanup on unmount
1014
+ const progressIntervalRef = React.useRef(null);
1015
+ // Cleanup interval on unmount to prevent memory leaks
1016
+ React.useEffect(() => {
1017
+ return () => {
1018
+ if (progressIntervalRef.current) {
1019
+ clearInterval(progressIntervalRef.current);
1020
+ progressIntervalRef.current = null;
1021
+ }
1022
+ };
1023
+ }, []);
1024
+ const apiClient = createApiClient(apiKey, apiBaseUrl);
1025
+ const emitEvent = (event, metadata = {}) => {
1026
+ onEventRef.current?.(event, { timestamp: Date.now(), ...metadata });
1027
+ };
1028
+ // Determine allowed types from config, default to all
1029
+ const allowedTypes = config.allowedTypes || ['CAMS_KFINTECH', 'CDSL', 'NSDL'];
1030
+ config.enableGenerator && selectedType === 'CAMS_KFINTECH';
1031
+ config.enableCdslFetch && selectedType === 'CDSL';
1032
+ const showShortcuts = config.showShortcuts !== false; // Default true
1033
+ const showPortalLinks = config.showPortalLinks !== false; // Default true
1034
+ React.useEffect(() => {
1035
+ if (isOpen) {
1036
+ // Always start at home (LOCATE) screen
1037
+ const firstType = allowedTypes[0] || 'CAMS_KFINTECH';
1038
+ setSelectedType(firstType);
1039
+ setMode('LOCATE');
1040
+ setIsProcessing(false);
1041
+ setProgress(0);
1042
+ setLastError(null); // Clear any previous errors
1043
+ emitEvent('WIDGET_OPENED');
1044
+ }
1045
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1046
+ }, [isOpen]);
1047
+ const handleClose = () => {
1048
+ emitEvent('WIDGET_CLOSED');
1049
+ onClose();
1050
+ };
1051
+ const handleModeSwitch = (newMode) => {
1052
+ setMode(newMode);
1053
+ emitEvent('MODE_SWITCHED', { from: mode, to: newMode });
1054
+ };
1055
+ const handleBrokerSelect = (broker) => {
1056
+ if (broker) {
1057
+ setSelectedBroker(broker.name);
1058
+ setSelectedType(broker.depository);
1059
+ emitEvent('BROKER_SELECTED', { broker: broker.name, depository: broker.depository });
1060
+ // If CDSL and CDSL fetch is enabled, show options
1061
+ if (broker.depository === 'CDSL' && config.enableCdslFetch) {
1062
+ setMode('DEMAT_OPTIONS');
1063
+ }
1064
+ else {
1065
+ setMode('UPLOAD');
1066
+ }
1067
+ }
1068
+ else {
1069
+ // "Other" selected - let user choose depository manually
1070
+ setSelectedBroker('Other');
1071
+ }
1072
+ };
1073
+ const handleManualDepositorySelect = (depository) => {
1074
+ setSelectedType(depository);
1075
+ emitEvent('BROKER_SELECTED', { broker: 'Other', depository });
1076
+ // If CDSL and CDSL fetch is enabled, show options
1077
+ if (depository === 'CDSL' && config.enableCdslFetch) {
1078
+ setMode('DEMAT_OPTIONS');
1079
+ }
1080
+ else {
1081
+ setMode('UPLOAD');
1082
+ }
1083
+ };
1084
+ const handleFileProcess = async (file, password) => {
1085
+ setIsProcessing(true);
1086
+ setProgress(10);
1087
+ emitEvent('UPLOAD_STARTED', { filename: file.name, size: file.size });
1088
+ progressIntervalRef.current = setInterval(() => {
1089
+ setProgress(prev => Math.min(prev + Math.random() * 10, 90));
1090
+ }, 500);
1091
+ try {
1092
+ const startTime = Date.now();
1093
+ const response = await parseStatement(apiClient, file, password, (percent) => setProgress(percent));
1094
+ if (progressIntervalRef.current) {
1095
+ clearInterval(progressIntervalRef.current);
1096
+ progressIntervalRef.current = null;
1097
+ }
1098
+ setProgress(100);
1099
+ const metadata = {
1100
+ filename: file.name,
1101
+ parser_type: response.cas_author || 'AUTO',
1102
+ parse_duration_ms: Date.now() - startTime,
1103
+ };
1104
+ emitEvent('PARSE_SUCCESS', metadata);
1105
+ setTimeout(() => {
1106
+ onSuccess(response, metadata);
1107
+ handleClose();
1108
+ }, 500);
1109
+ }
1110
+ catch (error) {
1111
+ if (progressIntervalRef.current) {
1112
+ clearInterval(progressIntervalRef.current);
1113
+ progressIntervalRef.current = null;
1114
+ }
1115
+ setIsProcessing(false);
1116
+ setProgress(0);
1117
+ const casError = {
1118
+ code: 'PARSE_ERROR',
1119
+ message: error.response?.data?.msg || error.message || 'Failed to parse statement',
1120
+ details: error.response?.data,
1121
+ };
1122
+ emitEvent('PARSE_ERROR', { error: casError });
1123
+ setLastError(casError); // Show error in widget, let user retry
1124
+ onError?.(casError);
1125
+ }
1126
+ };
1127
+ const handleRetry = () => {
1128
+ setLastError(null);
1129
+ setMode('UPLOAD');
1130
+ };
1131
+ const handleFetchSubmit = async (pan, email) => {
1132
+ setFetchLoading(true);
1133
+ emitEvent('GENERATOR_STARTED', { pan: pan.slice(0, 4) + '***' });
1134
+ try {
1135
+ // Pass generator config options (dateRange, password) if provided
1136
+ await generateCAS(apiClient, email, pan, config.generator);
1137
+ emitEvent('GENERATOR_SUCCESS');
1138
+ setFetchLoading(false);
1139
+ handleModeSwitch('LOCATE');
1140
+ }
1141
+ catch (error) {
1142
+ setFetchLoading(false);
1143
+ const casError = {
1144
+ code: 'GENERATOR_ERROR',
1145
+ message: error.response?.data?.message || error.message || 'Failed to request statement',
1146
+ details: error.response?.data,
1147
+ };
1148
+ emitEvent('GENERATOR_ERROR', { error: casError });
1149
+ throw new Error(casError.message);
1150
+ }
1151
+ };
1152
+ // CDSL Fetch handlers
1153
+ const handleCdslRequestOtp = async (pan, boId, dob) => {
1154
+ emitEvent('CDSL_FETCH_STARTED', { pan: pan.slice(0, 4) + '***' });
1155
+ try {
1156
+ const response = await cdslFetchRequestOtp(apiClient, pan, boId, dob);
1157
+ emitEvent('CDSL_OTP_SENT', { session_id: response.session_id });
1158
+ return response.session_id;
1159
+ }
1160
+ catch (error) {
1161
+ const casError = {
1162
+ code: 'CDSL_FETCH_ERROR',
1163
+ message: error.message || 'Failed to request OTP',
1164
+ details: error.response?.data,
1165
+ };
1166
+ emitEvent('CDSL_FETCH_ERROR', { error: casError });
1167
+ throw new Error(casError.message);
1168
+ }
1169
+ };
1170
+ const handleCdslVerifyOtp = async (sessionId, otp, pan) => {
1171
+ try {
1172
+ const response = await cdslFetchVerifyOtp(apiClient, sessionId, otp);
1173
+ emitEvent('CDSL_OTP_VERIFIED');
1174
+ // Files are ready - emit event with all file URLs for developer interception
1175
+ emitEvent('CDSL_FETCH_SUCCESS', {
1176
+ files: response.files,
1177
+ count: response.files.length,
1178
+ });
1179
+ // Auto-parse the most recent file (first in list) - PAN is the password
1180
+ await handleCdslAutoParse(response.files, pan);
1181
+ }
1182
+ catch (error) {
1183
+ const casError = {
1184
+ code: 'CDSL_FETCH_ERROR',
1185
+ message: error.message || 'Failed to verify OTP',
1186
+ details: error.response?.data,
1187
+ };
1188
+ emitEvent('CDSL_FETCH_ERROR', { error: casError });
1189
+ throw new Error(casError.message);
1190
+ }
1191
+ };
1192
+ const handleCdslAutoParse = async (files, pan) => {
1193
+ if (!files || files.length === 0) {
1194
+ const casError = {
1195
+ code: 'CDSL_FETCH_ERROR',
1196
+ message: 'No files returned from CDSL',
1197
+ };
1198
+ emitEvent('CDSL_FETCH_ERROR', { error: casError });
1199
+ onError?.(casError);
1200
+ return;
1201
+ }
1202
+ // Use the first (most recent) file
1203
+ const targetFile = files[0];
1204
+ setIsProcessing(true);
1205
+ setProgress(10);
1206
+ emitEvent('PARSE_STARTED', { filename: targetFile.filename, source: 'CDSL_FETCH' });
1207
+ try {
1208
+ const startTime = Date.now();
1209
+ // Parse via URL - smart parse API accepts pdf_url, PAN is the password
1210
+ const response = await parseStatementFromUrl(apiClient, targetFile.url, pan, // CDSL files are password protected with PAN
1211
+ (percent) => setProgress(percent));
1212
+ setProgress(100);
1213
+ const metadata = {
1214
+ filename: targetFile.filename,
1215
+ parser_type: response.cas_author || 'CDSL',
1216
+ parse_duration_ms: Date.now() - startTime,
1217
+ source: 'CDSL_FETCH',
1218
+ all_files: files, // Include all file URLs for developer reference
1219
+ };
1220
+ emitEvent('PARSE_SUCCESS', metadata);
1221
+ setTimeout(() => {
1222
+ onSuccess(response, metadata);
1223
+ handleClose();
1224
+ }, 500);
1225
+ }
1226
+ catch (error) {
1227
+ setIsProcessing(false);
1228
+ setProgress(0);
1229
+ const casError = {
1230
+ code: 'PARSE_ERROR',
1231
+ message: error.response?.data?.msg || error.message || 'Failed to parse CDSL statement',
1232
+ details: { ...error.response?.data, files },
1233
+ };
1234
+ emitEvent('PARSE_ERROR', { error: casError });
1235
+ onError?.(casError);
1236
+ }
1237
+ };
1238
+ const renderContent = () => {
1239
+ // Show error with retry option - don't crash the widget
1240
+ if (lastError) {
1241
+ return (React.createElement("div", { style: { textAlign: 'center', padding: '20px 0' } },
1242
+ React.createElement("div", { style: {
1243
+ width: 56,
1244
+ height: 56,
1245
+ backgroundColor: '#fef2f2',
1246
+ borderRadius: '50%',
1247
+ display: 'flex',
1248
+ alignItems: 'center',
1249
+ justifyContent: 'center',
1250
+ margin: '0 auto 16px',
1251
+ } },
1252
+ React.createElement("svg", { width: "28", height: "28", viewBox: "0 0 24 24", fill: "none", stroke: "#dc2626", strokeWidth: "2" },
1253
+ React.createElement("circle", { cx: "12", cy: "12", r: "10" }),
1254
+ React.createElement("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
1255
+ React.createElement("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" }))),
1256
+ React.createElement("h3", { style: { fontSize: 18, fontWeight: 600, color: '#111827', margin: '0 0 8px' } }, "Something went wrong"),
1257
+ React.createElement("p", { style: { fontSize: 14, color: '#6b7280', margin: '0 0 20px', lineHeight: 1.5 } }, lastError.message),
1258
+ React.createElement("div", { style: { display: 'flex', gap: 12, justifyContent: 'center' } },
1259
+ React.createElement("button", { onClick: handleRetry, style: {
1260
+ padding: '12px 24px',
1261
+ fontSize: 14,
1262
+ fontWeight: 600,
1263
+ backgroundColor: '#2563eb',
1264
+ color: '#fff',
1265
+ border: 'none',
1266
+ borderRadius: 8,
1267
+ cursor: 'pointer',
1268
+ } }, "Try Again"),
1269
+ React.createElement("button", { onClick: handleClose, style: {
1270
+ padding: '12px 24px',
1271
+ fontSize: 14,
1272
+ fontWeight: 500,
1273
+ backgroundColor: '#f3f4f6',
1274
+ color: '#374151',
1275
+ border: 'none',
1276
+ borderRadius: 8,
1277
+ cursor: 'pointer',
1278
+ } }, "Close"))));
1279
+ }
1280
+ if (mode === 'FETCH' && config.enableGenerator) {
1281
+ return (React.createElement(FetchMode, { prefill: config.prefill, onSubmit: handleFetchSubmit, onBack: () => handleModeSwitch('LOCATE'), isLoading: fetchLoading }));
1282
+ }
1283
+ if (mode === 'CDSL_FETCH' && config.enableCdslFetch) {
1284
+ return (React.createElement(CDSLFetchMode, { prefill: config.prefill, onRequestOtp: handleCdslRequestOtp, onVerifyOtp: handleCdslVerifyOtp, onBack: () => handleModeSwitch('LOCATE') }));
1285
+ }
1286
+ if (mode === 'UPLOAD') {
1287
+ const currentPortal = CAS_PORTALS[selectedType];
1288
+ return (React.createElement("div", null,
1289
+ React.createElement(UploadZone, { onFileSelect: handleFileProcess, isProcessing: isProcessing, progress: progress }),
1290
+ showShortcuts && (React.createElement("div", { style: { marginTop: 16 } },
1291
+ React.createElement(ShortcutLinks, { selectedType: selectedType, onSearchClick: (provider) => emitEvent('SEARCH_CLICKED', { provider }) }))),
1292
+ showPortalLinks && (React.createElement("div", { style: { marginTop: 16, textAlign: 'center' } },
1293
+ React.createElement("a", { href: currentPortal.url, target: "_blank", rel: "noopener noreferrer", onClick: () => emitEvent('PORTAL_CLICKED', { portal: selectedType }), style: { textDecoration: 'none', color: '#6b7280', fontSize: 13 } },
1294
+ "Don't have your statement? ",
1295
+ React.createElement("span", { style: { color: '#2563eb' } },
1296
+ "Download from ",
1297
+ currentPortal.name))))));
1298
+ }
1299
+ // MF_OPTIONS mode - show upload or KFintech eCAS options
1300
+ if (mode === 'MF_OPTIONS') {
1301
+ return (React.createElement("div", null,
1302
+ React.createElement("div", { style: { textAlign: 'center', marginBottom: 20 } },
1303
+ React.createElement("h3", { style: { fontSize: 18, fontWeight: 600, color: '#111827', margin: '0 0 8px' } }, "Mutual Funds"),
1304
+ React.createElement("p", { style: { fontSize: 14, color: '#6b7280', margin: 0 } }, "CAMS & KFintech consolidated statement")),
1305
+ React.createElement("div", { style: { display: 'flex', flexDirection: 'column', gap: 12 } },
1306
+ React.createElement("button", { onClick: () => handleModeSwitch('UPLOAD'), style: {
1307
+ width: '100%',
1308
+ padding: '16px 20px',
1309
+ fontSize: 15,
1310
+ fontWeight: 600,
1311
+ backgroundColor: '#2563eb',
1312
+ color: '#fff',
1313
+ border: 'none',
1314
+ borderRadius: 12,
1315
+ cursor: 'pointer',
1316
+ textAlign: 'left',
1317
+ } },
1318
+ React.createElement("span", { style: { display: 'block' } }, "Upload Statement"),
1319
+ React.createElement("span", { style: { fontSize: 12, fontWeight: 400, opacity: 0.85 } }, "I have my CAS PDF file")),
1320
+ config.enableGenerator && (React.createElement("button", { onClick: () => handleModeSwitch('FETCH'), style: {
1321
+ width: '100%',
1322
+ padding: '16px 20px',
1323
+ fontSize: 15,
1324
+ fontWeight: 600,
1325
+ backgroundColor: '#fff',
1326
+ color: '#166534',
1327
+ border: '2px solid #bbf7d0',
1328
+ borderRadius: 12,
1329
+ cursor: 'pointer',
1330
+ textAlign: 'left',
1331
+ } },
1332
+ React.createElement("span", { style: { display: 'block' } }, "Request via Email"),
1333
+ React.createElement("span", { style: { fontSize: 12, fontWeight: 400, color: '#6b7280' } }, "Get CAS from KFintech"))))));
1334
+ }
1335
+ // DEMAT_OPTIONS mode - show upload or CDSL fetch options (for CDSL only)
1336
+ if (mode === 'DEMAT_OPTIONS') {
1337
+ return (React.createElement("div", null,
1338
+ React.createElement("div", { style: { textAlign: 'center', marginBottom: 20 } },
1339
+ React.createElement("h3", { style: { fontSize: 18, fontWeight: 600, color: '#111827', margin: '0 0 8px' } }, selectedBroker ? `${selectedBroker} (CDSL)` : 'CDSL Demat'),
1340
+ React.createElement("p", { style: { fontSize: 14, color: '#6b7280', margin: 0 } }, "How would you like to import?")),
1341
+ React.createElement("div", { style: { display: 'flex', flexDirection: 'column', gap: 12 } },
1342
+ React.createElement("button", { onClick: () => handleModeSwitch('UPLOAD'), style: {
1343
+ width: '100%',
1344
+ padding: '16px 20px',
1345
+ fontSize: 15,
1346
+ fontWeight: 600,
1347
+ backgroundColor: '#2563eb',
1348
+ color: '#fff',
1349
+ border: 'none',
1350
+ borderRadius: 12,
1351
+ cursor: 'pointer',
1352
+ textAlign: 'left',
1353
+ } },
1354
+ React.createElement("span", { style: { display: 'block' } }, "Upload Statement"),
1355
+ React.createElement("span", { style: { fontSize: 12, fontWeight: 400, opacity: 0.85 } }, "I have my CDSL CAS PDF file")),
1356
+ React.createElement("button", { onClick: () => handleModeSwitch('CDSL_FETCH'), style: {
1357
+ width: '100%',
1358
+ padding: '16px 20px',
1359
+ fontSize: 15,
1360
+ fontWeight: 600,
1361
+ backgroundColor: '#fff',
1362
+ color: '#1e40af',
1363
+ border: '2px solid #bfdbfe',
1364
+ borderRadius: 12,
1365
+ cursor: 'pointer',
1366
+ textAlign: 'left',
1367
+ } },
1368
+ React.createElement("span", { style: { display: 'block' } }, "Fetch from CDSL"),
1369
+ React.createElement("span", { style: { fontSize: 12, fontWeight: 400, color: '#6b7280' } }, "OTP verification required")))));
1370
+ }
1371
+ // SELECT_BROKER mode - for Stocks/Bonds/ETFs
1372
+ if (mode === 'SELECT_BROKER') {
1373
+ const showManualSelection = selectedBroker === 'Other';
1374
+ if (showManualSelection) {
1375
+ // Show CDSL/NSDL manual selection
1376
+ return (React.createElement("div", null,
1377
+ React.createElement("div", { style: { textAlign: 'center', marginBottom: 20 } },
1378
+ React.createElement("h3", { style: { fontSize: 18, fontWeight: 600, color: '#111827', margin: '0 0 8px' } }, "Select Your Depository"),
1379
+ React.createElement("p", { style: { fontSize: 14, color: '#6b7280', margin: 0 } }, "Check your demat account statement to find your depository")),
1380
+ React.createElement("div", { style: { display: 'flex', flexDirection: 'column', gap: 12 } },
1381
+ React.createElement("button", { onClick: () => handleManualDepositorySelect('CDSL'), style: {
1382
+ padding: '16px 20px',
1383
+ fontSize: 15,
1384
+ fontWeight: 600,
1385
+ backgroundColor: '#fff',
1386
+ color: '#111827',
1387
+ border: '2px solid #e5e7eb',
1388
+ borderRadius: 12,
1389
+ cursor: 'pointer',
1390
+ textAlign: 'left',
1391
+ } },
1392
+ React.createElement("span", { style: { display: 'block' } }, "CDSL"),
1393
+ React.createElement("span", { style: { fontSize: 12, fontWeight: 400, color: '#6b7280' } }, "Central Depository Services Limited")),
1394
+ React.createElement("button", { onClick: () => handleManualDepositorySelect('NSDL'), style: {
1395
+ padding: '16px 20px',
1396
+ fontSize: 15,
1397
+ fontWeight: 600,
1398
+ backgroundColor: '#fff',
1399
+ color: '#111827',
1400
+ border: '2px solid #e5e7eb',
1401
+ borderRadius: 12,
1402
+ cursor: 'pointer',
1403
+ textAlign: 'left',
1404
+ } },
1405
+ React.createElement("span", { style: { display: 'block' } }, "NSDL"),
1406
+ React.createElement("span", { style: { fontSize: 12, fontWeight: 400, color: '#6b7280' } }, "National Securities Depository Limited")))));
1407
+ }
1408
+ // Show broker list - use config.brokers if provided, otherwise default
1409
+ const brokerList = config.brokers || DEFAULT_BROKERS;
1410
+ return (React.createElement("div", null,
1411
+ React.createElement("div", { style: { textAlign: 'center', marginBottom: 20 } },
1412
+ React.createElement("h3", { style: { fontSize: 18, fontWeight: 600, color: '#111827', margin: '0 0 8px' } }, "Select Your Broker"),
1413
+ React.createElement("p", { style: { fontSize: 14, color: '#6b7280', margin: 0 } }, "We'll automatically detect your depository")),
1414
+ React.createElement("div", { style: {
1415
+ display: 'grid',
1416
+ gridTemplateColumns: 'repeat(2, 1fr)',
1417
+ gap: 10,
1418
+ maxHeight: 320,
1419
+ overflowY: 'auto',
1420
+ paddingRight: 4,
1421
+ } }, brokerList.map((broker) => (React.createElement("button", { key: broker.name, onClick: () => handleBrokerSelect(broker), style: {
1422
+ padding: '10px 12px',
1423
+ fontSize: 13,
1424
+ fontWeight: 500,
1425
+ backgroundColor: '#fff',
1426
+ color: '#111827',
1427
+ border: '1.5px solid #e5e7eb',
1428
+ borderRadius: 10,
1429
+ cursor: 'pointer',
1430
+ textAlign: 'left',
1431
+ transition: 'all 0.15s',
1432
+ display: 'flex',
1433
+ alignItems: 'center',
1434
+ gap: 10,
1435
+ }, onMouseOver: (e) => {
1436
+ e.currentTarget.style.borderColor = '#2563eb';
1437
+ e.currentTarget.style.backgroundColor = '#f0f7ff';
1438
+ }, onMouseOut: (e) => {
1439
+ e.currentTarget.style.borderColor = '#e5e7eb';
1440
+ e.currentTarget.style.backgroundColor = '#fff';
1441
+ } },
1442
+ broker.logo && (React.createElement("img", { src: broker.logo, alt: broker.name, style: { width: 24, height: 24, objectFit: 'contain', flexShrink: 0 } })),
1443
+ React.createElement("div", { style: { minWidth: 0 } },
1444
+ React.createElement("span", { style: { display: 'block', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, broker.name),
1445
+ React.createElement("span", { style: { fontSize: 10, color: '#9ca3af' } }, broker.depository)))))),
1446
+ React.createElement("button", { onClick: () => {
1447
+ setSelectedBroker('Other');
1448
+ }, style: {
1449
+ width: '100%',
1450
+ marginTop: 12,
1451
+ padding: '12px',
1452
+ fontSize: 14,
1453
+ fontWeight: 500,
1454
+ backgroundColor: '#f9fafb',
1455
+ color: '#6b7280',
1456
+ border: '1.5px dashed #d1d5db',
1457
+ borderRadius: 10,
1458
+ cursor: 'pointer',
1459
+ } }, "Other broker / I don't know")));
1460
+ }
1461
+ // LOCATE mode (default)
1462
+ const logoUrl = config.logoUrl || DEFAULT_LOGO_URL;
1463
+ const title = config.title || 'Import Your Investments';
1464
+ const subtitle = config.subtitle || 'Mutual Funds, Stocks, Bonds — all in one place';
1465
+ // Check if MF and/or Demat are allowed
1466
+ const allowMutualFunds = allowedTypes.includes('CAMS_KFINTECH');
1467
+ const allowDemat = allowedTypes.includes('CDSL') || allowedTypes.includes('NSDL');
1468
+ return (React.createElement("div", null,
1469
+ React.createElement("div", { style: { textAlign: 'center', marginBottom: 24 } },
1470
+ React.createElement("img", { src: logoUrl, alt: "Logo", style: { width: 56, height: 56, marginBottom: 16, objectFit: 'contain' } }),
1471
+ React.createElement("h2", { style: { fontSize: 22, fontWeight: 700, margin: 0, color: '#111827', letterSpacing: '-0.02em' } }, title),
1472
+ React.createElement("p", { style: { color: '#6b7280', marginTop: 8, fontSize: 14, lineHeight: 1.5 } }, subtitle)),
1473
+ React.createElement("div", { style: { display: 'flex', flexDirection: 'column', gap: 12 } },
1474
+ allowMutualFunds && (React.createElement("button", { onClick: () => {
1475
+ setSelectedType('CAMS_KFINTECH');
1476
+ setSelectedBroker(null);
1477
+ // If generator is enabled, show options; otherwise go directly to upload
1478
+ if (config.enableGenerator) {
1479
+ handleModeSwitch('MF_OPTIONS');
1480
+ }
1481
+ else {
1482
+ handleModeSwitch('UPLOAD');
1483
+ }
1484
+ }, style: {
1485
+ width: '100%',
1486
+ padding: '18px 20px',
1487
+ fontSize: 16,
1488
+ fontWeight: 600,
1489
+ backgroundColor: '#2563eb',
1490
+ color: '#fff',
1491
+ border: 'none',
1492
+ borderRadius: 12,
1493
+ cursor: 'pointer',
1494
+ textAlign: 'left',
1495
+ transition: 'all 0.2s',
1496
+ boxShadow: '0 4px 14px rgba(37, 99, 235, 0.25)',
1497
+ }, onMouseOver: (e) => {
1498
+ e.currentTarget.style.backgroundColor = '#1d4ed8';
1499
+ e.currentTarget.style.transform = 'translateY(-1px)';
1500
+ }, onMouseOut: (e) => {
1501
+ e.currentTarget.style.backgroundColor = '#2563eb';
1502
+ e.currentTarget.style.transform = 'translateY(0)';
1503
+ } },
1504
+ React.createElement("span", { style: { display: 'block' } }, "Mutual Funds"),
1505
+ React.createElement("span", { style: { fontSize: 12, fontWeight: 400, opacity: 0.85 } }, "CAMS & KFintech CAS"))),
1506
+ allowDemat && (React.createElement("button", { onClick: () => {
1507
+ setSelectedBroker(null);
1508
+ handleModeSwitch('SELECT_BROKER');
1509
+ }, style: {
1510
+ width: '100%',
1511
+ padding: '18px 20px',
1512
+ fontSize: 16,
1513
+ fontWeight: 600,
1514
+ backgroundColor: '#fff',
1515
+ color: '#111827',
1516
+ border: '2px solid #e5e7eb',
1517
+ borderRadius: 12,
1518
+ cursor: 'pointer',
1519
+ textAlign: 'left',
1520
+ transition: 'all 0.2s',
1521
+ }, onMouseOver: (e) => {
1522
+ e.currentTarget.style.borderColor = '#2563eb';
1523
+ e.currentTarget.style.backgroundColor = '#f0f7ff';
1524
+ }, onMouseOut: (e) => {
1525
+ e.currentTarget.style.borderColor = '#e5e7eb';
1526
+ e.currentTarget.style.backgroundColor = '#fff';
1527
+ } },
1528
+ React.createElement("span", { style: { display: 'block' } }, "Stocks, Bonds, ETFs"),
1529
+ React.createElement("span", { style: { fontSize: 12, fontWeight: 400, color: '#6b7280' } }, "Demat \u2014 CDSL & NSDL"))))));
1530
+ };
1531
+ // Determine if we're in a sub-screen that should show back button
1532
+ const isSubScreen = mode !== 'LOCATE';
1533
+ const isSandbox = apiKey === 'sandbox-with-json-responses';
1534
+ const handleBack = () => {
1535
+ // Smart back navigation
1536
+ if (mode === 'UPLOAD') {
1537
+ if (selectedType === 'CAMS_KFINTECH' && config.enableGenerator) {
1538
+ setMode('MF_OPTIONS');
1539
+ }
1540
+ else if (selectedType === 'CDSL' && config.enableCdslFetch) {
1541
+ setMode('DEMAT_OPTIONS');
1542
+ }
1543
+ else if (selectedType === 'CDSL' || selectedType === 'NSDL') {
1544
+ setMode('SELECT_BROKER');
1545
+ }
1546
+ else {
1547
+ setMode('LOCATE');
1548
+ }
1549
+ }
1550
+ else if (mode === 'FETCH') {
1551
+ setMode('MF_OPTIONS');
1552
+ }
1553
+ else if (mode === 'CDSL_FETCH') {
1554
+ setMode('DEMAT_OPTIONS');
1555
+ }
1556
+ else if (mode === 'MF_OPTIONS' || mode === 'DEMAT_OPTIONS') {
1557
+ if (selectedType === 'CDSL' || selectedType === 'NSDL') {
1558
+ setMode('SELECT_BROKER');
1559
+ }
1560
+ else {
1561
+ setMode('LOCATE');
1562
+ }
1563
+ }
1564
+ else if (mode === 'SELECT_BROKER') {
1565
+ setMode('LOCATE');
1566
+ }
1567
+ else {
1568
+ setMode('LOCATE');
1569
+ }
1570
+ emitEvent('MODE_SWITCHED', { from: mode, to: 'LOCATE' });
1571
+ };
1572
+ return (React.createElement(Modal, { isOpen: isOpen, onClose: handleClose, onBack: handleBack, showBackButton: isSubScreen, isSandbox: isSandbox, width: 420 },
1573
+ React.createElement("div", { style: { padding: '28px 24px 20px' } },
1574
+ renderContent(),
1575
+ React.createElement("div", { style: {
1576
+ marginTop: 24,
1577
+ paddingTop: 16,
1578
+ borderTop: '1px solid #f3f4f6',
1579
+ textAlign: 'center',
1580
+ } },
1581
+ React.createElement("div", { style: { fontSize: 11, color: '#9ca3af' } }, "Encrypted \u2022 India-hosted")))));
1582
+ };
1583
+
1584
+ const PortfolioConnect = ({ accessToken, apiKey, // deprecated, use accessToken
1585
+ apiBaseUrl, config, onSuccess, onError, onExit, onEvent, children, }) => {
1586
+ const [isOpen, setIsOpen] = React.useState(false);
1587
+ // Support both accessToken (new) and apiKey (deprecated) - both work with x-api-key header
1588
+ const token = accessToken || apiKey;
1589
+ const open = React.useCallback(() => {
1590
+ setIsOpen(true);
1591
+ }, []);
1592
+ const handleClose = React.useCallback(() => {
1593
+ setIsOpen(false);
1594
+ onExit?.();
1595
+ }, [onExit]);
1596
+ const renderProps = {
1597
+ open,
1598
+ isReady: !!token,
1599
+ isOpen,
1600
+ };
1601
+ return (React.createElement(React.Fragment, null,
1602
+ children(renderProps),
1603
+ React.createElement(PortfolioConnectWidget, { isOpen: isOpen, onClose: handleClose, apiKey: token || '', apiBaseUrl: apiBaseUrl, config: config, onSuccess: onSuccess, onError: onError, onEvent: onEvent })));
1604
+ };
1605
+ // Backward compatibility alias
1606
+ const CASConnect = PortfolioConnect;
1607
+
1608
+ // Track active widget instance for cleanup
1609
+ let activeContainer = null;
1610
+ let activeRoot = null;
1611
+ /**
1612
+ * Cleanup any existing widget instance
1613
+ */
1614
+ function cleanup() {
1615
+ if (activeRoot) {
1616
+ activeRoot.unmount();
1617
+ activeRoot = null;
1618
+ }
1619
+ if (activeContainer && activeContainer.parentNode) {
1620
+ activeContainer.parentNode.removeChild(activeContainer);
1621
+ activeContainer = null;
1622
+ }
1623
+ }
1624
+ /**
1625
+ * Opens the Portfolio Connect widget and returns a promise that resolves with parsed data.
1626
+ *
1627
+ * This is the recommended API for non-React applications (Vanilla JS, Angular, Vue, etc.)
1628
+ *
1629
+ * @example
1630
+ * ```javascript
1631
+ * // Vanilla JS
1632
+ * document.getElementById('btn').onclick = async () => {
1633
+ * try {
1634
+ * const { data, metadata } = await PortfolioConnect.open({
1635
+ * apiKey: 'your_api_key',
1636
+ * config: { enableCdslFetch: true }
1637
+ * });
1638
+ * console.log('Portfolio data:', data);
1639
+ * } catch (error) {
1640
+ * if (error.message === 'Widget closed by user') {
1641
+ * console.log('User cancelled');
1642
+ * } else {
1643
+ * console.error('Error:', error);
1644
+ * }
1645
+ * }
1646
+ * };
1647
+ * ```
1648
+ *
1649
+ * @param config - Configuration options
1650
+ * @returns Promise that resolves with { data, metadata } on success, rejects on error/close
1651
+ */
1652
+ function open(config) {
1653
+ return new Promise((resolve, reject) => {
1654
+ // Cleanup any existing instance
1655
+ cleanup();
1656
+ // Create container
1657
+ activeContainer = document.createElement('div');
1658
+ activeContainer.id = 'portfolio-connect-root';
1659
+ activeContainer.setAttribute('data-portfolio-connect', 'true');
1660
+ document.body.appendChild(activeContainer);
1661
+ // Create React root
1662
+ activeRoot = ReactDOM.createRoot(activeContainer);
1663
+ // Notify open
1664
+ config.onOpen?.();
1665
+ // Render widget
1666
+ activeRoot.render(React.createElement(PortfolioConnectWidget, {
1667
+ isOpen: true,
1668
+ apiKey: config.accessToken || config.apiKey || '',
1669
+ config: config.config,
1670
+ onSuccess: (data, metadata) => {
1671
+ cleanup();
1672
+ resolve({ data, metadata });
1673
+ },
1674
+ onClose: () => {
1675
+ cleanup();
1676
+ reject(new Error('Widget closed by user'));
1677
+ },
1678
+ onError: (error) => {
1679
+ cleanup();
1680
+ reject(error);
1681
+ },
1682
+ onEvent: config.onEvent,
1683
+ }));
1684
+ });
1685
+ }
1686
+ /**
1687
+ * Creates a widget handle for manual control.
1688
+ * Useful when you need to open/close the widget multiple times.
1689
+ *
1690
+ * @example
1691
+ * ```javascript
1692
+ * const widget = PortfolioConnect.create({
1693
+ * apiKey: 'your_api_key',
1694
+ * onSuccess: (data) => console.log(data),
1695
+ * });
1696
+ *
1697
+ * document.getElementById('open-btn').onclick = () => widget.open();
1698
+ * document.getElementById('close-btn').onclick = () => widget.destroy();
1699
+ * ```
1700
+ *
1701
+ * @param config - Configuration options including callbacks
1702
+ * @returns Handle with open() and destroy() methods
1703
+ */
1704
+ function create(config) {
1705
+ let container = null;
1706
+ let root = null;
1707
+ function render(open) {
1708
+ if (!container) {
1709
+ container = document.createElement('div');
1710
+ container.id = 'portfolio-connect-root';
1711
+ container.setAttribute('data-portfolio-connect', 'true');
1712
+ document.body.appendChild(container);
1713
+ root = ReactDOM.createRoot(container);
1714
+ }
1715
+ root.render(React.createElement(PortfolioConnectWidget, {
1716
+ isOpen: open,
1717
+ apiKey: config.accessToken || config.apiKey || '',
1718
+ config: config.config,
1719
+ onSuccess: (data, metadata) => {
1720
+ render(false);
1721
+ config.onSuccess(data, metadata);
1722
+ },
1723
+ onClose: () => {
1724
+ render(false);
1725
+ config.onClose?.();
1726
+ },
1727
+ onError: (error) => {
1728
+ render(false);
1729
+ config.onError?.(error);
1730
+ },
1731
+ onEvent: config.onEvent,
1732
+ }));
1733
+ }
1734
+ return {
1735
+ open: () => {
1736
+ config.onOpen?.();
1737
+ render(true);
1738
+ },
1739
+ destroy: () => {
1740
+ if (root) {
1741
+ root.unmount();
1742
+ root = null;
1743
+ }
1744
+ if (container && container.parentNode) {
1745
+ container.parentNode.removeChild(container);
1746
+ container = null;
1747
+ }
1748
+ },
1749
+ };
1750
+ }
1751
+
1752
+ // Main exports - React components
1753
+ // SDK Version
1754
+ const VERSION = '1.1.0';
1755
+ const SDK_NAME = 'Portfolio Connect';
1756
+
1757
+ exports.CASConnect = CASConnect;
1758
+ exports.CASConnectWidget = PortfolioConnectWidget;
1759
+ exports.PortfolioConnect = PortfolioConnect;
1760
+ exports.PortfolioConnectWidget = PortfolioConnectWidget;
1761
+ exports.SDK_NAME = SDK_NAME;
1762
+ exports.VERSION = VERSION;
1763
+ exports.create = create;
1764
+ exports.isAccessToken = isAccessToken;
1765
+ exports.open = open;
1766
+ exports.requestAccessToken = requestAccessToken;
1767
+
1768
+ }));
1769
+ //# sourceMappingURL=portfolio-connect.umd.js.map