@finspringinnovations/fdsdk 0.0.5 → 0.0.7

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.
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  import { useCallback, useRef } from 'react';
17
+ import { Platform } from 'react-native';
17
18
  import { parseSSEBuffer } from '../utils/sseParser';
18
19
  import { getEnvironmentData, getUserInfoForAPI } from '../config/appDataConfig';
19
20
  import { getApiConfig, getSecureHeaders } from '../config/apiConfig';
@@ -40,6 +41,10 @@ export interface PaymentSSEExtraHeaders {
40
41
  export function usePaymentSSE() {
41
42
  // Store the XMLHttpRequest so we can abort it later
42
43
  const xhrRef = useRef<XMLHttpRequest | null>(null);
44
+ const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
45
+ const connectWatchdogTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
46
+ const stopRequestedRef = useRef<boolean>(false);
47
+ const connectionIdRef = useRef<number>(0);
43
48
  // Track how much of responseText we've already parsed
44
49
  const lastIndexRef = useRef<number>(0);
45
50
  // SSE text buffer (persists across onprogress calls)
@@ -47,11 +52,23 @@ export function usePaymentSSE() {
47
52
 
48
53
  const start = useCallback(
49
54
  (applicationId: string, callbacks: UsePaymentSSECallbacks, extraHeaders?: PaymentSSEExtraHeaders) => {
55
+ connectionIdRef.current += 1;
56
+ const connectionId = connectionIdRef.current;
57
+ stopRequestedRef.current = false;
58
+
50
59
  // Clean up any existing connection first
51
60
  if (xhrRef.current) {
52
61
  xhrRef.current.abort();
53
62
  xhrRef.current = null;
54
63
  }
64
+ if (reconnectTimerRef.current) {
65
+ clearTimeout(reconnectTimerRef.current);
66
+ reconnectTimerRef.current = null;
67
+ }
68
+ if (connectWatchdogTimerRef.current) {
69
+ clearTimeout(connectWatchdogTimerRef.current);
70
+ connectWatchdogTimerRef.current = null;
71
+ }
55
72
 
56
73
  // Reset parsing state
57
74
  lastIndexRef.current = 0;
@@ -63,177 +80,307 @@ export function usePaymentSSE() {
63
80
  const baseUrl = (envData?.apiBaseUrl || apiConfig?.baseUrl || '').replace(/\/$/, '');
64
81
  const url = baseUrl ? `${baseUrl}/events?applicationId=${encodeURIComponent(applicationId)}` : '';
65
82
 
83
+ const minimalLogs = envData?.sseMinimalLogs === true;
84
+ const verboseSSE = envData?.enableVerboseSSELogging !== false;
85
+ const maxReconnectAttempts = Number(envData?.sseReconnectAttempts ?? 4);
86
+ const reconnectDelayMs = Number(envData?.sseReconnectDelayMs ?? 2000);
87
+ const connectWatchdogMs = Number(envData?.sseConnectTimeoutMs ?? 15000);
88
+ const rnVersion = (() => {
89
+ const v = (Platform as any)?.constants?.reactNativeVersion;
90
+ return v ? `${v.major}.${v.minor}.${v.patch}` : 'unknown';
91
+ })();
92
+ const jsEngine = (global as any)?.HermesInternal ? 'hermes' : 'jsc/other';
93
+ let reconnectAttempts = 0;
94
+ let connected = false;
95
+
66
96
  if (!url) {
67
- if (__DEV__) console.warn('[usePaymentSSE] No base URL, skipping SSE');
97
+ if (__DEV__ && verboseSSE) console.warn('[usePaymentSSE] No base URL, skipping SSE');
68
98
  return;
69
99
  }
70
100
 
71
- // 2. Create XMLHttpRequest
72
- const xhr = new XMLHttpRequest();
73
- xhrRef.current = xhr;
101
+ const scheduleReconnect = (reason: string) => {
102
+ if (stopRequestedRef.current || connectionIdRef.current !== connectionId) return;
103
+ if (connectWatchdogTimerRef.current) {
104
+ clearTimeout(connectWatchdogTimerRef.current);
105
+ connectWatchdogTimerRef.current = null;
106
+ }
107
+ if (reconnectAttempts >= maxReconnectAttempts) {
108
+ callbacks.onError?.(new Error(reason));
109
+ return;
110
+ }
74
111
 
75
- xhr.open('GET', url);
112
+ reconnectAttempts += 1;
113
+ const delay = reconnectDelayMs * reconnectAttempts;
76
114
 
77
- // 3. Set headers (same headers your baseApi interceptor uses)
78
- const apiKey = envData?.apiKey || apiConfig?.headers?.['X-API-Key'] || '';
79
- if (apiKey) {
80
- xhr.setRequestHeader('x-api-key', apiKey);
81
- }
82
-
83
- // Add secure headers (Content-Type, User-Agent, etc.)
84
- const secureHeaders = getSecureHeaders();
85
- Object.entries(secureHeaders).forEach(([key, value]) => {
86
- if (key.toLowerCase() !== 'content-type') {
87
- xhr.setRequestHeader(key, value);
115
+ if (__DEV__ && !minimalLogs) {
116
+ console.warn(`[usePaymentSSE] Reconnecting (${reconnectAttempts}/${maxReconnectAttempts}) in ${delay}ms`);
117
+ console.log('[usePaymentSSE][runtime]', {
118
+ reason,
119
+ platform: Platform.OS,
120
+ osVersion: String(Platform.Version),
121
+ rnVersion,
122
+ jsEngine,
123
+ applicationId,
124
+ });
88
125
  }
89
- });
90
-
91
- // SSE-specific headers
92
- xhr.setRequestHeader('Accept', 'text/event-stream');
93
- xhr.setRequestHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
94
- xhr.setRequestHeader('Pragma', 'no-cache');
95
- xhr.setRequestHeader('Expires', '0');
96
-
97
- // Add userreferenceid header (required for authentication) - both cases
98
- try {
99
- const userInfo = getUserInfoForAPI();
100
- const userRefId = userInfo?.userReferenceId || userInfo?.id;
101
- if (userRefId) {
102
- xhr.setRequestHeader('userreferenceid', userRefId);
103
- xhr.setRequestHeader('userReferenceId', userRefId);
126
+
127
+ if (reconnectTimerRef.current) {
128
+ clearTimeout(reconnectTimerRef.current);
104
129
  }
105
- } catch {
106
- if (__DEV__) console.warn('[usePaymentSSE] Could not get userReferenceId');
107
- }
108
-
109
- // Add provider header (required for authentication)
110
- if (extraHeaders?.providerId) {
111
- xhr.setRequestHeader('provider', extraHeaders.providerId);
112
- }
113
-
114
- // Add onboarding headers - both camelCase and lowercase variants
115
- if (extraHeaders?.workflowInstanceId) {
116
- xhr.setRequestHeader('workflowInstanceId', extraHeaders.workflowInstanceId);
117
- }
118
- if (extraHeaders?.applicationId) {
119
- xhr.setRequestHeader('applicationId', extraHeaders.applicationId);
120
- xhr.setRequestHeader('applicationid', extraHeaders.applicationId);
121
- }
122
- if (extraHeaders?.entityid) {
123
- xhr.setRequestHeader('entityid', extraHeaders.entityid);
124
- xhr.setRequestHeader('entityId', extraHeaders.entityid);
125
- }
126
130
 
127
- // 4. Handle incoming data
128
- xhr.onprogress = () => {
129
- // responseText contains ALL text received so far
130
- // Only parse the NEW text since last call
131
- const newText = xhr.responseText.slice(lastIndexRef.current);
132
- lastIndexRef.current = xhr.responseText.length;
131
+ reconnectTimerRef.current = setTimeout(() => {
132
+ if (stopRequestedRef.current || connectionIdRef.current !== connectionId) return;
133
+ openConnection();
134
+ }, delay);
135
+ };
133
136
 
134
- if (!newText) return;
137
+ const openConnection = () => {
138
+ if (stopRequestedRef.current || connectionIdRef.current !== connectionId) return;
139
+ connected = false;
135
140
 
136
- // Parse new text into SSE events
137
- const events = parseSSEBuffer(bufferRef.current, newText);
141
+ // 2. Create XMLHttpRequest
142
+ const xhr = new XMLHttpRequest();
143
+ xhrRef.current = xhr;
138
144
 
139
- // Handle each event (support encrypted payload like web)
140
- const handleEventData = async (rawData: string) => {
141
- try {
142
- const parsed = JSON.parse(rawData || '{}') as Record<string, unknown>;
143
- let payload = parsed;
144
- if (typeof parsed?.encryptedResponse === 'string' && parsed.encryptedResponse) {
145
+ xhr.open('GET', url);
146
+
147
+ // 3. Set headers (same headers your baseApi interceptor uses)
148
+ const apiKey = envData?.apiKey || apiConfig?.headers?.['X-API-Key'] || '';
149
+ if (apiKey) {
150
+ xhr.setRequestHeader('x-api-key', apiKey);
151
+ xhr.setRequestHeader('X-API-Key', apiKey);
152
+ }
153
+
154
+ // Add secure headers (Content-Type, User-Agent, etc.)
155
+ const secureHeaders = getSecureHeaders();
156
+ Object.entries(secureHeaders).forEach(([key, value]) => {
157
+ if (key.toLowerCase() !== 'content-type') {
158
+ xhr.setRequestHeader(key, value);
159
+ }
160
+ });
161
+
162
+ // SSE-specific headers
163
+ xhr.setRequestHeader('Accept', 'text/event-stream');
164
+ xhr.setRequestHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
165
+ xhr.setRequestHeader('Pragma', 'no-cache');
166
+ xhr.setRequestHeader('Expires', '0');
167
+
168
+ // Add userreferenceid header (required for authentication) - both cases
169
+ try {
170
+ const userInfo = getUserInfoForAPI();
171
+ const userRefId = userInfo?.userReferenceId || userInfo?.id;
172
+ if (userRefId) {
173
+ xhr.setRequestHeader('userreferenceid', userRefId);
174
+ xhr.setRequestHeader('userReferenceId', userRefId);
175
+ }
176
+ } catch {
177
+ if (__DEV__ && verboseSSE) console.warn('[usePaymentSSE] Could not get userReferenceId');
178
+ }
179
+
180
+ // Add provider header variants
181
+ if (extraHeaders?.providerId) {
182
+ xhr.setRequestHeader('provider', extraHeaders.providerId);
183
+ xhr.setRequestHeader('providerid', extraHeaders.providerId);
184
+ xhr.setRequestHeader('providerId', extraHeaders.providerId);
185
+ }
186
+
187
+ // Add onboarding headers - both camelCase and lowercase variants
188
+ if (extraHeaders?.workflowInstanceId) {
189
+ xhr.setRequestHeader('workflowInstanceId', extraHeaders.workflowInstanceId);
190
+ xhr.setRequestHeader('workflowinstanceid', extraHeaders.workflowInstanceId);
191
+ }
192
+ if (extraHeaders?.applicationId) {
193
+ xhr.setRequestHeader('applicationId', extraHeaders.applicationId);
194
+ xhr.setRequestHeader('applicationid', extraHeaders.applicationId);
195
+ }
196
+ if (extraHeaders?.entityid) {
197
+ xhr.setRequestHeader('entityid', extraHeaders.entityid);
198
+ xhr.setRequestHeader('entityId', extraHeaders.entityid);
199
+ }
200
+
201
+ const markConnected = (path: 'headers' | 'progress') => {
202
+ if (connected) return;
203
+ connected = true;
204
+ reconnectAttempts = 0;
205
+ if (connectWatchdogTimerRef.current) {
206
+ clearTimeout(connectWatchdogTimerRef.current);
207
+ connectWatchdogTimerRef.current = null;
208
+ }
209
+ callbacks.onConnected?.();
210
+ if (__DEV__ && !minimalLogs) {
211
+ console.log('[usePaymentSSE] Connected to SSE stream');
212
+ console.log('[usePaymentSSE][runtime]', {
213
+ path,
214
+ platform: Platform.OS,
215
+ osVersion: String(Platform.Version),
216
+ rnVersion,
217
+ jsEngine,
218
+ applicationId,
219
+ });
220
+ }
221
+ };
222
+
223
+ // 4. Handle incoming data
224
+ xhr.onprogress = () => {
225
+ if (xhr.responseText && xhr.responseText.length > 0) {
226
+ markConnected('progress');
227
+ }
228
+
229
+ // responseText contains ALL text received so far
230
+ // Only parse the NEW text since last call
231
+ const newText = xhr.responseText.slice(lastIndexRef.current);
232
+ lastIndexRef.current = xhr.responseText.length;
233
+
234
+ if (!newText) return;
235
+
236
+ // Parse new text into SSE events
237
+ const events = parseSSEBuffer(bufferRef.current, newText);
238
+
239
+ // Handle each event (support encrypted payload like web)
240
+ const handleEventData = async (rawData: string) => {
241
+ try {
242
+ const parsed = JSON.parse(rawData || '{}') as Record<string, unknown>;
243
+ let payload = parsed;
244
+ if (typeof parsed?.encryptedResponse === 'string' && parsed.encryptedResponse) {
245
+ try {
246
+ const config = getEncryptionConfig();
247
+ const decrypted = await decryptResponse(
248
+ { encryptedResponse: parsed.encryptedResponse },
249
+ config
250
+ );
251
+ payload = (decrypted || {}) as Record<string, unknown>;
252
+ } catch {
253
+ if (__DEV__ && verboseSSE) console.warn('[usePaymentSSE] Decrypt failed, using raw');
254
+ }
255
+ }
256
+ const payloadData = (payload?.data as Record<string, unknown> | undefined) || {};
257
+ const status = String(
258
+ payload?.paymentStatus ??
259
+ payload?.payment_status ??
260
+ payloadData?.paymentStatus ??
261
+ payloadData?.payment_status ??
262
+ ''
263
+ ).toUpperCase();
264
+ if (status) callbacks.onPaymentStatus(status, payload);
265
+ } catch (parseError) {
266
+ if (__DEV__ && verboseSSE) console.error('[usePaymentSSE] Failed to parse event data:', parseError);
267
+ }
268
+ };
269
+
270
+ for (const { eventType, data } of events) {
271
+ if (!data) continue;
272
+ const normalizedEventType = String(eventType || '').trim().toLowerCase();
273
+ if (normalizedEventType === 'fd_payment_status') {
274
+ handleEventData(data);
275
+ } else if (normalizedEventType === 'message') {
145
276
  try {
146
- const config = getEncryptionConfig();
147
- const decrypted = await decryptResponse(
148
- { encryptedResponse: parsed.encryptedResponse },
149
- config
150
- );
151
- payload = (decrypted || {}) as Record<string, unknown>;
277
+ const parsed = JSON.parse(data) as { event?: string; [k: string]: unknown };
278
+ const embeddedEvent = String(parsed?.event || '').trim().toLowerCase();
279
+ if (embeddedEvent === 'fd_payment_status') handleEventData(data);
152
280
  } catch {
153
- if (__DEV__) console.warn('[usePaymentSSE] Decrypt failed, using raw');
281
+ // ignore
154
282
  }
155
283
  }
156
- const status = String(payload?.paymentStatus ?? payload?.payment_status ?? '').toUpperCase();
157
- if (status) callbacks.onPaymentStatus(status, payload);
158
- } catch (parseError) {
159
- if (__DEV__) console.error('[usePaymentSSE] Failed to parse event data:', parseError);
160
284
  }
161
285
  };
162
286
 
163
- for (const { eventType, data } of events) {
164
- if (!data) continue;
165
- if (eventType === 'fd_payment_status') {
166
- handleEventData(data);
167
- } else if (eventType === 'message') {
168
- try {
169
- const parsed = JSON.parse(data) as { event?: string; [k: string]: unknown };
170
- if (parsed?.event === 'fd_payment_status') handleEventData(data);
171
- } catch {
172
- // ignore
287
+ // 5. Handle connection status
288
+ xhr.onreadystatechange = () => {
289
+ if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED || xhr.readyState === XMLHttpRequest.LOADING) {
290
+ if (xhr.status === 200) {
291
+ markConnected('headers');
292
+ } else if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED && xhr.status > 0) {
293
+ if (__DEV__) {
294
+ console.error('[usePaymentSSE] Bad status:', xhr.status);
295
+ }
296
+ scheduleReconnect(`SSE response status: ${xhr.status}`);
173
297
  }
174
298
  }
175
- }
176
- };
177
299
 
178
- // 5. Handle successful connection
179
- xhr.onreadystatechange = () => {
180
- if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
181
- if (xhr.status === 200) {
182
- callbacks.onConnected?.();
183
- if (__DEV__) {
184
- console.log('[usePaymentSSE] Connected to SSE stream');
300
+ if (xhr.readyState === XMLHttpRequest.DONE && !stopRequestedRef.current && connectionIdRef.current === connectionId) {
301
+ if (xhr.status !== 200) {
302
+ scheduleReconnect(`SSE closed with status: ${xhr.status}`);
185
303
  }
186
- } else {
187
- if (__DEV__) {
188
- console.error('[usePaymentSSE] Bad status:', xhr.status);
189
- }
190
- callbacks.onError?.(new Error(`SSE response status: ${xhr.status}`));
191
304
  }
192
- }
193
- };
305
+ };
194
306
 
195
- // 6. Handle errors
196
- xhr.onerror = () => {
197
- if (__DEV__) {
198
- console.error('[usePaymentSSE] Connection error');
307
+ // 6. Handle errors
308
+ xhr.onerror = () => {
309
+ if (__DEV__ && verboseSSE) {
310
+ console.error('[usePaymentSSE] Connection error');
311
+ }
312
+ scheduleReconnect('SSE connection failed');
313
+ };
314
+
315
+ // 7. Handle timeout
316
+ xhr.ontimeout = () => {
317
+ if (__DEV__) {
318
+ console.error('[usePaymentSSE] Connection timed out');
319
+ }
320
+ scheduleReconnect('SSE connection timed out');
321
+ };
322
+
323
+ // 8. Send the request (connection stays open)
324
+ xhr.send();
325
+
326
+ // If we never get connected callback/progress, restart connection.
327
+ if (connectWatchdogTimerRef.current) {
328
+ clearTimeout(connectWatchdogTimerRef.current);
199
329
  }
200
- callbacks.onError?.(new Error('SSE connection failed'));
201
- };
330
+ connectWatchdogTimerRef.current = setTimeout(() => {
331
+ if (connected || stopRequestedRef.current || connectionIdRef.current !== connectionId) return;
332
+ try {
333
+ xhr.abort();
334
+ } catch {
335
+ // ignore
336
+ }
337
+ scheduleReconnect('SSE connect watchdog timeout');
338
+ }, connectWatchdogMs);
202
339
 
203
- // 7. Handle timeout
204
- xhr.ontimeout = () => {
205
340
  if (__DEV__) {
206
- console.error('[usePaymentSSE] Connection timed out');
341
+ console.log('[usePaymentSSE] Starting SSE connection to:', url);
342
+ console.log('[usePaymentSSE][runtime]', {
343
+ phase: 'start',
344
+ platform: Platform.OS,
345
+ osVersion: String(Platform.Version),
346
+ rnVersion,
347
+ jsEngine,
348
+ applicationId,
349
+ });
350
+ console.log('[usePaymentSSE] Headers:', {
351
+ 'x-api-key': apiKey ? '***' : 'missing',
352
+ 'userreferenceid': (() => {
353
+ try {
354
+ const userInfo = getUserInfoForAPI();
355
+ return userInfo?.userReferenceId || userInfo?.id || 'missing';
356
+ } catch {
357
+ return 'missing';
358
+ }
359
+ })(),
360
+ 'provider': extraHeaders?.providerId || 'missing',
361
+ 'workflowInstanceId': extraHeaders?.workflowInstanceId || 'missing',
362
+ 'applicationId': extraHeaders?.applicationId || 'missing',
363
+ 'entityid': extraHeaders?.entityid || 'missing',
364
+ });
207
365
  }
208
- callbacks.onError?.(new Error('SSE connection timed out'));
209
366
  };
210
367
 
211
- // 8. Send the request (connection stays open)
212
- xhr.send();
213
-
214
- if (__DEV__) {
215
- console.log('[usePaymentSSE] Starting SSE connection to:', url);
216
- console.log('[usePaymentSSE] Headers:', {
217
- 'x-api-key': apiKey ? '***' : 'missing',
218
- 'userreferenceid': (() => {
219
- try {
220
- const userInfo = getUserInfoForAPI();
221
- return userInfo?.userReferenceId || userInfo?.id || 'missing';
222
- } catch {
223
- return 'missing';
224
- }
225
- })(),
226
- 'provider': extraHeaders?.providerId || 'missing',
227
- 'workflowInstanceId': extraHeaders?.workflowInstanceId || 'missing',
228
- 'applicationId': extraHeaders?.applicationId || 'missing',
229
- 'entityid': extraHeaders?.entityid || 'missing',
230
- });
231
- }
368
+ openConnection();
232
369
  },
233
370
  []
234
371
  );
235
372
 
236
373
  const stop = useCallback(() => {
374
+ stopRequestedRef.current = true;
375
+ connectionIdRef.current += 1;
376
+ if (reconnectTimerRef.current) {
377
+ clearTimeout(reconnectTimerRef.current);
378
+ reconnectTimerRef.current = null;
379
+ }
380
+ if (connectWatchdogTimerRef.current) {
381
+ clearTimeout(connectWatchdogTimerRef.current);
382
+ connectWatchdogTimerRef.current = null;
383
+ }
237
384
  if (xhrRef.current) {
238
385
  xhrRef.current.abort();
239
386
  xhrRef.current = null;
@@ -246,4 +393,4 @@ export function usePaymentSSE() {
246
393
  }, []);
247
394
 
248
395
  return { start, stop };
249
- }
396
+ }
@@ -757,7 +757,29 @@ const FDCalculator: React.FC<FDCalculatorProps> = ({ onGoBack, onExitSDK, onPanR
757
757
  ? data?.sdrCalc?.[0]
758
758
  : data?.fdrCalc?.[0];
759
759
  const maturityAmount = calcData?.maturityAmount || data?.maturityAmount || data?.totalAmount || data?.finalAmount || data?.amount;
760
- return maturityAmount ? `₹${maturityAmount.toLocaleString()}` : "";
760
+ return maturityAmount ? `₹${Number(maturityAmount).toLocaleString()}` : "";
761
+ })()}
762
+ intPayAmt={(() => {
763
+ const data = calculationResult?.data;
764
+ const investmentType = payoutValue === 'On Maturity' ? 'SDR' : 'FDR';
765
+ const calcData = investmentType === 'SDR'
766
+ ? data?.sdrCalc?.[0]
767
+ : data?.fdrCalc?.[0];
768
+ const val = calcData?.intPayAmt;
769
+ if (val == null) return undefined;
770
+ const num = Number(val);
771
+ return Number.isFinite(num) ? `₹ ${num.toLocaleString()}` : String(val);
772
+ })()}
773
+ totalInterestEarnings={(() => {
774
+ const data = calculationResult?.data;
775
+ const investmentType = payoutValue === 'On Maturity' ? 'SDR' : 'FDR';
776
+ const calcData = investmentType === 'SDR'
777
+ ? data?.sdrCalc?.[0]
778
+ : data?.fdrCalc?.[0];
779
+ const val = calcData?.totalInterestEarnings;
780
+ if (val == null) return undefined;
781
+ const num = Number(val);
782
+ return Number.isFinite(num) ? `₹ ${num.toLocaleString()}` : String(val);
761
783
  })()}
762
784
  >
763
785
  <CheckboxOption