@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.
- package/lib/api/baseApi.js +17 -6
- package/lib/api/masterDataApi.js +21 -1
- package/lib/api/workflowApi.js +46 -1
- package/lib/config/appDataConfig.d.ts +5 -0
- package/lib/hooks/usePaymentSSE.d.ts +7 -1
- package/lib/hooks/usePaymentSSE.js +287 -63
- package/lib/navigation/RootNavigator.js +10 -2
- package/lib/screens/AadhaarVerification.js +2 -2
- package/lib/screens/FDCalculator.js +6 -2
- package/lib/screens/FDList.js +37 -64
- package/lib/screens/Payment.js +159 -27
- package/lib/screens/PaymentStatus.js +139 -25
- package/lib/screens/ReviewKYC.js +45 -97
- package/lib/utils/sseParser.js +19 -3
- package/package.json +1 -1
- package/src/api/baseApi.ts +19 -6
- package/src/api/masterDataApi.ts +21 -3
- package/src/api/workflowApi.ts +50 -1
- package/src/config/appDataConfig.ts +5 -0
- package/src/hooks/usePaymentSSE.ts +307 -66
- package/src/navigation/RootNavigator.tsx +8 -2
- package/src/screens/AadhaarVerification.tsx +2 -2
- package/src/screens/FDCalculator.tsx +7 -2
- package/src/screens/FDList.tsx +37 -76
- package/src/screens/Payment.tsx +181 -38
- package/src/screens/PaymentStatus.tsx +158 -43
- package/src/screens/ReviewKYC.tsx +94 -170
- package/src/utils/sseParser.ts +20 -4
package/src/screens/FDList.tsx
CHANGED
|
@@ -20,7 +20,6 @@ import { useGetCustomerApplicationsMutation } from '../api/customerApi';
|
|
|
20
20
|
import { useGetMasterDataQuery } from '../api/masterDataApi';
|
|
21
21
|
import { useMasterData } from '../providers/MasterDataProvider';
|
|
22
22
|
import { useTerminateWorkflowMutation } from '../api/workflowApi';
|
|
23
|
-
import { usePaymentReverseFeedMutation } from '../api/fdApi';
|
|
24
23
|
import type { ColorScheme, ThemeName } from '../theme';
|
|
25
24
|
import SafeAreaWrapper from '../components/SafeAreaWrapper';
|
|
26
25
|
import { getUserInfoForAPI, getAppData } from '../config/appDataConfig';
|
|
@@ -99,22 +98,9 @@ const FDList: React.FC<FDListProps> = ({
|
|
|
99
98
|
isLoading: isTerminatingWorkflow,
|
|
100
99
|
}] = useTerminateWorkflowMutation();
|
|
101
100
|
|
|
102
|
-
// Payment Reverse Feed API
|
|
103
|
-
const [paymentReverseFeed, {
|
|
104
|
-
data: paymentReverseFeedResponse,
|
|
105
|
-
error: paymentReverseFeedError,
|
|
106
|
-
isLoading: isLoadingPaymentReverseFeed,
|
|
107
|
-
}] = usePaymentReverseFeedMutation();
|
|
108
|
-
|
|
109
101
|
const styles = createStyles(colors, typography, spacing, themeName);
|
|
110
102
|
const { setMasterData } = useMasterData();
|
|
111
103
|
|
|
112
|
-
// Redux selectors for workflow IDs
|
|
113
|
-
const workflowInstanceId = useAppSelector((state: any) => state?.onboarding?.workflowInstanceId);
|
|
114
|
-
const applicationId = useAppSelector((state: any) => state?.onboarding?.applicationId);
|
|
115
|
-
const entityId = useAppSelector((state: any) => state?.onboarding?.entityid);
|
|
116
|
-
const providerId = useAppSelector((state: any) => state?.onboarding?.providerId);
|
|
117
|
-
|
|
118
104
|
// Helper function to check if customer applications is empty
|
|
119
105
|
const isCustomerApplicationsEmpty = () => {
|
|
120
106
|
if (!customerApplications) {
|
|
@@ -223,7 +209,12 @@ const FDList: React.FC<FDListProps> = ({
|
|
|
223
209
|
|
|
224
210
|
const { data: masterData, isLoading: isLoadingMaster } = useGetMasterDataQuery(
|
|
225
211
|
{ providerId: inferredProviderId },
|
|
226
|
-
{
|
|
212
|
+
{
|
|
213
|
+
skip: !inferredProviderId,
|
|
214
|
+
refetchOnMountOrArgChange: true,
|
|
215
|
+
refetchOnFocus: true,
|
|
216
|
+
refetchOnReconnect: true,
|
|
217
|
+
}
|
|
227
218
|
);
|
|
228
219
|
|
|
229
220
|
// Only render once all three API calls have completed (success or error)
|
|
@@ -491,26 +482,34 @@ const FDList: React.FC<FDListProps> = ({
|
|
|
491
482
|
|
|
492
483
|
// Persist onboarding identifiers globally for subsequent API calls (like FDCalculator)
|
|
493
484
|
try {
|
|
494
|
-
// Extract workflow parameters from response
|
|
485
|
+
// Extract workflow parameters from response (support array / data[] / applications[])
|
|
495
486
|
const responseData = customerApplications?.data || customerApplications;
|
|
496
|
-
let
|
|
487
|
+
let applicationsData: any[] = [];
|
|
488
|
+
if (Array.isArray(responseData)) {
|
|
489
|
+
applicationsData = responseData;
|
|
490
|
+
} else if (responseData?.applications && Array.isArray(responseData.applications)) {
|
|
491
|
+
applicationsData = responseData.applications;
|
|
492
|
+
} else if (Array.isArray(customerApplications?.applications)) {
|
|
493
|
+
applicationsData = customerApplications.applications;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
let applicationData = applicationsData.length > 0 ? applicationsData[0] : (Array.isArray(responseData) ? responseData[0] : responseData);
|
|
497
497
|
|
|
498
498
|
// Prefer the application with wf_status === 'Active'
|
|
499
|
-
if (
|
|
500
|
-
const activeOnly =
|
|
499
|
+
if (applicationsData.length > 0) {
|
|
500
|
+
const activeOnly = applicationsData.find((app: any) => {
|
|
501
501
|
const status = (app.wf_status || app.status || '').toString();
|
|
502
502
|
return status === 'Active';
|
|
503
503
|
});
|
|
504
504
|
if (activeOnly) applicationData = activeOnly;
|
|
505
505
|
}
|
|
506
506
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
}
|
|
507
|
+
// Set completed flag robustly for downstream ReviewKYC/Aadhaar flow decisions
|
|
508
|
+
completeFDData = applicationsData.find((app: any) => {
|
|
509
|
+
const status = (app.wf_status || app.status || '').toString().toLowerCase();
|
|
510
|
+
return status === 'completed';
|
|
511
|
+
});
|
|
512
|
+
setGlobalData({ completeFDData: !!completeFDData });
|
|
514
513
|
if (applicationData) {
|
|
515
514
|
const ids = {
|
|
516
515
|
workflowInstanceId: applicationData.workflow_instance_id,
|
|
@@ -593,56 +592,18 @@ const FDList: React.FC<FDListProps> = ({
|
|
|
593
592
|
// Handle error silently
|
|
594
593
|
}
|
|
595
594
|
|
|
596
|
-
// If current state is payment and transactionId is available,
|
|
595
|
+
// If current state is payment and transactionId is available,
|
|
596
|
+
// open PaymentStatus and let SSE (/events) drive status updates.
|
|
597
597
|
if (currentState === WORKFLOW_STATES.PAYMENT && transactionId) {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
userreferenceid: userInfo.id,
|
|
608
|
-
applicationid: applicationId,
|
|
609
|
-
entityid: entityId,
|
|
610
|
-
// Body
|
|
611
|
-
transactionId: transactionId,
|
|
612
|
-
};
|
|
613
|
-
|
|
614
|
-
const response = await paymentReverseFeed(paymentReverseFeedRequest).unwrap();
|
|
615
|
-
|
|
616
|
-
// Handle the response based on payment status
|
|
617
|
-
const paymentStatus = (response?.data?.paymentStatus || '').toLowerCase();
|
|
618
|
-
const statusParam = paymentStatus === 'success' ? 'success' : paymentStatus === 'failed' ? 'failed' : 'pending';
|
|
619
|
-
|
|
620
|
-
// Build fdData for display
|
|
621
|
-
const fdDataParam = activeFD ? {
|
|
622
|
-
companyName: activeFD.name,
|
|
623
|
-
amount: Number(activeFD.invested) || 0,
|
|
624
|
-
fdRate: `${activeFD.returns}% p.a.`,
|
|
625
|
-
tenure: activeFD.maturityDate ? `${activeFD.maturityDate}` : '-',
|
|
626
|
-
interestPayout: activeFD.interestPayout || 'Yearly',
|
|
627
|
-
} : undefined;
|
|
628
|
-
|
|
629
|
-
navigate('PaymentStatus', { status: statusParam, transactionId, fdData: fdDataParam } as any);
|
|
630
|
-
return;
|
|
631
|
-
|
|
632
|
-
} catch (error) {
|
|
633
|
-
// Handle error silently
|
|
634
|
-
|
|
635
|
-
// On error, navigate with pending status as fallback
|
|
636
|
-
const fdDataParam = activeFD ? {
|
|
637
|
-
companyName: activeFD.name,
|
|
638
|
-
amount: Number(activeFD.invested) || 0,
|
|
639
|
-
fdRate: `${activeFD.returns}% p.a.`,
|
|
640
|
-
tenure: activeFD.maturityDate ? `${activeFD.maturityDate}` : '-',
|
|
641
|
-
interestPayout: activeFD.interestPayout || 'Yearly',
|
|
642
|
-
} : undefined;
|
|
643
|
-
navigate('PaymentStatus', { status: 'pending', transactionId, fdData: fdDataParam } as any);
|
|
644
|
-
return;
|
|
645
|
-
}
|
|
598
|
+
const fdDataParam = activeFD ? {
|
|
599
|
+
companyName: activeFD.name,
|
|
600
|
+
amount: Number(activeFD.invested) || 0,
|
|
601
|
+
fdRate: `${activeFD.returns}% p.a.`,
|
|
602
|
+
tenure: activeFD.maturityDate ? `${activeFD.maturityDate}` : '-',
|
|
603
|
+
interestPayout: activeFD.interestPayout || 'Yearly',
|
|
604
|
+
} : undefined;
|
|
605
|
+
navigate('PaymentStatus', { status: 'pending', transactionId, fdData: fdDataParam } as any);
|
|
606
|
+
return;
|
|
646
607
|
}
|
|
647
608
|
|
|
648
609
|
// Use workflow navigation based on current state (non-payment states or no transactionId)
|
|
@@ -1281,4 +1242,4 @@ const createStyles = (colors: ColorScheme, typography: any, spacing: any, themeN
|
|
|
1281
1242
|
},
|
|
1282
1243
|
});
|
|
1283
1244
|
|
|
1284
|
-
export default FDList;
|
|
1245
|
+
export default FDList;
|
package/src/screens/Payment.tsx
CHANGED
|
@@ -1,13 +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';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { getEnvironmentData, getUserInfoForAPI } from '../config/appDataConfig';
|
|
10
|
+
import { getApiConfig, getSecureHeaders } from '../config/apiConfig';
|
|
11
|
+
import { useAppSelector } from '../store';
|
|
11
12
|
|
|
12
13
|
export interface PaymentProps {
|
|
13
14
|
onGoBack?: () => void;
|
|
@@ -32,36 +33,27 @@ const Payment: React.FC<PaymentProps> = ({
|
|
|
32
33
|
const styles = createStyles(colors);
|
|
33
34
|
const webViewRef = useRef<WebView>(null);
|
|
34
35
|
const [loading, setLoading] = useState(true);
|
|
35
|
-
const currentStatusRef = useRef<PaymentStatus>('pending');
|
|
36
|
-
const { transactionId } = getPaymentSession();
|
|
37
36
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
37
|
+
const applicationId = useAppSelector((state: any) => state?.onboarding?.applicationId);
|
|
38
|
+
const workflowInstanceId = useAppSelector((state: any) => state?.onboarding?.workflowInstanceId);
|
|
39
|
+
const entityId = useAppSelector((state: any) => state?.onboarding?.entityid);
|
|
40
|
+
const providerId = useAppSelector((state: any) => state?.onboarding?.providerId);
|
|
41
|
+
const sseXhrRef = useRef<XMLHttpRequest | null>(null);
|
|
42
|
+
const sseLastIndexRef = useRef<number>(0);
|
|
43
|
+
const sseBufferRef = useRef<{ value: string }>({ value: '' });
|
|
44
|
+
const onSuccessRef = useRef(onPaymentSuccess);
|
|
45
|
+
const onFailureRef = useRef(onPaymentFailure);
|
|
46
|
+
onSuccessRef.current = onPaymentSuccess;
|
|
47
|
+
onFailureRef.current = onPaymentFailure;
|
|
48
|
+
|
|
49
|
+
const stopPaymentSSE = useCallback(() => {
|
|
50
|
+
if (sseXhrRef.current) {
|
|
51
|
+
sseXhrRef.current.abort();
|
|
52
|
+
sseXhrRef.current = null;
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
useFocusEffect(
|
|
57
|
-
React.useCallback(() => {
|
|
58
|
-
startTimer();
|
|
59
|
-
|
|
60
|
-
}, [startTimer, stopTimer])
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
54
|
+
sseLastIndexRef.current = 0;
|
|
55
|
+
sseBufferRef.current = { value: '' };
|
|
56
|
+
}, []);
|
|
65
57
|
|
|
66
58
|
const handleNavigationStateChange = async (navState: any) => {
|
|
67
59
|
const { url } = navState;
|
|
@@ -89,6 +81,163 @@ const Payment: React.FC<PaymentProps> = ({
|
|
|
89
81
|
setLoading(false);
|
|
90
82
|
};
|
|
91
83
|
|
|
84
|
+
// SSE: listen for payment status while user is on Payment WebView (same as web integration)
|
|
85
|
+
useEffect(() => {
|
|
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
|
|
165
|
+
}
|
|
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
|
|
214
|
+
}
|
|
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();
|
|
235
|
+
|
|
236
|
+
return () => {
|
|
237
|
+
stopPaymentSSE();
|
|
238
|
+
};
|
|
239
|
+
}, [applicationId, paymentUrl, workflowInstanceId, entityId, providerId, stopPaymentSSE]);
|
|
240
|
+
|
|
92
241
|
const handleMessage = async (event: any) => {
|
|
93
242
|
try {
|
|
94
243
|
const dataString = event.nativeEvent.data;
|
|
@@ -133,18 +282,13 @@ const Payment: React.FC<PaymentProps> = ({
|
|
|
133
282
|
const paymentStatus = response.data?.paymentStatus?.toLowerCase();
|
|
134
283
|
|
|
135
284
|
if (paymentStatus === 'success') {
|
|
136
|
-
currentStatusRef.current = "success";
|
|
137
|
-
stopTimer();
|
|
138
285
|
onPaymentSuccess?.(response);
|
|
139
286
|
} else if (paymentStatus === 'failed') {
|
|
140
|
-
currentStatusRef.current = "failed";
|
|
141
|
-
stopTimer();
|
|
142
287
|
onPaymentFailure?.(response);
|
|
143
288
|
} else {
|
|
144
289
|
onPaymentPending?.(response);
|
|
145
290
|
}
|
|
146
291
|
} else {
|
|
147
|
-
currentStatusRef.current = "pending";
|
|
148
292
|
onPaymentPending?.(response);
|
|
149
293
|
}
|
|
150
294
|
|
|
@@ -252,4 +396,3 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|
|
252
396
|
});
|
|
253
397
|
|
|
254
398
|
export default Payment;
|
|
255
|
-
|
|
@@ -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,14 +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 {
|
|
19
|
-
import {
|
|
19
|
+
import { parseSSEBuffer } from '../utils/sseParser';
|
|
20
|
+
import { decryptResponse } from '../utils/encryption';
|
|
21
|
+
import { getEncryptionConfig } from '../config/encryptionConfig';
|
|
20
22
|
|
|
21
23
|
export interface PaymentStatusProps {
|
|
22
24
|
onRetry?: () => void;
|
|
@@ -63,24 +65,9 @@ const PaymentStatus: React.FC<PaymentStatusProps> = ({
|
|
|
63
65
|
const applicationId = useAppSelector((state: any) => state?.onboarding?.applicationId);
|
|
64
66
|
const entityId = useAppSelector((state: any) => state?.onboarding?.entityid);
|
|
65
67
|
const providerId = useAppSelector((state: any) => state?.onboarding?.providerId);
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
stopTimer,
|
|
70
|
-
triggerStatusCheck,
|
|
71
|
-
isCheckingStatus,
|
|
72
|
-
} = usePaymentStatusTimer({
|
|
73
|
-
transactionId: finalTransactionId,
|
|
74
|
-
overrides: {
|
|
75
|
-
providerId,
|
|
76
|
-
workflowInstanceId,
|
|
77
|
-
applicationid: applicationId,
|
|
78
|
-
entityid: entityId,
|
|
79
|
-
},
|
|
80
|
-
onStatusUpdate: (nextStatus) => {
|
|
81
|
-
setCurrentStatus(nextStatus);
|
|
82
|
-
},
|
|
83
|
-
});
|
|
68
|
+
const sseXhrRef = useRef<XMLHttpRequest | null>(null);
|
|
69
|
+
const sseLastIndexRef = useRef<number>(0);
|
|
70
|
+
const sseBufferRef = useRef<{ value: string }>({ value: '' });
|
|
84
71
|
|
|
85
72
|
// Payment Retry API
|
|
86
73
|
const [paymentRetry, {
|
|
@@ -117,21 +104,160 @@ const PaymentStatus: React.FC<PaymentStatusProps> = ({
|
|
|
117
104
|
return amount.toLocaleString('en-IN');
|
|
118
105
|
};
|
|
119
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
|
+
|
|
120
116
|
useEffect(() => {
|
|
121
|
-
if (currentStatus
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
stopTimer();
|
|
117
|
+
if (currentStatus !== 'pending' || !applicationId) {
|
|
118
|
+
stopPaymentSSE();
|
|
119
|
+
return;
|
|
125
120
|
}
|
|
126
121
|
|
|
127
|
-
|
|
128
|
-
|
|
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') {
|
|
209
|
+
setCurrentStatus('success');
|
|
210
|
+
stopPaymentSSE();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (status === 'FAILED') {
|
|
215
|
+
setCurrentStatus('failed');
|
|
216
|
+
stopPaymentSSE();
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
// ignore parse errors
|
|
220
|
+
}
|
|
129
221
|
};
|
|
130
|
-
}, [currentStatus, startTimer, stopTimer]);
|
|
131
222
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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();
|
|
249
|
+
|
|
250
|
+
return () => {
|
|
251
|
+
stopPaymentSSE();
|
|
252
|
+
};
|
|
253
|
+
}, [
|
|
254
|
+
applicationId,
|
|
255
|
+
currentStatus,
|
|
256
|
+
entityId,
|
|
257
|
+
providerId,
|
|
258
|
+
stopPaymentSSE,
|
|
259
|
+
workflowInstanceId
|
|
260
|
+
]);
|
|
135
261
|
|
|
136
262
|
// Handle payment retry when status is failed
|
|
137
263
|
const handlePaymentRetry = async () => {
|
|
@@ -181,9 +307,6 @@ const PaymentStatus: React.FC<PaymentStatusProps> = ({
|
|
|
181
307
|
// Get user info from app data
|
|
182
308
|
const userInfo = getUserInfoForAPI();
|
|
183
309
|
|
|
184
|
-
// Get API configuration
|
|
185
|
-
const apiConfig = getApiConfig();
|
|
186
|
-
|
|
187
310
|
// Prepare request payload
|
|
188
311
|
const requestPayload = {
|
|
189
312
|
userReferenceId: userInfo.id
|
|
@@ -412,13 +535,6 @@ const PaymentStatus: React.FC<PaymentStatusProps> = ({
|
|
|
412
535
|
disabled={isLoadingPaymentRetry}
|
|
413
536
|
/>
|
|
414
537
|
)}
|
|
415
|
-
{/* {currentStatus === 'pending' && (
|
|
416
|
-
<ActionButton
|
|
417
|
-
title={isCheckingStatus ? COMMON_STRINGS.CHECKING : BANK_STRINGS.REFRESH_STATUS_BUTTON}
|
|
418
|
-
onPress={handlePaymentReverseFeed}
|
|
419
|
-
disabled={isCheckingStatus}
|
|
420
|
-
/>
|
|
421
|
-
)} */}
|
|
422
538
|
</View>
|
|
423
539
|
</View>
|
|
424
540
|
</SafeAreaWrapper>
|
|
@@ -545,4 +661,3 @@ const createStyles = (colors: any, typography: any, status: 'success' | 'failed'
|
|
|
545
661
|
};
|
|
546
662
|
|
|
547
663
|
export default PaymentStatus;
|
|
548
|
-
|