saafe-redirection-flow 2.0.0 → 2.1.0
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/CHANGELOG.md +16 -0
- package/package.json +1 -1
- package/src/components/alert/alert.tsx +10 -4
- package/src/components/modal/HelpModal.tsx +122 -0
- package/src/components/session/SessionTimer.tsx +20 -33
- package/src/components/ui/bottom-sheet.tsx +1 -1
- package/src/components/ui/frosted-panel.tsx +0 -2
- package/src/components/ui/otp-input.tsx +5 -3
- package/src/hooks/use-account-discovery.ts +13 -6
- package/src/hooks/use-auto-discovery.ts +226 -0
- package/src/index.css +3 -0
- package/src/pages/accounts/AccountsToProceed.tsx +283 -20
- package/src/pages/accounts/Discover.tsx +127 -9
- package/src/pages/accounts/DiscoverAccount.tsx +176 -38
- package/src/pages/accounts/LinkSelectedAccounts.tsx +15 -9
- package/src/pages/accounts/OldUser.tsx +192 -40
- package/src/pages/accounts/link-accounts.tsx +19 -11
- package/src/pages/consent/ReviewConsent.tsx +18 -10
- package/src/pages/consent/rejected.tsx +17 -8
- package/src/pages/consent/success.tsx +125 -76
- package/src/store/fip.store.ts +74 -28
- package/src/store/redirect.store.ts +13 -0
- package/src/utils/auto-discovery.ts +126 -0
- package/stage-aa-0407251720.zip +0 -0
package/CHANGELOG.md
CHANGED
@@ -1,3 +1,19 @@
|
|
1
|
+
# [2.1.0](https://gitlab.com/Networth360/saafe-redirection/compare/v2.0.0...v2.1.0) (2025-07-04)
|
2
|
+
|
3
|
+
|
4
|
+
### Bug Fixes
|
5
|
+
|
6
|
+
* link signature and auto discovery ([5953384](https://gitlab.com/Networth360/saafe-redirection/commit/5953384080386c4bf29aa9b1a74bad87b31443c9))
|
7
|
+
* merger branch conflict ([b524051](https://gitlab.com/Networth360/saafe-redirection/commit/b524051f4590521933e61835aaa37105ef472bb1))
|
8
|
+
* mobile view accepted screen and help greveance ([88dd323](https://gitlab.com/Networth360/saafe-redirection/commit/88dd323530c18bcc665c0a0bd8259eef1f201665))
|
9
|
+
* otp input eye placement ([2206d3a](https://gitlab.com/Networth360/saafe-redirection/commit/2206d3ab63897f6ebfe4240b81a14781433f55c8))
|
10
|
+
|
11
|
+
|
12
|
+
### Features
|
13
|
+
|
14
|
+
* customer support info added ([b8842bb](https://gitlab.com/Networth360/saafe-redirection/commit/b8842bb6df8a95c2d4a29f4f861d7b0241726b7a))
|
15
|
+
* **Success:** handled the rejected consent in approved screen ([d3e483e](https://gitlab.com/Networth360/saafe-redirection/commit/d3e483e1a436e62385abefb408f3fcb9dcbbf399))
|
16
|
+
|
1
17
|
# [2.0.0](https://gitlab.com/Networth360/saafe-redirection/compare/v1.1.0...v2.0.0) (2025-07-02)
|
2
18
|
|
3
19
|
|
package/package.json
CHANGED
@@ -3,13 +3,19 @@ interface AlertInterface {
|
|
3
3
|
title?: string
|
4
4
|
description?: string
|
5
5
|
rightSection?: React.ReactNode
|
6
|
+
type?: 'error' | 'warning'
|
6
7
|
}
|
7
8
|
|
8
|
-
const
|
9
|
+
const colors = {
|
10
|
+
warning: 'bg-orange-100/50 dark:bg-ring border-orange-400/50 dark:border-orange-400/50 border-1 text-yellow-600 dark:text-yellow-600 p-4 rounded-lg',
|
11
|
+
error: 'bg-red-100/50 dark:bg-warning-primary border-red-400/50 dark:border-red-400/50 border-1 text-gray-800 dark:text-gray-800 p-4 rounded-lg',
|
12
|
+
}
|
13
|
+
|
14
|
+
const AlertComp = ({ icon, title, description, rightSection, type = 'warning' }: AlertInterface) => {
|
9
15
|
return (
|
10
|
-
<div className=
|
16
|
+
<div className={colors[type]}>
|
11
17
|
<div className='flex items-center justify-between'>
|
12
|
-
<div className='flex items-
|
18
|
+
<div className='flex items-center w-full gap-4'>
|
13
19
|
{icon ? icon : null}
|
14
20
|
<div className='flex flex-col gap-1'>
|
15
21
|
{title ? <p className='text-md md:text-lg font-medium'>{title}</p> : null}
|
@@ -20,7 +26,7 @@ const AlertComp = ({ icon, title, description, rightSection }: AlertInterface) =
|
|
20
26
|
rightSection
|
21
27
|
: null}
|
22
28
|
</div>
|
23
|
-
</div>
|
29
|
+
</div >
|
24
30
|
)
|
25
31
|
}
|
26
32
|
|
@@ -0,0 +1,122 @@
|
|
1
|
+
import { useState } from "react";
|
2
|
+
import { useTranslation } from "react-i18next";
|
3
|
+
import { Button } from "../ui/button";
|
4
|
+
import { DialogFooter } from "../ui/dialog";
|
5
|
+
import { DialogContent } from "../ui/dialog";
|
6
|
+
import { Dialog } from "../ui/dialog";
|
7
|
+
import { DialogHeader, DialogTitle } from "../ui/dialog";
|
8
|
+
import { CircleHelp } from "lucide-react";
|
9
|
+
|
10
|
+
interface HelpModalProps {
|
11
|
+
buttonClassName?: string;
|
12
|
+
iconSize?: number;
|
13
|
+
textSize?: string;
|
14
|
+
variant?: "light" | "dark";
|
15
|
+
}
|
16
|
+
|
17
|
+
export const HelpModal = ({
|
18
|
+
buttonClassName = "",
|
19
|
+
iconSize = 14,
|
20
|
+
textSize = "text-sm",
|
21
|
+
variant = "dark"
|
22
|
+
}: HelpModalProps) => {
|
23
|
+
const [showHelpModal, setShowHelpModal] = useState(false);
|
24
|
+
const { t } = useTranslation();
|
25
|
+
|
26
|
+
const buttonStyles = variant === "light"
|
27
|
+
? "text-white/80 hover:text-white"
|
28
|
+
: "text-muted-foreground hover:text-foreground";
|
29
|
+
|
30
|
+
return (
|
31
|
+
<>
|
32
|
+
<button
|
33
|
+
onClick={() => setShowHelpModal(true)}
|
34
|
+
className={`flex items-center gap-1 cursor-pointer transition-colors ${buttonStyles} ${buttonClassName}`}
|
35
|
+
>
|
36
|
+
<CircleHelp size={iconSize} />
|
37
|
+
<span className={`${textSize} mt-0.5`}>
|
38
|
+
{t("session.help")}
|
39
|
+
</span>
|
40
|
+
</button>
|
41
|
+
|
42
|
+
{/* Help Modal */}
|
43
|
+
<Dialog open={showHelpModal} onOpenChange={setShowHelpModal}>
|
44
|
+
<DialogContent className="max-w-4xl w-[90vw] max-h-[85vh] overflow-y-auto my-4 fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
|
45
|
+
<DialogHeader>
|
46
|
+
<DialogTitle className="text-left text-xl md:text-2xl font-semibold dark:text-white">
|
47
|
+
Customer Support & Complaint Redressal
|
48
|
+
</DialogTitle>
|
49
|
+
</DialogHeader>
|
50
|
+
<div className="space-y-6 text-sm md:text-base pr-2">
|
51
|
+
<p className="text-muted-foreground leading-relaxed">
|
52
|
+
The Customers who have any Complaint, can follow the following process for its redressal:
|
53
|
+
</p>
|
54
|
+
|
55
|
+
<div className="space-y-5">
|
56
|
+
<div>
|
57
|
+
<p className="font-semibold mb-3 text-foreground">Register the Complaint:</p>
|
58
|
+
<p className="text-muted-foreground mb-3 leading-relaxed">
|
59
|
+
Register the Complaint in a complaint register / complaint box, which is available at the corporate office of the Company following address:
|
60
|
+
</p>
|
61
|
+
<div className="mt-2 p-4 bg-card border border-border rounded-lg shadow-sm">
|
62
|
+
<p className="font-semibold text-foreground">Dashboard Account Aggregation Services Private Limited</p>
|
63
|
+
<p className="text-muted-foreground">Workafella, New No. 431, Teynampet,</p>
|
64
|
+
<p className="text-muted-foreground">Anna Salai Chennai – 600018</p>
|
65
|
+
</div>
|
66
|
+
</div>
|
67
|
+
|
68
|
+
<div>
|
69
|
+
<p className="font-semibold mb-3 text-foreground">Email:</p>
|
70
|
+
<div className="p-4 bg-card border border-border rounded-lg shadow-sm">
|
71
|
+
<a href="mailto:general@saafe.in" className="text-primary hover:underline font-medium">
|
72
|
+
general@saafe.in
|
73
|
+
</a>
|
74
|
+
</div>
|
75
|
+
</div>
|
76
|
+
|
77
|
+
<div>
|
78
|
+
<p className="font-semibold mb-3 text-foreground">Write to the Company:</p>
|
79
|
+
<div className="p-4 bg-card border border-border rounded-lg shadow-sm">
|
80
|
+
<p className="font-semibold text-foreground mb-1">Kind Attention: Customer Service Team</p>
|
81
|
+
<p className="text-muted-foreground">Dashboard Account Aggregation Services Private Limited</p>
|
82
|
+
<p className="text-muted-foreground">Suite 422, Workafella, New No. 431, Teynampet,</p>
|
83
|
+
<p className="text-muted-foreground">Anna Salai Chennai – 600018</p>
|
84
|
+
</div>
|
85
|
+
</div>
|
86
|
+
|
87
|
+
<div className="border-t border-border pt-5">
|
88
|
+
<p className="font-semibold mb-3 text-amber-600 dark:text-amber-400">Escalation:</p>
|
89
|
+
<p className="text-muted-foreground mb-4 leading-relaxed">
|
90
|
+
If the customer's query or complaint is not resolved within a period of one month from date of complaint the customer may also approach the RBI Ombudsman / Regional Office of Dept. of Supervision – RBI:
|
91
|
+
</p>
|
92
|
+
<div className="p-4 bg-amber-50/80 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800 rounded-lg">
|
93
|
+
<p className="font-semibold text-foreground">Officer-in-Charge</p>
|
94
|
+
<p className="text-muted-foreground">Department of Supervision</p>
|
95
|
+
<p className="text-muted-foreground">Regional Office – Chennai</p>
|
96
|
+
<p className="text-muted-foreground">Reserve Bank of India</p>
|
97
|
+
<p className="text-muted-foreground">Fort Glacis, No.16, Rajaji Salai</p>
|
98
|
+
<p className="text-muted-foreground">Chennai 600 001</p>
|
99
|
+
<p className="mt-3">
|
100
|
+
<span className="font-semibold text-foreground">Tel:</span>
|
101
|
+
<a href="tel:044-25399189" className="text-primary hover:underline ml-2 font-medium">
|
102
|
+
044-2539 9189
|
103
|
+
</a>
|
104
|
+
</p>
|
105
|
+
</div>
|
106
|
+
</div>
|
107
|
+
</div>
|
108
|
+
</div>
|
109
|
+
<DialogFooter className="mt-6">
|
110
|
+
<Button
|
111
|
+
onClick={() => setShowHelpModal(false)}
|
112
|
+
variant="default"
|
113
|
+
className="w-full md:w-auto px-8"
|
114
|
+
>
|
115
|
+
Close
|
116
|
+
</Button>
|
117
|
+
</DialogFooter>
|
118
|
+
</DialogContent>
|
119
|
+
</Dialog>
|
120
|
+
</>
|
121
|
+
);
|
122
|
+
};
|
@@ -4,10 +4,10 @@ import { Button } from "../ui/button";
|
|
4
4
|
import { DialogFooter } from "../ui/dialog";
|
5
5
|
import { DialogContent } from "../ui/dialog";
|
6
6
|
import { Dialog } from "../ui/dialog";
|
7
|
-
import {
|
7
|
+
import { Clock, Clock4 } from "lucide-react";
|
8
8
|
import { useNavigationBlock } from "@/store/NavigationBlockContext";
|
9
9
|
import { SESSION } from "@/config/urls";
|
10
|
-
import {
|
10
|
+
import { HelpModal } from "../modal/HelpModal";
|
11
11
|
|
12
12
|
export const SessionTimer = ({ onTimeout, mobile = false }: { onTimeout: () => void, mobile?: boolean }) => {
|
13
13
|
const [timeLeft, setTimeLeft] = useState<number>(30 * 1000); // Default to 30 seconds initially
|
@@ -42,7 +42,7 @@ export const SessionTimer = ({ onTimeout, mobile = false }: { onTimeout: () => v
|
|
42
42
|
// Save the end time in session storage
|
43
43
|
const endTime = Date.now() + sessionTimeout;
|
44
44
|
sessionStorage.setItem(SESSION_TIMER_KEY, JSON.stringify({ endTime }));
|
45
|
-
} catch
|
45
|
+
} catch {
|
46
46
|
setTimeLeft(30 * 1000);
|
47
47
|
}
|
48
48
|
}, []);
|
@@ -106,21 +106,12 @@ export const SessionTimer = ({ onTimeout, mobile = false }: { onTimeout: () => v
|
|
106
106
|
<Clock4 size={12} className="mt-[1px]" />
|
107
107
|
<span className={`font-medium ${mobile ? "text-xs" : ""}`}>{formatTime(timeLeft)}</span>
|
108
108
|
</div>
|
109
|
-
<
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
</span>
|
116
|
-
</div>
|
117
|
-
</PopoverTrigger>
|
118
|
-
<PopoverContent className="w-68">
|
119
|
-
<div className="text-xs text-muted-secondary">
|
120
|
-
For any questions or concerns, please report it to our Customer Grievance Redressal Officer (<a href="https://saafe.in/contact/" target="_blank" rel="noopener noreferrer"><span className="text-primary cursor-pointer">https://saafe.in/contact/</span></a>) - AA
|
121
|
-
</div>
|
122
|
-
</PopoverContent>
|
123
|
-
</Popover>
|
109
|
+
<HelpModal
|
110
|
+
variant="dark"
|
111
|
+
iconSize={12}
|
112
|
+
textSize="text-xs"
|
113
|
+
buttonClassName="text-left"
|
114
|
+
/>
|
124
115
|
</div>
|
125
116
|
) : (
|
126
117
|
<div className="flex justify-between gap-1.5">
|
@@ -128,24 +119,20 @@ export const SessionTimer = ({ onTimeout, mobile = false }: { onTimeout: () => v
|
|
128
119
|
<span className="font-light text-sm">{t("session.timeRemaining")}:</span>
|
129
120
|
<span className={`font-medium ${mobile ? "text-sm" : ""}`}>{formatTime(timeLeft)}</span>
|
130
121
|
</div>
|
131
|
-
<
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
</
|
140
|
-
|
141
|
-
<div className="text-sm text-muted-secondary">
|
142
|
-
For any questions or concerns, please report it to our Customer Grievance Redressal Officer (<a href="https://saafe.in/contact/" target="_blank" rel="noopener noreferrer"><span className="text-primary cursor-pointer">https://saafe.in/contact/</span></a>) - AA
|
143
|
-
</div>
|
144
|
-
</PopoverContent>
|
145
|
-
</Popover>
|
122
|
+
<HelpModal />
|
123
|
+
{/* <button
|
124
|
+
onClick={() => setShowHelpModal(true)}
|
125
|
+
className="flex items-center gap-1 cursor-pointer"
|
126
|
+
>
|
127
|
+
<CircleHelp size={14} />
|
128
|
+
<span className="text-sm mt-0.5">
|
129
|
+
{t("session.help")}
|
130
|
+
</span>
|
131
|
+
</button> */}
|
146
132
|
</div>
|
147
133
|
)}
|
148
134
|
|
135
|
+
{/* Session Warning Modal */}
|
149
136
|
<Dialog open={showWarningModal} onOpenChange={setShowWarningModal}>
|
150
137
|
<DialogContent className="text-center">
|
151
138
|
<div className="flex flex-col items-center justify-center gap-2">
|
@@ -92,7 +92,7 @@ export function BottomSheet({
|
|
92
92
|
{/* Header with title and close button */}
|
93
93
|
{(title || showCloseButton) && (
|
94
94
|
<div className="flex items-center justify-between px-4 py-3">
|
95
|
-
{title && <h3 className="font-medium text-lg">{title}</h3>}
|
95
|
+
{title && <h3 className="font-medium text-lg dark:text-white">{title}</h3>}
|
96
96
|
{showCloseButton && (
|
97
97
|
<button
|
98
98
|
onClick={onClose}
|
@@ -118,8 +118,6 @@ function Aside({ children, className, width, ratio, ...props }: AsideProps) {
|
|
118
118
|
const context = useContext(FrostedPanelContext);
|
119
119
|
const { isAuthenticated } = useAuthStore();
|
120
120
|
const { data: trustedCount, isLoading } = useTrustedCount();
|
121
|
-
console.log(trustedCount);
|
122
|
-
|
123
121
|
|
124
122
|
// Handle session timeout - only called when timer actually reaches zero
|
125
123
|
const handleTimeout = () => {
|
@@ -24,7 +24,8 @@ interface OTPInputComponentProps {
|
|
24
24
|
name?: string;
|
25
25
|
editable?: boolean;
|
26
26
|
autoFocus?: boolean;
|
27
|
-
resendLoading?: boolean
|
27
|
+
resendLoading?: boolean;
|
28
|
+
eyeClassName?: string;
|
28
29
|
}
|
29
30
|
|
30
31
|
// Extend SlotProps with our custom isError property
|
@@ -50,7 +51,8 @@ const OTPInputComponent = forwardRef<HTMLDivElement, OTPInputComponentProps>(({
|
|
50
51
|
value = "",
|
51
52
|
onChange,
|
52
53
|
name,
|
53
|
-
autoFocus = true
|
54
|
+
autoFocus = true,
|
55
|
+
eyeClassName
|
54
56
|
}, ref) => {
|
55
57
|
|
56
58
|
const id = useId();
|
@@ -148,7 +150,7 @@ const OTPInputComponent = forwardRef<HTMLDivElement, OTPInputComponentProps>(({
|
|
148
150
|
type="button"
|
149
151
|
variant="link"
|
150
152
|
size="icon"
|
151
|
-
className="absolute top-1/2 -translate-y-1/2 -translate-x-1/2 right-0 sm:right-2 z-10 mt-1"
|
153
|
+
className={cn("absolute top-1/2 -translate-y-1/2 -translate-x-1/2 right-0 sm:right-2 z-10 mt-1", eyeClassName)}
|
152
154
|
onClick={toggleOtpVisibility}
|
153
155
|
>
|
154
156
|
{isOtpVisible ? (
|
@@ -45,6 +45,8 @@ export function useAccountDiscovery() {
|
|
45
45
|
const { decodedInfo } = useRedirectStore()
|
46
46
|
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
47
47
|
|
48
|
+
console.log('Identifiers:', identifiers, 'Active Category:', activeCategory)
|
49
|
+
|
48
50
|
return {
|
49
51
|
...useMutation({
|
50
52
|
mutationFn: async (
|
@@ -79,11 +81,15 @@ export function useAccountDiscovery() {
|
|
79
81
|
fiTypes.push(...fiTypeCategoryMap[activeCategory])
|
80
82
|
}
|
81
83
|
|
82
|
-
const commonIdentifiers = identifiers
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
84
|
+
const commonIdentifiers = identifiers
|
85
|
+
.filter(i => i.value && i.value.trim() !== '')
|
86
|
+
.map(i => ({
|
87
|
+
type: i.type,
|
88
|
+
value: i.value as string,
|
89
|
+
categoryType: i.category
|
90
|
+
}))
|
91
|
+
|
92
|
+
console.log('Common Identifiers:', commonIdentifiers)
|
87
93
|
|
88
94
|
// If multiple FIP IDs, we need to call account discovery for each
|
89
95
|
const accountPromises = fipIds.map(async (fipId) => {
|
@@ -135,7 +141,8 @@ export function useAccountDiscovery() {
|
|
135
141
|
const originalAccounts = successfulResults.map(({ fipId, result }) => ({
|
136
142
|
fipId,
|
137
143
|
fipName: fipId, // We'll need to get the actual name from somewhere
|
138
|
-
DiscoveredAccounts: result.DiscoveredAccounts
|
144
|
+
DiscoveredAccounts: result.DiscoveredAccounts,
|
145
|
+
signature: result.signature
|
139
146
|
}))
|
140
147
|
|
141
148
|
// Process the accounts from successful API responses
|
@@ -0,0 +1,226 @@
|
|
1
|
+
import { useMutation } from '@tanstack/react-query'
|
2
|
+
import { useState } from 'react'
|
3
|
+
import { useFipStore, DiscoveredAccount } from '@/store/fip.store'
|
4
|
+
import { useRedirectStore } from '@/store/redirect.store'
|
5
|
+
import { useAuthStore } from '@/store/auth.store'
|
6
|
+
import { fiTypeCategoryMap } from '@/const/fiTypeCategoryMap'
|
7
|
+
import { accountService } from '@/services/api/account.service'
|
8
|
+
import { ProcessedDiscoveryResult } from './use-account-discovery'
|
9
|
+
import { getAutoDiscoveryFipIds } from '@/utils/auto-discovery'
|
10
|
+
|
11
|
+
/**
|
12
|
+
* Hook for auto discovery - discovers accounts from all available FIPs for a category
|
13
|
+
*/
|
14
|
+
export function useAutoDiscovery() {
|
15
|
+
const { user } = useAuthStore()
|
16
|
+
const { groupedFips } = useFipStore()
|
17
|
+
const { decodedInfo } = useRedirectStore()
|
18
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
19
|
+
const [isLoading, setIsLoading] = useState(false)
|
20
|
+
|
21
|
+
const autoDiscoveryMutation = useMutation({
|
22
|
+
mutationFn: async (category: string): Promise<ProcessedDiscoveryResult> => {
|
23
|
+
if (!user?.phoneNumber) {
|
24
|
+
const error = new Error('Mobile number is not available')
|
25
|
+
setErrorMessage(error.message)
|
26
|
+
throw error
|
27
|
+
}
|
28
|
+
|
29
|
+
if (!decodedInfo?.fiuId) {
|
30
|
+
const error = new Error('FIU ID is not available')
|
31
|
+
setErrorMessage(error.message)
|
32
|
+
throw error
|
33
|
+
}
|
34
|
+
|
35
|
+
try {
|
36
|
+
setIsLoading(true)
|
37
|
+
const fiuId = decodedInfo.fiuId
|
38
|
+
const fiTypes = Array.isArray(decodedInfo.fiTypesRequiredForConsent)
|
39
|
+
? decodedInfo.fiTypesRequiredForConsent.filter(i =>
|
40
|
+
fiTypeCategoryMap[category.toUpperCase()]?.includes(i)
|
41
|
+
)
|
42
|
+
: []
|
43
|
+
|
44
|
+
if (fiTypes.length === 0) {
|
45
|
+
// Fallback to category map if no FI types are provided
|
46
|
+
const categoryTypes = fiTypeCategoryMap[category.toUpperCase()]
|
47
|
+
if (categoryTypes) {
|
48
|
+
fiTypes.push(...categoryTypes)
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
// Get FIP IDs for auto discovery
|
53
|
+
const fipIds = getAutoDiscoveryFipIds(
|
54
|
+
category,
|
55
|
+
decodedInfo.fipId,
|
56
|
+
groupedFips
|
57
|
+
)
|
58
|
+
|
59
|
+
if (fipIds.length === 0) {
|
60
|
+
throw new Error('No FIPs available for auto discovery')
|
61
|
+
}
|
62
|
+
|
63
|
+
// Construct identifiers from redirect store data and FIP requirements
|
64
|
+
const constructIdentifiers = (fipIds: string[]) => {
|
65
|
+
const { decodedInfo } = useRedirectStore.getState()
|
66
|
+
const { fips } = useFipStore.getState()
|
67
|
+
|
68
|
+
return fipIds
|
69
|
+
.flatMap((fipId) => {
|
70
|
+
const fip = fips.find((f) => f.id === fipId)
|
71
|
+
return fip?.Identifiers || []
|
72
|
+
})
|
73
|
+
.reduce<{ type: string; value: string | null; categoryType: string }[]>(
|
74
|
+
(acc, identifier) => {
|
75
|
+
if (!acc.find((i) => i.type === identifier.type)) {
|
76
|
+
let value: string | null = null
|
77
|
+
if (identifier.type === 'PAN') {
|
78
|
+
value = decodedInfo?.pan || null
|
79
|
+
} else if (identifier.type === 'MOBILE') {
|
80
|
+
value = decodedInfo?.phoneNumber || null
|
81
|
+
} else if (identifier.type === 'DOB') {
|
82
|
+
value = decodedInfo?.dob || null
|
83
|
+
} else if (identifier.type === 'AADHAAR') {
|
84
|
+
value = null // AADHAAR not available in decodedInfo
|
85
|
+
}
|
86
|
+
acc.push({
|
87
|
+
type: identifier.type,
|
88
|
+
value,
|
89
|
+
categoryType: identifier.category
|
90
|
+
})
|
91
|
+
}
|
92
|
+
return acc
|
93
|
+
},
|
94
|
+
[]
|
95
|
+
)
|
96
|
+
}
|
97
|
+
|
98
|
+
const commonIdentifiers = constructIdentifiers(fipIds).filter(id => id.value !== null) as { type: string; value: string; categoryType: string; }[]
|
99
|
+
|
100
|
+
// Log the constructed identifiers for debugging
|
101
|
+
console.log('Auto Discovery: Constructed identifiers:', commonIdentifiers)
|
102
|
+
|
103
|
+
// Perform account discovery for all FIPs
|
104
|
+
const accountPromises = fipIds.map(async (fipId) => {
|
105
|
+
return accountService.accountDiscovery({
|
106
|
+
Identifiers: [...commonIdentifiers],
|
107
|
+
FiuId: fiuId,
|
108
|
+
FipId: fipId,
|
109
|
+
FITypes: fiTypes
|
110
|
+
})
|
111
|
+
})
|
112
|
+
|
113
|
+
// Wait for all account discovery calls to complete
|
114
|
+
const results = await Promise.allSettled(accountPromises)
|
115
|
+
|
116
|
+
// Process results
|
117
|
+
const successfulResults: { fipId: string; result: { DiscoveredAccounts: DiscoveredAccount[]; signature: string } }[] = []
|
118
|
+
const failedFips: string[] = []
|
119
|
+
const errorMessages: string[] = []
|
120
|
+
|
121
|
+
results.forEach((settledResult, index) => {
|
122
|
+
const fipId = fipIds[index]
|
123
|
+
if (settledResult.status === 'fulfilled') {
|
124
|
+
successfulResults.push({ fipId, result: settledResult.value })
|
125
|
+
} else {
|
126
|
+
failedFips.push(fipId)
|
127
|
+
const error = settledResult.reason as Error & {
|
128
|
+
response?: {
|
129
|
+
status?: number;
|
130
|
+
data?: { message?: string }
|
131
|
+
}
|
132
|
+
}
|
133
|
+
if (error?.response?.data?.message) {
|
134
|
+
errorMessages.push(`${fipId}: ${error.response.data.message}`)
|
135
|
+
} else {
|
136
|
+
errorMessages.push(`${fipId}: Failed to discover accounts`)
|
137
|
+
}
|
138
|
+
}
|
139
|
+
})
|
140
|
+
|
141
|
+
// If no successful results, throw an error
|
142
|
+
if (successfulResults.length === 0) {
|
143
|
+
const combinedError = errorMessages.join('; ')
|
144
|
+
setErrorMessage(combinedError)
|
145
|
+
throw new Error(combinedError)
|
146
|
+
}
|
147
|
+
|
148
|
+
// Process and merge the accounts from successful FIPs
|
149
|
+
const originalAccounts = successfulResults.map(({ fipId, result }) => ({
|
150
|
+
fipId,
|
151
|
+
fipName: fipId, // We'll get the actual name from the FIP data
|
152
|
+
DiscoveredAccounts: result.DiscoveredAccounts,
|
153
|
+
signature: result.signature
|
154
|
+
}))
|
155
|
+
|
156
|
+
// Process the accounts from successful API responses
|
157
|
+
const processedAccounts = originalAccounts.flatMap(fipData =>
|
158
|
+
fipData.DiscoveredAccounts.map((account: DiscoveredAccount) => ({
|
159
|
+
id: account.accRefNumber,
|
160
|
+
type: account.FIType,
|
161
|
+
maskedAccountNumber: account.maskedAccNumber,
|
162
|
+
bankName: fipData.fipName,
|
163
|
+
logoUrl: account.logoUrl,
|
164
|
+
isNew: false,
|
165
|
+
fipId: fipData.fipId
|
166
|
+
}))
|
167
|
+
)
|
168
|
+
|
169
|
+
// Group accounts by type
|
170
|
+
const groupedAccounts = processedAccounts.reduce((acc: ProcessedDiscoveryResult['groupedAccounts'], account) => {
|
171
|
+
const key = account.type
|
172
|
+
if (!acc[key]) {
|
173
|
+
acc[key] = []
|
174
|
+
}
|
175
|
+
acc[key].push(account)
|
176
|
+
return acc
|
177
|
+
}, {} as ProcessedDiscoveryResult['groupedAccounts'])
|
178
|
+
|
179
|
+
// Use the signature from the first successful result
|
180
|
+
const signature = successfulResults[0].result.signature
|
181
|
+
|
182
|
+
// Set partial error message if some FIPs failed
|
183
|
+
if (failedFips.length > 0) {
|
184
|
+
const partialErrorMsg = `Some providers failed to load: ${failedFips.join(', ')}`
|
185
|
+
setErrorMessage(partialErrorMsg)
|
186
|
+
} else {
|
187
|
+
setErrorMessage(null)
|
188
|
+
}
|
189
|
+
|
190
|
+
setIsLoading(false)
|
191
|
+
return {
|
192
|
+
originalAccounts,
|
193
|
+
accounts: processedAccounts,
|
194
|
+
groupedAccounts,
|
195
|
+
signature
|
196
|
+
}
|
197
|
+
} catch (error) {
|
198
|
+
setIsLoading(false)
|
199
|
+
const typedError = error as Error & {
|
200
|
+
response?: {
|
201
|
+
status?: number;
|
202
|
+
data?: { message?: string }
|
203
|
+
}
|
204
|
+
}
|
205
|
+
|
206
|
+
if (typedError.response?.status === 401) {
|
207
|
+
setErrorMessage('Authentication error. Please log in again.')
|
208
|
+
} else if (typedError.response?.status === 403) {
|
209
|
+
setErrorMessage("You don't have permission to discover accounts.")
|
210
|
+
} else if (typedError.response?.data?.message) {
|
211
|
+
setErrorMessage(typedError.response.data.message)
|
212
|
+
} else {
|
213
|
+
setErrorMessage('Failed to discover accounts. Please try again.')
|
214
|
+
}
|
215
|
+
throw error
|
216
|
+
}
|
217
|
+
}
|
218
|
+
})
|
219
|
+
|
220
|
+
return {
|
221
|
+
...autoDiscoveryMutation,
|
222
|
+
isLoading,
|
223
|
+
errorMessage,
|
224
|
+
clearError: () => setErrorMessage(null)
|
225
|
+
}
|
226
|
+
}
|
package/src/index.css
CHANGED
@@ -50,6 +50,8 @@
|
|
50
50
|
|
51
51
|
--surface: oklch(0.98 0.0017 247.84);
|
52
52
|
|
53
|
+
--warning-primary: oklch(0.9791 0.018 78.24);
|
54
|
+
|
53
55
|
}
|
54
56
|
|
55
57
|
.dark {
|
@@ -136,6 +138,7 @@
|
|
136
138
|
|
137
139
|
--color-surface: var(--surface);
|
138
140
|
|
141
|
+
--color-warning-primary: var(--warning-primary);
|
139
142
|
}
|
140
143
|
|
141
144
|
@layer base {
|