saafe-redirection-flow 2.0.0 → 2.2.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 +23 -0
- package/aa-redirection-1607251826.zip +0 -0
- package/docs/features/account-discovery.md +803 -0
- package/docs/features/authentication.md +583 -0
- package/docs/features/consent-management.md +740 -0
- package/docs/features/index.md +206 -0
- package/docs/features/navigation-routing.md +846 -0
- package/docs/features/overview.md +554 -0
- package/docs/features/theming-internationalization.md +982 -0
- package/docs/index.md +1 -1
- package/package.json +1 -1
- package/src/components/AutoDiscoveryLoader.tsx +29 -0
- package/src/components/LinkedAccountTypeAccordion.tsx +154 -0
- 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/title/AppBar.tsx +1 -1
- 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 +38 -6
- 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 +598 -59
- package/src/pages/accounts/Discover.tsx +127 -9
- package/src/pages/accounts/DiscoverAccount.tsx +195 -42
- package/src/pages/accounts/LinkSelectedAccounts.tsx +15 -9
- package/src/pages/accounts/OldUser.tsx +184 -79
- package/src/pages/accounts/link-accounts.tsx +19 -11
- package/src/pages/consent/ReviewConsent.tsx +19 -10
- package/src/pages/consent/rejected.tsx +17 -8
- package/src/pages/consent/success.tsx +125 -76
- package/src/services/api/account.service.ts +2 -2
- package/src/store/fip.store.ts +74 -28
- package/src/store/redirect.store.ts +19 -0
- package/src/utils/auto-discovery.ts +126 -0
- package/src/utils/toast-helpers.ts +5 -0
- package/stage-aa-2506251021.zip +0 -0
|
@@ -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 {
|