@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.
- package/LICENSE +21 -0
- package/README.md +263 -0
- package/dist/PortfolioConnect.d.ts +5 -0
- package/dist/components/CDSLFetchMode.d.ts +13 -0
- package/dist/components/FetchMode.d.ts +12 -0
- package/dist/components/Modal.d.ts +12 -0
- package/dist/components/PortfolioConnectWidget.d.ts +14 -0
- package/dist/components/ShortcutLinks.d.ts +8 -0
- package/dist/components/UploadZone.d.ts +8 -0
- package/dist/imperative.d.ts +87 -0
- package/dist/index.cjs.js +1766 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.esm.js +1755 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/portfolio-connect.standalone.min.js +2 -0
- package/dist/portfolio-connect.standalone.min.js.map +1 -0
- package/dist/portfolio-connect.umd.js +1769 -0
- package/dist/portfolio-connect.umd.js.map +1 -0
- package/dist/portfolio-connect.umd.min.js +2 -0
- package/dist/portfolio-connect.umd.min.js.map +1 -0
- package/dist/types/index.d.ts +86 -0
- package/dist/utils/api.d.ts +57 -0
- package/package.json +84 -0
|
@@ -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
|