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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saafe-redirection-flow",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite",
@@ -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 AlertComp = ({ icon, title, description, rightSection }: AlertInterface) => {
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='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'>
16
+ <div className={colors[type]}>
11
17
  <div className='flex items-center justify-between'>
12
- <div className='flex items-start w-full gap-4'>
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 { CircleHelp, Clock, Clock4 } from "lucide-react";
7
+ import { Clock, Clock4 } from "lucide-react";
8
8
  import { useNavigationBlock } from "@/store/NavigationBlockContext";
9
9
  import { SESSION } from "@/config/urls";
10
- import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
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 (error) {
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
- <Popover>
110
- <PopoverTrigger>
111
- <div className="flex items-center gap-1 cursor-pointer">
112
- <CircleHelp size={12} />
113
- <span className="text-xs mt-0.5">
114
- {t("session.help")}
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
- <Popover>
132
- <PopoverTrigger>
133
- <div className="flex items-center gap-1 cursor-pointer">
134
- <CircleHelp size={14} />
135
- <span className="text-sm mt-0.5">
136
- {t("session.help")}
137
- </span>
138
- </div>
139
- </PopoverTrigger>
140
- <PopoverContent className="w-68">
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.map(i => ({
83
- type: i.type,
84
- value: i.value,
85
- categoryType: i.category
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 {