@finspringinnovations/fdsdk 0.0.1 → 0.0.3
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/components/CompanyHeader.js +40 -36
- package/lib/components/InterestRateCard.js +3 -2
- package/lib/components/PaymentDetailsCard.js +8 -7
- package/lib/components/PendingFDBottomSheet.js +2 -2
- package/lib/components/TextFieldWithLabel.js +1 -1
- package/lib/components/TrustBox.js +2 -2
- package/lib/config/appDataConfig.d.ts +53 -2
- package/lib/config/appDataConfig.js +39 -6
- package/lib/hooks/usePaymentSSE.d.ts +24 -0
- package/lib/hooks/usePaymentSSE.js +135 -0
- package/lib/hooks/usePaymentStatusTimer.d.ts +25 -0
- package/lib/hooks/usePaymentStatusTimer.js +122 -0
- package/lib/index.d.ts +2 -2
- package/lib/index.js +5 -2
- package/lib/navigation/RootNavigator.js +6 -1
- package/lib/navigation/index.d.ts +2 -0
- package/lib/navigation/index.js +13 -11
- package/lib/screens/FDCalculator.d.ts +1 -0
- package/lib/screens/FDCalculator.js +64 -48
- package/lib/screens/FDList.js +10 -12
- package/lib/screens/NomineeDetail.js +21 -15
- package/lib/screens/PayNow.js +6 -6
- package/lib/screens/Payment.d.ts +1 -0
- package/lib/screens/Payment.js +34 -13
- package/lib/screens/PaymentStatus.js +33 -42
- package/lib/theme/ThemeContext.d.ts +2 -0
- package/lib/theme/ThemeContext.js +4 -2
- package/lib/theme/index.d.ts +6 -1
- package/lib/theme/index.js +24 -8
- package/lib/utils/sseParser.d.ts +7 -0
- package/lib/utils/sseParser.js +27 -0
- package/package.json +2 -2
- package/src/components/CompanyHeader.tsx +50 -44
- package/src/components/InterestRateCard.tsx +3 -2
- package/src/components/PaymentDetailsCard.tsx +45 -40
- package/src/components/PendingFDBottomSheet.tsx +2 -2
- package/src/components/TextFieldWithLabel.tsx +1 -1
- package/src/components/TrustBox.tsx +2 -2
- package/src/config/appDataConfig.ts +70 -5
- package/src/hooks/usePaymentSSE.ts +155 -0
- package/src/hooks/usePaymentStatusTimer.ts +169 -0
- package/src/index.tsx +4 -1
- package/src/navigation/RootNavigator.tsx +7 -0
- package/src/navigation/index.tsx +16 -17
- package/src/screens/FDCalculator.tsx +64 -40
- package/src/screens/FDList.tsx +11 -11
- package/src/screens/NomineeDetail.tsx +20 -14
- package/src/screens/PayNow.tsx +7 -7
- package/src/screens/Payment.tsx +45 -14
- package/src/screens/PaymentStatus.tsx +44 -57
- package/src/theme/ThemeContext.tsx +6 -1
- package/src/theme/index.ts +30 -8
- package/src/utils/sseParser.ts +40 -0
|
@@ -76,45 +76,50 @@ const PaymentDetailsCard: React.FC<PaymentDetailsCardProps> = ({
|
|
|
76
76
|
);
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
-
const createStyles = (colors: any, typography: any, themeName: string) =>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
79
|
+
const createStyles = (colors: any, typography: any, themeName: string) => {
|
|
80
|
+
const isDark = themeName === 'dark';
|
|
81
|
+
|
|
82
|
+
return StyleSheet.create({
|
|
83
|
+
card: {
|
|
84
|
+
backgroundColor: isDark ? colors.headerBg : 'rgba(0,235,180,0.1)',
|
|
85
|
+
borderRadius: 16,
|
|
86
|
+
padding: 20,
|
|
87
|
+
alignItems: 'flex-start',
|
|
88
|
+
marginHorizontal: 16,
|
|
89
|
+
borderWidth: isDark ? 1 : 0,
|
|
90
|
+
borderColor: isDark ? colors.headerBg + 'AA' : 'transparent',
|
|
91
|
+
},
|
|
92
|
+
companyName: {
|
|
93
|
+
...typography.styles.h3,
|
|
94
|
+
color: isDark ? colors.headerText : colors.text,
|
|
95
|
+
marginBottom: 12,
|
|
96
|
+
},
|
|
97
|
+
companyLine: {
|
|
98
|
+
height: 1,
|
|
99
|
+
backgroundColor: isDark ? colors.headerText + '33' : colors.border + '40',
|
|
100
|
+
marginBottom: 12,
|
|
101
|
+
width: '100%',
|
|
102
|
+
},
|
|
103
|
+
detailsContainer: {
|
|
104
|
+
width: '100%',
|
|
105
|
+
},
|
|
106
|
+
detailRow: {
|
|
107
|
+
flexDirection: 'row',
|
|
108
|
+
justifyContent: 'space-between',
|
|
109
|
+
alignItems: 'center',
|
|
110
|
+
paddingVertical: 8,
|
|
111
|
+
},
|
|
112
|
+
detailLabel: {
|
|
113
|
+
...typography.styles.text12Regular,
|
|
114
|
+
color: isDark ? colors.headerText : colors.text,
|
|
115
|
+
opacity: isDark ? 0.7 : 1,
|
|
116
|
+
},
|
|
117
|
+
detailValue: {
|
|
118
|
+
...typography.styles.text14Medium,
|
|
119
|
+
color: isDark ? colors.headerText : colors.text,
|
|
120
|
+
lineHeight: (typography.styles.text14Medium?.lineHeight ?? typography.styles.text14Medium?.fontSize ?? 14) + 3,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
};
|
|
119
124
|
|
|
120
125
|
export default PaymentDetailsCard;
|
|
@@ -218,7 +218,7 @@ const createStyles = (colors: ColorScheme, typography: any, spacing: any, themeN
|
|
|
218
218
|
},
|
|
219
219
|
continueButton: {
|
|
220
220
|
height: 50,
|
|
221
|
-
backgroundColor: '#007AFF',
|
|
221
|
+
backgroundColor: (colors as any).buttonBackground || (colors as any).headerBg || '#007AFF',
|
|
222
222
|
borderRadius: 25,
|
|
223
223
|
paddingHorizontal: spacing.lg,
|
|
224
224
|
alignItems: 'center',
|
|
@@ -227,7 +227,7 @@ const createStyles = (colors: ColorScheme, typography: any, spacing: any, themeN
|
|
|
227
227
|
},
|
|
228
228
|
continueButtonText: {
|
|
229
229
|
...typography.styles.button,
|
|
230
|
-
color: '
|
|
230
|
+
color: (colors as any).buttonTextColor || (colors as any).headerText || '#FFFFFF',
|
|
231
231
|
fontWeight: '600',
|
|
232
232
|
},
|
|
233
233
|
});
|
|
@@ -50,12 +50,12 @@ const createStyles = (colors: any, typography: any, themeName: string) => StyleS
|
|
|
50
50
|
},
|
|
51
51
|
titleLight: {
|
|
52
52
|
...typography.styles.bodyMedium,
|
|
53
|
-
color:
|
|
53
|
+
color: colors.textLight,
|
|
54
54
|
marginBottom: 0,
|
|
55
55
|
},
|
|
56
56
|
title: {
|
|
57
57
|
...typography.styles.bodyMedium,
|
|
58
|
-
color:
|
|
58
|
+
color: colors.text,
|
|
59
59
|
marginBottom: 0,
|
|
60
60
|
},
|
|
61
61
|
});
|
|
@@ -8,6 +8,36 @@ export interface EnvironmentData {
|
|
|
8
8
|
enableLogging?: boolean;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Custom color overrides that can be passed during SDK initialization.
|
|
13
|
+
* Any color defined here will override the corresponding color from the selected theme.
|
|
14
|
+
*/
|
|
15
|
+
export interface CustomColors {
|
|
16
|
+
primary?: string;
|
|
17
|
+
tabSelected?: string;
|
|
18
|
+
headerBg?: string;
|
|
19
|
+
headerText?: string;
|
|
20
|
+
success?: string;
|
|
21
|
+
textSecondary?: string;
|
|
22
|
+
border?: string;
|
|
23
|
+
shadow?: string;
|
|
24
|
+
background?: string;
|
|
25
|
+
surface?: string;
|
|
26
|
+
text?: string;
|
|
27
|
+
textLight?: string;
|
|
28
|
+
error?: string;
|
|
29
|
+
warning?: string;
|
|
30
|
+
info?: string;
|
|
31
|
+
muted?: string;
|
|
32
|
+
inputBackground?: string;
|
|
33
|
+
inputBorder?: string;
|
|
34
|
+
placeholderColor?: string;
|
|
35
|
+
labelColor?: string;
|
|
36
|
+
buttonBackground?: string;
|
|
37
|
+
buttonTextColor?: string;
|
|
38
|
+
cancelButtonBg?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
11
41
|
export interface AppData {
|
|
12
42
|
// Required user identification fields
|
|
13
43
|
id: string; // Required: User ID from main app
|
|
@@ -16,8 +46,7 @@ export interface AppData {
|
|
|
16
46
|
gender: string; // Required: Male, Female, Other
|
|
17
47
|
mobNo: string; // Required: Mobile number (10 digits)
|
|
18
48
|
email: string; // Required: Email address
|
|
19
|
-
panNumber
|
|
20
|
-
|
|
49
|
+
panNumber?: string; // <-- change from required to optional
|
|
21
50
|
// Optional fields - can be collected in SDK if not provided
|
|
22
51
|
address?: string;
|
|
23
52
|
area?: string;
|
|
@@ -32,6 +61,7 @@ export interface AppData {
|
|
|
32
61
|
maritalStatus?: string; // Optional: Single, Married, Divorced, Widowed
|
|
33
62
|
userReferenceId?: string; // Optional: User reference ID from main app
|
|
34
63
|
eventNotifyUrl?: string; // Optional webhook/callback URL for SDK events
|
|
64
|
+
startFDAlertMessage?: string; // ADD THIS LINE
|
|
35
65
|
}
|
|
36
66
|
|
|
37
67
|
// Global app data storage
|
|
@@ -40,6 +70,9 @@ let globalAppData: AppData | null = null;
|
|
|
40
70
|
// Global environment data storage
|
|
41
71
|
let globalEnvironmentData: EnvironmentData | null = null;
|
|
42
72
|
|
|
73
|
+
// Global custom color overrides
|
|
74
|
+
let globalCustomColors: CustomColors | null = null;
|
|
75
|
+
|
|
43
76
|
/**
|
|
44
77
|
* Initialize the SDK with app data from the main application
|
|
45
78
|
* This should be called when the SDK starts
|
|
@@ -53,7 +86,7 @@ export const initializeSDK = (appData: AppData, onValidationError?: (errors: str
|
|
|
53
86
|
{ field: 'gender', label: 'Gender' },
|
|
54
87
|
{ field: 'mobNo', label: 'Mobile Number' },
|
|
55
88
|
{ field: 'email', label: 'Email' },
|
|
56
|
-
{ field: 'panNumber', label: 'PAN Number' },
|
|
89
|
+
// { field: 'panNumber', label: 'PAN Number' },
|
|
57
90
|
];
|
|
58
91
|
|
|
59
92
|
const missingFields: string[] = [];
|
|
@@ -227,14 +260,15 @@ export const validateAppData = (appData: AppData): { isValid: boolean; errors: s
|
|
|
227
260
|
if (!appData.gender) errors.push('Gender is required');
|
|
228
261
|
if (!appData.mobNo?.trim()) errors.push('Mobile number is required');
|
|
229
262
|
if (!appData.email?.trim()) errors.push('Email is required');
|
|
230
|
-
|
|
263
|
+
// PAN is optional — validated later on FD Calculator screen
|
|
264
|
+
// if (!appData.panNumber?.trim()) errors.push('PAN number is required');
|
|
231
265
|
|
|
232
266
|
// Format validation (only if fields are provided)
|
|
233
267
|
if (appData.dob && !/^\d{4}-\d{2}-\d{2}$/.test(appData.dob)) {
|
|
234
268
|
errors.push('Date of birth must be in YYYY-MM-DD format');
|
|
235
269
|
}
|
|
236
270
|
|
|
237
|
-
if (appData.panNumber && !/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/.test(appData.panNumber)) {
|
|
271
|
+
if (appData.panNumber && appData.panNumber.trim() && !/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/.test(appData.panNumber)) {
|
|
238
272
|
errors.push('PAN number must be in valid format (e.g., ABCDE1234F)');
|
|
239
273
|
}
|
|
240
274
|
|
|
@@ -256,6 +290,36 @@ export const validateAppData = (appData: AppData): { isValid: boolean; errors: s
|
|
|
256
290
|
};
|
|
257
291
|
};
|
|
258
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Set custom color overrides for the SDK theme.
|
|
295
|
+
* Colors defined here will override the corresponding color from the selected theme (primary/dark/corporate).
|
|
296
|
+
* Call this before rendering the SDK navigator.
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* setSDKColors({
|
|
300
|
+
* primary: '#FF5722',
|
|
301
|
+
* headerBg: '#1976D2',
|
|
302
|
+
* buttonBackground: '#1976D2',
|
|
303
|
+
* });
|
|
304
|
+
*/
|
|
305
|
+
export const setSDKColors = (customColors: CustomColors): void => {
|
|
306
|
+
globalCustomColors = customColors;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Get the current custom color overrides
|
|
311
|
+
*/
|
|
312
|
+
export const getSDKColors = (): CustomColors | null => {
|
|
313
|
+
return globalCustomColors;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Clear custom color overrides
|
|
318
|
+
*/
|
|
319
|
+
export const clearSDKColors = (): void => {
|
|
320
|
+
globalCustomColors = null;
|
|
321
|
+
};
|
|
322
|
+
|
|
259
323
|
/**
|
|
260
324
|
* Clear app data (for testing or logout scenarios)
|
|
261
325
|
*/
|
|
@@ -276,4 +340,5 @@ export const clearEnvironmentData = (): void => {
|
|
|
276
340
|
export const clearAllData = (): void => {
|
|
277
341
|
globalAppData = null;
|
|
278
342
|
globalEnvironmentData = null;
|
|
343
|
+
globalCustomColors = null;
|
|
279
344
|
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePaymentSSE - React Native SSE hook for payment status tracking.
|
|
3
|
+
*
|
|
4
|
+
* Uses XMLHttpRequest instead of fetch streams because
|
|
5
|
+
* React Native does not support ReadableStream / getReader().
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { start, stop } = usePaymentSSE();
|
|
9
|
+
* start(applicationId, {
|
|
10
|
+
* onPaymentStatus: (status, payload) => { ... },
|
|
11
|
+
* onError: (error) => { ... },
|
|
12
|
+
* });
|
|
13
|
+
* stop(); // to cancel
|
|
14
|
+
*/
|
|
15
|
+
|
|
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';
|
|
20
|
+
|
|
21
|
+
// -- Types --
|
|
22
|
+
|
|
23
|
+
interface UsePaymentSSECallbacks {
|
|
24
|
+
onPaymentStatus: (status: string, payload: Record<string, unknown>) => void;
|
|
25
|
+
onError?: (error: any) => void;
|
|
26
|
+
onConnected?: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// -- Hook --
|
|
30
|
+
|
|
31
|
+
export function usePaymentSSE() {
|
|
32
|
+
// Store the XMLHttpRequest so we can abort it later
|
|
33
|
+
const xhrRef = useRef<XMLHttpRequest | null>(null);
|
|
34
|
+
// Track how much of responseText we've already parsed
|
|
35
|
+
const lastIndexRef = useRef<number>(0);
|
|
36
|
+
// SSE text buffer (persists across onprogress calls)
|
|
37
|
+
const bufferRef = useRef<{ value: string }>({ value: '' });
|
|
38
|
+
|
|
39
|
+
const start = useCallback(
|
|
40
|
+
(applicationId: string, callbacks: UsePaymentSSECallbacks) => {
|
|
41
|
+
// Clean up any existing connection first
|
|
42
|
+
if (xhrRef.current) {
|
|
43
|
+
xhrRef.current.abort();
|
|
44
|
+
xhrRef.current = null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Reset parsing state
|
|
48
|
+
lastIndexRef.current = 0;
|
|
49
|
+
bufferRef.current = { value: '' };
|
|
50
|
+
|
|
51
|
+
// 1. Build the SSE endpoint URL
|
|
52
|
+
const envData = getEnvironmentData();
|
|
53
|
+
const apiConfig = getApiConfig();
|
|
54
|
+
const baseUrl = envData?.apiBaseUrl || apiConfig?.baseUrl || '';
|
|
55
|
+
const url = `${baseUrl}/events?applicationId=${encodeURIComponent(applicationId)}`;
|
|
56
|
+
|
|
57
|
+
// 2. Create XMLHttpRequest
|
|
58
|
+
const xhr = new XMLHttpRequest();
|
|
59
|
+
xhrRef.current = xhr;
|
|
60
|
+
|
|
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);
|
|
67
|
+
}
|
|
68
|
+
xhr.setRequestHeader('Accept', 'text/event-stream');
|
|
69
|
+
xhr.setRequestHeader('Cache-Control', 'no-cache');
|
|
70
|
+
|
|
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;
|
|
77
|
+
|
|
78
|
+
if (!newText) return;
|
|
79
|
+
|
|
80
|
+
// Parse new text into SSE events
|
|
81
|
+
const events = parseSSEBuffer(bufferRef.current, newText);
|
|
82
|
+
|
|
83
|
+
// Handle each event
|
|
84
|
+
for (const { eventType, data } of events) {
|
|
85
|
+
if (eventType === 'fd_payment_status') {
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(data);
|
|
88
|
+
const status = (parsed.paymentStatus ?? '').toUpperCase();
|
|
89
|
+
callbacks.onPaymentStatus(status, parsed);
|
|
90
|
+
} catch (parseError) {
|
|
91
|
+
if (__DEV__) {
|
|
92
|
+
console.error('[usePaymentSSE] Failed to parse event data:', parseError);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
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');
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
if (__DEV__) {
|
|
109
|
+
console.error('[usePaymentSSE] Bad status:', xhr.status);
|
|
110
|
+
}
|
|
111
|
+
callbacks.onError?.(new Error(`SSE response status: ${xhr.status}`));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// 6. Handle errors
|
|
117
|
+
xhr.onerror = () => {
|
|
118
|
+
if (__DEV__) {
|
|
119
|
+
console.error('[usePaymentSSE] Connection error');
|
|
120
|
+
}
|
|
121
|
+
callbacks.onError?.(new Error('SSE connection failed'));
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// 7. Handle timeout
|
|
125
|
+
xhr.ontimeout = () => {
|
|
126
|
+
if (__DEV__) {
|
|
127
|
+
console.error('[usePaymentSSE] Connection timed out');
|
|
128
|
+
}
|
|
129
|
+
callbacks.onError?.(new Error('SSE connection timed out'));
|
|
130
|
+
};
|
|
131
|
+
|
|
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
|
+
}
|
|
138
|
+
},
|
|
139
|
+
[]
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const stop = useCallback(() => {
|
|
143
|
+
if (xhrRef.current) {
|
|
144
|
+
xhrRef.current.abort();
|
|
145
|
+
xhrRef.current = null;
|
|
146
|
+
if (__DEV__) {
|
|
147
|
+
console.log('[usePaymentSSE] SSE connection stopped');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
lastIndexRef.current = 0;
|
|
151
|
+
bufferRef.current = { value: '' };
|
|
152
|
+
}, []);
|
|
153
|
+
|
|
154
|
+
return { start, stop };
|
|
155
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import { usePaymentReverseFeedMutation } from '../api/fdApi';
|
|
3
|
+
import { useAppSelector } from '../store';
|
|
4
|
+
import { getUserInfoForAPI } from '../config/appDataConfig';
|
|
5
|
+
import { getPaymentSession } from '../state/paymentSession';
|
|
6
|
+
|
|
7
|
+
export type PaymentStatus = 'success' | 'failed' | 'pending';
|
|
8
|
+
|
|
9
|
+
interface ReverseFeedOverrides {
|
|
10
|
+
providerId?: string;
|
|
11
|
+
workflowInstanceId?: string;
|
|
12
|
+
userreferenceid?: string;
|
|
13
|
+
applicationid?: string;
|
|
14
|
+
entityid?: string;
|
|
15
|
+
transactionId?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface UsePaymentStatusTimerConfig {
|
|
19
|
+
transactionId?: string;
|
|
20
|
+
overrides?: ReverseFeedOverrides;
|
|
21
|
+
initialDelayMs?: number; // default 60_000
|
|
22
|
+
repeatDelayMs?: number; // default 15_000
|
|
23
|
+
maxDurationMs?: number; // default 15 * 60_000
|
|
24
|
+
onStatusUpdate?: (status: PaymentStatus, response?: any) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const usePaymentStatusTimer = ({
|
|
28
|
+
transactionId,
|
|
29
|
+
overrides,
|
|
30
|
+
initialDelayMs = 60_000,
|
|
31
|
+
repeatDelayMs = 15_000,
|
|
32
|
+
maxDurationMs = 15 * 60_000,
|
|
33
|
+
onStatusUpdate,
|
|
34
|
+
}: UsePaymentStatusTimerConfig) => {
|
|
35
|
+
const workflowInstanceId = useAppSelector((state: any) => state?.onboarding?.workflowInstanceId);
|
|
36
|
+
const applicationId = useAppSelector((state: any) => state?.onboarding?.applicationId);
|
|
37
|
+
const entityId = useAppSelector((state: any) => state?.onboarding?.entityid);
|
|
38
|
+
const providerId = useAppSelector((state: any) => state?.onboarding?.providerId);
|
|
39
|
+
|
|
40
|
+
const { transactionId: sessionTransactionId } = getPaymentSession();
|
|
41
|
+
const userInfo = getUserInfoForAPI();
|
|
42
|
+
|
|
43
|
+
const resolvedTransactionId = overrides?.transactionId || transactionId || sessionTransactionId || '';
|
|
44
|
+
|
|
45
|
+
const requestPayload = useMemo(() => ({
|
|
46
|
+
providerId: overrides?.providerId || providerId,
|
|
47
|
+
workflowInstanceId: overrides?.workflowInstanceId || workflowInstanceId,
|
|
48
|
+
userreferenceid: overrides?.userreferenceid || userInfo?.id,
|
|
49
|
+
applicationid: overrides?.applicationid || applicationId,
|
|
50
|
+
entityid: overrides?.entityid || entityId,
|
|
51
|
+
transactionId: resolvedTransactionId,
|
|
52
|
+
}), [
|
|
53
|
+
applicationId,
|
|
54
|
+
entityId,
|
|
55
|
+
overrides?.applicationid,
|
|
56
|
+
overrides?.entityid,
|
|
57
|
+
overrides?.providerId,
|
|
58
|
+
overrides?.transactionId,
|
|
59
|
+
overrides?.userreferenceid,
|
|
60
|
+
overrides?.workflowInstanceId,
|
|
61
|
+
providerId,
|
|
62
|
+
resolvedTransactionId,
|
|
63
|
+
userInfo?.id,
|
|
64
|
+
workflowInstanceId,
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
const [paymentReverseFeed, { isLoading: isCheckingStatus }] = usePaymentReverseFeedMutation();
|
|
68
|
+
|
|
69
|
+
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
70
|
+
const startTimestampRef = useRef(0);
|
|
71
|
+
const isRunningRef = useRef(false);
|
|
72
|
+
const latestStatusRef = useRef<PaymentStatus>('pending');
|
|
73
|
+
|
|
74
|
+
const clearTimer = useCallback(() => {
|
|
75
|
+
if (timeoutRef.current) {
|
|
76
|
+
clearTimeout(timeoutRef.current);
|
|
77
|
+
timeoutRef.current = null;
|
|
78
|
+
}
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
const stopTimer = useCallback(() => {
|
|
82
|
+
isRunningRef.current = false;
|
|
83
|
+
clearTimer();
|
|
84
|
+
}, [clearTimer]);
|
|
85
|
+
|
|
86
|
+
const triggerStatusCheck = useCallback(async () => {
|
|
87
|
+
if (!requestPayload.transactionId) {
|
|
88
|
+
return latestStatusRef.current;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const response = await paymentReverseFeed(requestPayload).unwrap();
|
|
93
|
+
|
|
94
|
+
const status = (response?.data?.paymentStatus || '').toUpperCase();
|
|
95
|
+
|
|
96
|
+
if (status === 'SUCCESS') {
|
|
97
|
+
latestStatusRef.current = 'success';
|
|
98
|
+
onStatusUpdate?.('success', response);
|
|
99
|
+
return 'success';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (status === 'FAILED') {
|
|
103
|
+
latestStatusRef.current = 'failed';
|
|
104
|
+
onStatusUpdate?.('failed', response);
|
|
105
|
+
return 'failed';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// IN_PROGRESS OR ANY OTHER STATUS → do nothing
|
|
109
|
+
latestStatusRef.current = 'pending';
|
|
110
|
+
return 'pending';
|
|
111
|
+
|
|
112
|
+
} catch (error) {
|
|
113
|
+
// // ANY error → Pending
|
|
114
|
+
// latestStatusRef.current = 'pending';
|
|
115
|
+
|
|
116
|
+
// onStatusUpdate?.('pending', {
|
|
117
|
+
// status: 'pending',
|
|
118
|
+
// message: 'Reverse feed API failed or backend unreachable',
|
|
119
|
+
// error
|
|
120
|
+
// });
|
|
121
|
+
|
|
122
|
+
// return 'pending';
|
|
123
|
+
}
|
|
124
|
+
}, [onStatusUpdate, paymentReverseFeed, requestPayload]);
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
const scheduleNext = useCallback((delay: number) => {
|
|
128
|
+
clearTimer();
|
|
129
|
+
timeoutRef.current = setTimeout(async () => {
|
|
130
|
+
if (!isRunningRef.current) {
|
|
131
|
+
clearTimer();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (Date.now() - startTimestampRef.current >= maxDurationMs) {
|
|
136
|
+
stopTimer();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const status = await triggerStatusCheck();
|
|
141
|
+
|
|
142
|
+
if (status === 'success' || status === 'failed') {
|
|
143
|
+
stopTimer();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
scheduleNext(repeatDelayMs);
|
|
148
|
+
}, delay);
|
|
149
|
+
}, [clearTimer, maxDurationMs, repeatDelayMs, stopTimer, triggerStatusCheck]);
|
|
150
|
+
|
|
151
|
+
const startTimer = useCallback(() => {
|
|
152
|
+
if (isRunningRef.current) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
isRunningRef.current = true;
|
|
156
|
+
startTimestampRef.current = Date.now();
|
|
157
|
+
scheduleNext(initialDelayMs);
|
|
158
|
+
}, [initialDelayMs, scheduleNext]);
|
|
159
|
+
|
|
160
|
+
useEffect(() => stopTimer, [stopTimer]);
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
startTimer,
|
|
164
|
+
stopTimer,
|
|
165
|
+
triggerStatusCheck,
|
|
166
|
+
isCheckingStatus,
|
|
167
|
+
latestStatusRef,
|
|
168
|
+
};
|
|
169
|
+
};
|
package/src/index.tsx
CHANGED
|
@@ -92,8 +92,11 @@ export {
|
|
|
92
92
|
isEnvironmentInitialized,
|
|
93
93
|
clearEnvironmentData,
|
|
94
94
|
clearAllData,
|
|
95
|
+
setSDKColors,
|
|
96
|
+
getSDKColors,
|
|
97
|
+
clearSDKColors,
|
|
95
98
|
} from './config/appDataConfig';
|
|
96
|
-
export type { AppData, EnvironmentData } from './config/appDataConfig';
|
|
99
|
+
export type { AppData, EnvironmentData, CustomColors } from './config/appDataConfig';
|
|
97
100
|
|
|
98
101
|
// Export validation utilities
|
|
99
102
|
export { showValidationErrorAlert, validateAndProceed } from './components/ValidationErrorAlert';
|
|
@@ -150,6 +150,7 @@ const RootNavigator: React.FC<RootNavigatorProps> = ({
|
|
|
150
150
|
{(props) => (
|
|
151
151
|
<FDCalculatorScreen
|
|
152
152
|
onGoBack={() => goBack()}
|
|
153
|
+
onExitSDK={() => onExit?.()}
|
|
153
154
|
onNavigateToReviewKYC={() => navigate('ReviewKYC', { fdData: props.route.params?.fdData })}
|
|
154
155
|
fdData={(props.route.params as any)?.fdData}
|
|
155
156
|
{...props}
|
|
@@ -319,6 +320,12 @@ const RootNavigator: React.FC<RootNavigatorProps> = ({
|
|
|
319
320
|
paymentData: error
|
|
320
321
|
} as any);
|
|
321
322
|
}}
|
|
323
|
+
onPaymentPending={(info) => {
|
|
324
|
+
navigate('PaymentStatus', {
|
|
325
|
+
status: 'pending',
|
|
326
|
+
paymentData: info
|
|
327
|
+
} as any);
|
|
328
|
+
}}
|
|
322
329
|
{...props}
|
|
323
330
|
/>
|
|
324
331
|
)}
|