@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.
@@ -1,12 +1,14 @@
1
- import React, { useRef, useState, useEffect } from 'react';
1
+ import React, { useRef, useState, useEffect, useCallback } from 'react';
2
2
  import { View, StyleSheet, ActivityIndicator, BackHandler, Platform, StatusBar } from 'react-native';
3
3
  import { WebView } from 'react-native-webview';
4
4
  import SafeAreaWrapper from '../components/SafeAreaWrapper';
5
5
  import { useColors } from '../theme/ThemeContext';
6
6
  import { decryptResponse } from '../utils/encryption';
7
+ import { parseSSEBuffer } from '../utils/sseParser';
7
8
  import { getEncryptionConfig } from '../config/encryptionConfig';
9
+ import { getEnvironmentData, getUserInfoForAPI } from '../config/appDataConfig';
10
+ import { getApiConfig, getSecureHeaders } from '../config/apiConfig';
8
11
  import { useAppSelector } from '../store';
9
- import { usePaymentSSE } from '../hooks/usePaymentSSE';
10
12
 
11
13
  export interface PaymentProps {
12
14
  onGoBack?: () => void;
@@ -36,14 +38,22 @@ const Payment: React.FC<PaymentProps> = ({
36
38
  const workflowInstanceId = useAppSelector((state: any) => state?.onboarding?.workflowInstanceId);
37
39
  const entityId = useAppSelector((state: any) => state?.onboarding?.entityid);
38
40
  const providerId = useAppSelector((state: any) => state?.onboarding?.providerId);
39
- const { start: startPaymentSSE, stop: stopPaymentSSE } = usePaymentSSE();
41
+ const sseXhrRef = useRef<XMLHttpRequest | null>(null);
42
+ const sseLastIndexRef = useRef<number>(0);
43
+ const sseBufferRef = useRef<{ value: string }>({ value: '' });
40
44
  const onSuccessRef = useRef(onPaymentSuccess);
41
45
  const onFailureRef = useRef(onPaymentFailure);
42
46
  onSuccessRef.current = onPaymentSuccess;
43
47
  onFailureRef.current = onPaymentFailure;
44
48
 
45
-
46
-
49
+ const stopPaymentSSE = useCallback(() => {
50
+ if (sseXhrRef.current) {
51
+ sseXhrRef.current.abort();
52
+ sseXhrRef.current = null;
53
+ }
54
+ sseLastIndexRef.current = 0;
55
+ sseBufferRef.current = { value: '' };
56
+ }, []);
47
57
 
48
58
  const handleNavigationStateChange = async (navState: any) => {
49
59
  const { url } = navState;
@@ -73,37 +83,160 @@ const Payment: React.FC<PaymentProps> = ({
73
83
 
74
84
  // SSE: listen for payment status while user is on Payment WebView (same as web integration)
75
85
  useEffect(() => {
76
- if (!applicationId || !paymentUrl?.trim()) return;
77
-
78
- startPaymentSSE(
79
- applicationId,
80
- {
81
- onPaymentStatus: (status, payload) => {
82
- const normalized = String(status || '').toUpperCase();
83
- if (normalized === 'SUCCESS') {
84
- stopPaymentSSE();
85
- onSuccessRef.current?.(payload);
86
- return;
86
+ if (!applicationId || !paymentUrl?.trim()) {
87
+ stopPaymentSSE();
88
+ return;
89
+ }
90
+
91
+ const envData = getEnvironmentData();
92
+ const apiConfig = getApiConfig();
93
+ const baseUrl = (envData?.apiBaseUrl || apiConfig?.baseUrl || '').replace(/\/$/, '');
94
+ const url = baseUrl ? `${baseUrl}/events?applicationId=${encodeURIComponent(applicationId)}` : '';
95
+
96
+ if (!url) return;
97
+
98
+ stopPaymentSSE();
99
+
100
+ const xhr = new XMLHttpRequest();
101
+ sseXhrRef.current = xhr;
102
+ xhr.open('GET', url);
103
+
104
+ const apiKey = envData?.apiKey || apiConfig?.headers?.['X-API-Key'] || '';
105
+ if (apiKey) {
106
+ xhr.setRequestHeader('x-api-key', apiKey);
107
+ xhr.setRequestHeader('X-API-Key', apiKey);
108
+ }
109
+
110
+ const secureHeaders = getSecureHeaders();
111
+ Object.entries(secureHeaders).forEach(([key, value]) => {
112
+ if (key.toLowerCase() !== 'content-type') {
113
+ xhr.setRequestHeader(key, value);
114
+ }
115
+ });
116
+
117
+ xhr.setRequestHeader('Accept', 'text/event-stream');
118
+ xhr.setRequestHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
119
+ xhr.setRequestHeader('Pragma', 'no-cache');
120
+ xhr.setRequestHeader('Expires', '0');
121
+
122
+ try {
123
+ const userInfo = getUserInfoForAPI();
124
+ const userRefId = userInfo?.userReferenceId || userInfo?.id;
125
+ if (userRefId) {
126
+ xhr.setRequestHeader('userreferenceid', userRefId);
127
+ xhr.setRequestHeader('userReferenceId', userRefId);
128
+ }
129
+ } catch {
130
+ // ignore
131
+ }
132
+
133
+ if (providerId) {
134
+ xhr.setRequestHeader('provider', providerId);
135
+ xhr.setRequestHeader('providerid', providerId);
136
+ xhr.setRequestHeader('providerId', providerId);
137
+ }
138
+ if (workflowInstanceId) {
139
+ xhr.setRequestHeader('workflowInstanceId', workflowInstanceId);
140
+ xhr.setRequestHeader('workflowinstanceid', workflowInstanceId);
141
+ }
142
+ if (applicationId) {
143
+ xhr.setRequestHeader('applicationId', applicationId);
144
+ xhr.setRequestHeader('applicationid', applicationId);
145
+ }
146
+ if (entityId) {
147
+ xhr.setRequestHeader('entityid', entityId);
148
+ xhr.setRequestHeader('entityId', entityId);
149
+ }
150
+
151
+ const handleEventData = async (rawData: string) => {
152
+ try {
153
+ const parsed = JSON.parse(rawData || '{}') as Record<string, unknown>;
154
+ let payload = parsed;
155
+ if (typeof parsed?.encryptedResponse === 'string' && parsed.encryptedResponse) {
156
+ try {
157
+ const config = getEncryptionConfig();
158
+ const decrypted = await decryptResponse(
159
+ { encryptedResponse: parsed.encryptedResponse },
160
+ config
161
+ );
162
+ payload = (decrypted || {}) as Record<string, unknown>;
163
+ } catch {
164
+ // ignore and use raw payload
87
165
  }
88
- if (normalized === 'FAILED') {
89
- stopPaymentSSE();
90
- onFailureRef.current?.(payload);
166
+ }
167
+
168
+ const payloadData = (payload?.data as Record<string, unknown> | undefined) || {};
169
+ const status = String(
170
+ payload?.paymentStatus ??
171
+ payload?.payment_status ??
172
+ payloadData?.paymentStatus ??
173
+ payloadData?.payment_status ??
174
+ ''
175
+ ).toUpperCase();
176
+
177
+ if (status === 'SUCCESS') {
178
+ stopPaymentSSE();
179
+ onSuccessRef.current?.(payload);
180
+ return;
181
+ }
182
+
183
+ if (status === 'FAILED') {
184
+ stopPaymentSSE();
185
+ onFailureRef.current?.(payload);
186
+ }
187
+ } catch {
188
+ // ignore parse errors
189
+ }
190
+ };
191
+
192
+ xhr.onprogress = () => {
193
+ setLoading(false);
194
+
195
+ const newText = xhr.responseText.slice(sseLastIndexRef.current);
196
+ sseLastIndexRef.current = xhr.responseText.length;
197
+ if (!newText) return;
198
+
199
+ const events = parseSSEBuffer(sseBufferRef.current, newText);
200
+ for (const { eventType, data } of events) {
201
+ if (!data) continue;
202
+ const normalizedEventType = String(eventType || '').trim().toLowerCase();
203
+ if (normalizedEventType === 'fd_payment_status') {
204
+ handleEventData(data);
205
+ } else if (normalizedEventType === 'message') {
206
+ try {
207
+ const parsed = JSON.parse(data) as { event?: string; [k: string]: unknown };
208
+ const embeddedEvent = String(parsed?.event || '').trim().toLowerCase();
209
+ if (embeddedEvent === 'fd_payment_status') {
210
+ handleEventData(data);
211
+ }
212
+ } catch {
213
+ // ignore
91
214
  }
92
- },
93
- onError: () => {
94
- if (__DEV__) console.warn('[Payment] SSE connection error');
95
- },
96
- onConnected: () => {
97
- if (__DEV__) console.log('[Payment] SSE connected for payment status');
98
- },
99
- },
100
- { workflowInstanceId, applicationId, entityid: entityId, providerId }
101
- );
215
+ }
216
+ }
217
+ };
218
+
219
+ xhr.onreadystatechange = () => {
220
+ if (
221
+ (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED ||
222
+ xhr.readyState === XMLHttpRequest.LOADING) &&
223
+ xhr.status === 200
224
+ ) {
225
+ setLoading(false);
226
+ if (__DEV__) console.log('[Payment] SSE connected for payment status');
227
+ }
228
+ };
229
+
230
+ xhr.onerror = () => {
231
+ if (__DEV__) console.warn('[Payment] SSE connection error');
232
+ };
233
+
234
+ xhr.send();
102
235
 
103
236
  return () => {
104
237
  stopPaymentSSE();
105
238
  };
106
- }, [applicationId, paymentUrl, workflowInstanceId, entityId, providerId, startPaymentSSE, stopPaymentSSE]);
239
+ }, [applicationId, paymentUrl, workflowInstanceId, entityId, providerId, stopPaymentSSE]);
107
240
 
108
241
  const handleMessage = async (event: any) => {
109
242
  try {
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect } from 'react';
1
+ import React, { useState, useEffect, useRef, useCallback } from 'react';
2
2
  import { View, Text, StyleSheet, ScrollView, Alert, Image, BackHandler, Platform, StatusBar } from 'react-native';
3
3
  import Icon from 'react-native-vector-icons/Ionicons';
4
4
  import { base64Images } from '../constants/strings/base64Images';
@@ -9,13 +9,16 @@ import { useColors, useTypography, useTheme } from '../theme/ThemeContext';
9
9
  import { usePaymentRetryMutation } from '../api/fdApi';
10
10
  import { useGetCustomerApplicationsMutation } from '../api/customerApi';
11
11
  import { useAppSelector } from '../store';
12
- import { getUserInfoForAPI } from '../config/appDataConfig';
12
+ import { getEnvironmentData, getUserInfoForAPI } from '../config/appDataConfig';
13
+ import { getApiConfig, getSecureHeaders } from '../config/apiConfig';
13
14
  import { navigate } from '../navigation/helpers';
14
15
  import { setPaymentSession, getPaymentSession } from '../state/paymentSession';
15
16
  import { useRoute } from '@react-navigation/native';
16
17
  import { BANK_STRINGS } from '../constants/strings/bank';
17
18
  import { COMMON_STRINGS } from '../constants/strings/common';
18
- import { usePaymentSSE } from '../hooks/usePaymentSSE';
19
+ import { parseSSEBuffer } from '../utils/sseParser';
20
+ import { decryptResponse } from '../utils/encryption';
21
+ import { getEncryptionConfig } from '../config/encryptionConfig';
19
22
 
20
23
  export interface PaymentStatusProps {
21
24
  onRetry?: () => void;
@@ -62,8 +65,9 @@ const PaymentStatus: React.FC<PaymentStatusProps> = ({
62
65
  const applicationId = useAppSelector((state: any) => state?.onboarding?.applicationId);
63
66
  const entityId = useAppSelector((state: any) => state?.onboarding?.entityid);
64
67
  const providerId = useAppSelector((state: any) => state?.onboarding?.providerId);
65
-
66
- const { start: startPaymentSSE, stop: stopPaymentSSE } = usePaymentSSE();
68
+ const sseXhrRef = useRef<XMLHttpRequest | null>(null);
69
+ const sseLastIndexRef = useRef<number>(0);
70
+ const sseBufferRef = useRef<{ value: string }>({ value: '' });
67
71
 
68
72
  // Payment Retry API
69
73
  const [paymentRetry, {
@@ -100,34 +104,160 @@ const PaymentStatus: React.FC<PaymentStatusProps> = ({
100
104
  return amount.toLocaleString('en-IN');
101
105
  };
102
106
 
107
+ const stopPaymentSSE = useCallback(() => {
108
+ if (sseXhrRef.current) {
109
+ sseXhrRef.current.abort();
110
+ sseXhrRef.current = null;
111
+ }
112
+ sseLastIndexRef.current = 0;
113
+ sseBufferRef.current = { value: '' };
114
+ }, []);
115
+
103
116
  useEffect(() => {
104
117
  if (currentStatus !== 'pending' || !applicationId) {
105
118
  stopPaymentSSE();
106
119
  return;
107
120
  }
108
121
 
109
- startPaymentSSE(applicationId, {
110
- onPaymentStatus: (status) => {
111
- const normalized = String(status || '').toUpperCase();
112
- if (normalized === 'SUCCESS') {
122
+ const envData = getEnvironmentData();
123
+ const apiConfig = getApiConfig();
124
+ const baseUrl = (envData?.apiBaseUrl || apiConfig?.baseUrl || '').replace(/\/$/, '');
125
+ const url = baseUrl ? `${baseUrl}/events?applicationId=${encodeURIComponent(applicationId)}` : '';
126
+
127
+ if (!url) return;
128
+
129
+ stopPaymentSSE();
130
+
131
+ const xhr = new XMLHttpRequest();
132
+ sseXhrRef.current = xhr;
133
+ xhr.open('GET', url);
134
+
135
+ const apiKey = envData?.apiKey || apiConfig?.headers?.['X-API-Key'] || '';
136
+ if (apiKey) {
137
+ xhr.setRequestHeader('x-api-key', apiKey);
138
+ xhr.setRequestHeader('X-API-Key', apiKey);
139
+ }
140
+
141
+ const secureHeaders = getSecureHeaders();
142
+ Object.entries(secureHeaders).forEach(([key, value]) => {
143
+ if (key.toLowerCase() !== 'content-type') {
144
+ xhr.setRequestHeader(key, value);
145
+ }
146
+ });
147
+
148
+ xhr.setRequestHeader('Accept', 'text/event-stream');
149
+ xhr.setRequestHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
150
+ xhr.setRequestHeader('Pragma', 'no-cache');
151
+ xhr.setRequestHeader('Expires', '0');
152
+
153
+ try {
154
+ const userInfo = getUserInfoForAPI();
155
+ const userRefId = userInfo?.userReferenceId || userInfo?.id;
156
+ if (userRefId) {
157
+ xhr.setRequestHeader('userreferenceid', userRefId);
158
+ xhr.setRequestHeader('userReferenceId', userRefId);
159
+ }
160
+ } catch {
161
+ // ignore
162
+ }
163
+
164
+ if (providerId) {
165
+ xhr.setRequestHeader('provider', providerId);
166
+ xhr.setRequestHeader('providerid', providerId);
167
+ xhr.setRequestHeader('providerId', providerId);
168
+ }
169
+ if (workflowInstanceId) {
170
+ xhr.setRequestHeader('workflowInstanceId', workflowInstanceId);
171
+ xhr.setRequestHeader('workflowinstanceid', workflowInstanceId);
172
+ }
173
+ if (applicationId) {
174
+ xhr.setRequestHeader('applicationId', applicationId);
175
+ xhr.setRequestHeader('applicationid', applicationId);
176
+ }
177
+ if (entityId) {
178
+ xhr.setRequestHeader('entityid', entityId);
179
+ xhr.setRequestHeader('entityId', entityId);
180
+ }
181
+
182
+ const handleEventData = async (rawData: string) => {
183
+ try {
184
+ const parsed = JSON.parse(rawData || '{}') as Record<string, unknown>;
185
+ let payload = parsed;
186
+ if (typeof parsed?.encryptedResponse === 'string' && parsed.encryptedResponse) {
187
+ try {
188
+ const config = getEncryptionConfig();
189
+ const decrypted = await decryptResponse(
190
+ { encryptedResponse: parsed.encryptedResponse },
191
+ config
192
+ );
193
+ payload = (decrypted || {}) as Record<string, unknown>;
194
+ } catch {
195
+ // ignore and use raw payload
196
+ }
197
+ }
198
+
199
+ const payloadData = (payload?.data as Record<string, unknown> | undefined) || {};
200
+ const status = String(
201
+ payload?.paymentStatus ??
202
+ payload?.payment_status ??
203
+ payloadData?.paymentStatus ??
204
+ payloadData?.payment_status ??
205
+ ''
206
+ ).toUpperCase();
207
+
208
+ if (status === 'SUCCESS') {
113
209
  setCurrentStatus('success');
114
210
  stopPaymentSSE();
115
211
  return;
116
212
  }
117
- if (normalized === 'FAILED') {
213
+
214
+ if (status === 'FAILED') {
118
215
  setCurrentStatus('failed');
119
216
  stopPaymentSSE();
120
217
  }
121
- },
122
- onError: () => {
123
- // keep pending state; stream can be restarted on re-render
124
- },
125
- });
218
+ } catch {
219
+ // ignore parse errors
220
+ }
221
+ };
222
+
223
+ xhr.onprogress = () => {
224
+ const newText = xhr.responseText.slice(sseLastIndexRef.current);
225
+ sseLastIndexRef.current = xhr.responseText.length;
226
+ if (!newText) return;
227
+
228
+ const events = parseSSEBuffer(sseBufferRef.current, newText);
229
+ for (const { eventType, data } of events) {
230
+ if (!data) continue;
231
+ const normalizedEventType = String(eventType || '').trim().toLowerCase();
232
+ if (normalizedEventType === 'fd_payment_status') {
233
+ handleEventData(data);
234
+ } else if (normalizedEventType === 'message') {
235
+ try {
236
+ const parsed = JSON.parse(data) as { event?: string; [k: string]: unknown };
237
+ const embeddedEvent = String(parsed?.event || '').trim().toLowerCase();
238
+ if (embeddedEvent === 'fd_payment_status') {
239
+ handleEventData(data);
240
+ }
241
+ } catch {
242
+ // ignore
243
+ }
244
+ }
245
+ }
246
+ };
247
+
248
+ xhr.send();
126
249
 
127
250
  return () => {
128
251
  stopPaymentSSE();
129
252
  };
130
- }, [applicationId, currentStatus, startPaymentSSE, stopPaymentSSE]);
253
+ }, [
254
+ applicationId,
255
+ currentStatus,
256
+ entityId,
257
+ providerId,
258
+ stopPaymentSSE,
259
+ workflowInstanceId
260
+ ]);
131
261
 
132
262
  // Handle payment retry when status is failed
133
263
  const handlePaymentRetry = async () => {
@@ -11,13 +11,29 @@ export function parseSSEBuffer(
11
11
 
12
12
  buffer.value += chunk;
13
13
 
14
- let idx: number;
14
+ let idx = -1;
15
+ let delimiterLength = 0;
16
+ const findDelimiter = () => {
17
+ const unixIdx = buffer.value.indexOf('\n\n');
18
+ const windowsIdx = buffer.value.indexOf('\r\n\r\n');
19
+ if (windowsIdx >= 0 && (unixIdx < 0 || windowsIdx < unixIdx)) {
20
+ idx = windowsIdx;
21
+ delimiterLength = 4;
22
+ return true;
23
+ }
24
+ if (unixIdx >= 0) {
25
+ idx = unixIdx;
26
+ delimiterLength = 2;
27
+ return true;
28
+ }
29
+ return false;
30
+ };
15
31
 
16
- while ((idx = buffer.value.indexOf('\n\n')) >= 0) {
32
+ while (findDelimiter()) {
17
33
 
18
34
  const block = buffer.value.slice(0, idx);
19
35
 
20
- buffer.value = buffer.value.slice(idx + 2);
36
+ buffer.value = buffer.value.slice(idx + delimiterLength);
21
37
 
22
38
  let eventType = 'message';
23
39
 
@@ -37,4 +53,4 @@ export function parseSSEBuffer(
37
53
  }
38
54
  }
39
55
  return results;
40
- }
56
+ }