@finspringinnovations/fdsdk 0.0.4 → 0.0.6

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,9 +14,12 @@
14
14
  */
15
15
 
16
16
  import { useCallback, useRef } from 'react';
17
- import { parseSSEBuffer, type SSEEvent } from '../utils/sseParser';
18
- import { getEnvironmentData } from '../config/appDataConfig';
19
- import { getApiConfig } from '../config/apiConfig';
17
+ import { Platform } from 'react-native';
18
+ import { parseSSEBuffer } from '../utils/sseParser';
19
+ import { getEnvironmentData, getUserInfoForAPI } from '../config/appDataConfig';
20
+ import { getApiConfig, getSecureHeaders } from '../config/apiConfig';
21
+ import { decryptResponse } from '../utils/encryption';
22
+ import { getEncryptionConfig } from '../config/encryptionConfig';
20
23
 
21
24
  // -- Types --
22
25
 
@@ -26,23 +29,46 @@ interface UsePaymentSSECallbacks {
26
29
  onConnected?: () => void;
27
30
  }
28
31
 
32
+ export interface PaymentSSEExtraHeaders {
33
+ workflowInstanceId?: string;
34
+ applicationId?: string;
35
+ entityid?: string;
36
+ providerId?: string;
37
+ }
38
+
29
39
  // -- Hook --
30
40
 
31
41
  export function usePaymentSSE() {
32
42
  // Store the XMLHttpRequest so we can abort it later
33
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);
34
48
  // Track how much of responseText we've already parsed
35
49
  const lastIndexRef = useRef<number>(0);
36
50
  // SSE text buffer (persists across onprogress calls)
37
51
  const bufferRef = useRef<{ value: string }>({ value: '' });
38
52
 
39
53
  const start = useCallback(
40
- (applicationId: string, callbacks: UsePaymentSSECallbacks) => {
54
+ (applicationId: string, callbacks: UsePaymentSSECallbacks, extraHeaders?: PaymentSSEExtraHeaders) => {
55
+ connectionIdRef.current += 1;
56
+ const connectionId = connectionIdRef.current;
57
+ stopRequestedRef.current = false;
58
+
41
59
  // Clean up any existing connection first
42
60
  if (xhrRef.current) {
43
61
  xhrRef.current.abort();
44
62
  xhrRef.current = null;
45
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
+ }
46
72
 
47
73
  // Reset parsing state
48
74
  lastIndexRef.current = 0;
@@ -51,95 +77,310 @@ export function usePaymentSSE() {
51
77
  // 1. Build the SSE endpoint URL
52
78
  const envData = getEnvironmentData();
53
79
  const apiConfig = getApiConfig();
54
- const baseUrl = envData?.apiBaseUrl || apiConfig?.baseUrl || '';
55
- const url = `${baseUrl}/events?applicationId=${encodeURIComponent(applicationId)}`;
80
+ const baseUrl = (envData?.apiBaseUrl || apiConfig?.baseUrl || '').replace(/\/$/, '');
81
+ const url = baseUrl ? `${baseUrl}/events?applicationId=${encodeURIComponent(applicationId)}` : '';
56
82
 
57
- // 2. Create XMLHttpRequest
58
- const xhr = new XMLHttpRequest();
59
- xhrRef.current = xhr;
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;
60
95
 
61
- xhr.open('GET', url);
62
-
63
- // 3. Set headers (same headers your baseApi interceptor uses)
64
- const apiKey = envData?.apiKey || apiConfig?.headers?.['X-API-Key'] || '';
65
- if (apiKey) {
66
- xhr.setRequestHeader('x-api-key', apiKey);
96
+ if (!url) {
97
+ if (__DEV__ && verboseSSE) console.warn('[usePaymentSSE] No base URL, skipping SSE');
98
+ return;
67
99
  }
68
- xhr.setRequestHeader('Accept', 'text/event-stream');
69
- xhr.setRequestHeader('Cache-Control', 'no-cache');
70
100
 
71
- // 4. Handle incoming data
72
- xhr.onprogress = () => {
73
- // responseText contains ALL text received so far
74
- // Only parse the NEW text since last call
75
- const newText = xhr.responseText.slice(lastIndexRef.current);
76
- lastIndexRef.current = xhr.responseText.length;
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
+ }
111
+
112
+ reconnectAttempts += 1;
113
+ const delay = reconnectDelayMs * reconnectAttempts;
114
+
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
+ });
125
+ }
126
+
127
+ if (reconnectTimerRef.current) {
128
+ clearTimeout(reconnectTimerRef.current);
129
+ }
130
+
131
+ reconnectTimerRef.current = setTimeout(() => {
132
+ if (stopRequestedRef.current || connectionIdRef.current !== connectionId) return;
133
+ openConnection();
134
+ }, delay);
135
+ };
136
+
137
+ const openConnection = () => {
138
+ if (stopRequestedRef.current || connectionIdRef.current !== connectionId) return;
139
+ connected = false;
140
+
141
+ // 2. Create XMLHttpRequest
142
+ const xhr = new XMLHttpRequest();
143
+ xhrRef.current = xhr;
77
144
 
78
- if (!newText) return;
145
+ xhr.open('GET', url);
79
146
 
80
- // Parse new text into SSE events
81
- const events = parseSSEBuffer(bufferRef.current, newText);
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
+ };
82
222
 
83
- // Handle each event
84
- for (const { eventType, data } of events) {
85
- if (eventType === 'fd_payment_status') {
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) => {
86
241
  try {
87
- const parsed = JSON.parse(data);
88
- const status = (parsed.paymentStatus ?? '').toUpperCase();
89
- callbacks.onPaymentStatus(status, parsed);
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);
90
265
  } catch (parseError) {
91
- if (__DEV__) {
92
- console.error('[usePaymentSSE] Failed to parse event data:', 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') {
276
+ try {
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);
280
+ } catch {
281
+ // ignore
93
282
  }
94
283
  }
95
284
  }
96
- }
97
- };
285
+ };
98
286
 
99
- // 5. Handle successful connection
100
- xhr.onreadystatechange = () => {
101
- if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
102
- if (xhr.status === 200) {
103
- callbacks.onConnected?.();
104
- if (__DEV__) {
105
- console.log('[usePaymentSSE] Connected to SSE stream');
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}`);
106
297
  }
107
- } else {
108
- if (__DEV__) {
109
- console.error('[usePaymentSSE] Bad status:', xhr.status);
298
+ }
299
+
300
+ if (xhr.readyState === XMLHttpRequest.DONE && !stopRequestedRef.current && connectionIdRef.current === connectionId) {
301
+ if (xhr.status !== 200) {
302
+ scheduleReconnect(`SSE closed with status: ${xhr.status}`);
110
303
  }
111
- callbacks.onError?.(new Error(`SSE response status: ${xhr.status}`));
112
304
  }
113
- }
114
- };
305
+ };
115
306
 
116
- // 6. Handle errors
117
- xhr.onerror = () => {
118
- if (__DEV__) {
119
- 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);
120
329
  }
121
- callbacks.onError?.(new Error('SSE connection failed'));
122
- };
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);
123
339
 
124
- // 7. Handle timeout
125
- xhr.ontimeout = () => {
126
340
  if (__DEV__) {
127
- 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
+ });
128
365
  }
129
- callbacks.onError?.(new Error('SSE connection timed out'));
130
366
  };
131
367
 
132
- // 8. Send the request (connection stays open)
133
- xhr.send();
134
-
135
- if (__DEV__) {
136
- console.log('[usePaymentSSE] Starting SSE connection to:', url);
137
- }
368
+ openConnection();
138
369
  },
139
370
  []
140
371
  );
141
372
 
142
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
+ }
143
384
  if (xhrRef.current) {
144
385
  xhrRef.current.abort();
145
386
  xhrRef.current = null;
@@ -152,4 +393,4 @@ export function usePaymentSSE() {
152
393
  }, []);
153
394
 
154
395
  return { start, stop };
155
- }
396
+ }
@@ -312,15 +312,21 @@ const RootNavigator: React.FC<RootNavigatorProps> = ({
312
312
  onGoBack={() => goBack()}
313
313
  paymentUrl={(getPaymentSession().paymentUrl) || ''}
314
314
  onPaymentSuccess={(data) => {
315
+ const payload = data && typeof data === 'object' ? data : {};
316
+ const transactionId = (payload as any)?.transactionId ?? (payload as any)?.transaction_id ?? (payload as any)?.TransactionId;
315
317
  navigate('PaymentStatus', {
316
318
  status: 'success',
317
- paymentData: data
319
+ paymentData: data,
320
+ transactionId,
318
321
  } as any);
319
322
  }}
320
323
  onPaymentFailure={(error) => {
324
+ const payload = error && typeof error === 'object' ? error : {};
325
+ const transactionId = (payload as any)?.transactionId ?? (payload as any)?.transaction_id ?? (payload as any)?.TransactionId;
321
326
  navigate('PaymentStatus', {
322
327
  status: 'failed',
323
- paymentData: error
328
+ paymentData: error,
329
+ transactionId,
324
330
  } as any);
325
331
  }}
326
332
  onPaymentPending={(info) => {
@@ -295,7 +295,7 @@ const AadhaarVerification: React.FC<AadhaarVerificationProps> = ({
295
295
  applicationid: applicationId,
296
296
  entityid: entityId,
297
297
  // Request params/body
298
- targetTaskId: WORKFLOW_TASKS.VALIDATE_OTP,
298
+ targetTaskCaption: WORKFLOW_TASKS.VALIDATE_OTP,
299
299
  }).unwrap();
300
300
  setIsValidateOtpTaskCalled(true);
301
301
  } else {
@@ -357,7 +357,7 @@ const AadhaarVerification: React.FC<AadhaarVerificationProps> = ({
357
357
  applicationid: applicationId,
358
358
  entityid: entityId,
359
359
  // Request params/body
360
- targetTaskId: WORKFLOW_TASKS.RESEND_OTP,
360
+ targetTaskCaption: WORKFLOW_TASKS.RESEND_OTP,
361
361
  }).unwrap();
362
362
 
363
363
 
@@ -140,7 +140,12 @@ const FDCalculator: React.FC<FDCalculatorProps> = ({ onGoBack, onExitSDK, onPanR
140
140
 
141
141
  const { data: fallbackMasterData, isLoading: isLoadingFallback, error: fallbackError, refetch: refetchMasterData } = useGetMasterDataQuery(
142
142
  { providerId: defaultProviderId }, // Use default provider ID
143
- { skip: !!masterData } // Skip if master data is already available
143
+ {
144
+ skip: !!masterData || !defaultProviderId,
145
+ refetchOnMountOrArgChange: true,
146
+ refetchOnFocus: true,
147
+ refetchOnReconnect: true,
148
+ }
144
149
  );
145
150
 
146
151
  // Use fallback master data if global master data is not available
@@ -1011,4 +1016,4 @@ const createStyles = (typography: any, colors: any, themeName: string) => StyleS
1011
1016
  backgroundColor: 'rgba(0, 0, 0, 0.3)',
1012
1017
  zIndex: 1000,
1013
1018
  },
1014
- });
1019
+ });