saafe-redirection-flow 2.0.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.
Files changed (225) hide show
  1. package/.github/workflows/build-and-deploy.yml +41 -0
  2. package/.gitlab-ci.yml +108 -0
  3. package/.releaserc.json +18 -0
  4. package/.storybook/main.ts +28 -0
  5. package/.storybook/preview.ts +16 -0
  6. package/.storybook/vitest.setup.ts +9 -0
  7. package/.vite/deps/@radix-ui_react-avatar.js +230 -0
  8. package/.vite/deps/@radix-ui_react-avatar.js.map +7 -0
  9. package/.vite/deps/@radix-ui_react-slot.js +12 -0
  10. package/.vite/deps/@radix-ui_react-slot.js.map +7 -0
  11. package/.vite/deps/_metadata.json +79 -0
  12. package/.vite/deps/chunk-5VGQBUCU.js +597 -0
  13. package/.vite/deps/chunk-5VGQBUCU.js.map +7 -0
  14. package/.vite/deps/chunk-DC5AMYBS.js +38 -0
  15. package/.vite/deps/chunk-DC5AMYBS.js.map +7 -0
  16. package/.vite/deps/chunk-HUIEPYH7.js +11265 -0
  17. package/.vite/deps/chunk-HUIEPYH7.js.map +7 -0
  18. package/.vite/deps/chunk-TKHB4QMX.js +281 -0
  19. package/.vite/deps/chunk-TKHB4QMX.js.map +7 -0
  20. package/.vite/deps/chunk-YLDSBLSF.js +1139 -0
  21. package/.vite/deps/chunk-YLDSBLSF.js.map +7 -0
  22. package/.vite/deps/class-variance-authority.js +63 -0
  23. package/.vite/deps/class-variance-authority.js.map +7 -0
  24. package/.vite/deps/lucide-react.js +36984 -0
  25. package/.vite/deps/lucide-react.js.map +7 -0
  26. package/.vite/deps/package.json +3 -0
  27. package/.vite/deps/react-dom_client.js +17917 -0
  28. package/.vite/deps/react-dom_client.js.map +7 -0
  29. package/.vite/deps/react-router-dom.js +452 -0
  30. package/.vite/deps/react-router-dom.js.map +7 -0
  31. package/.vite/deps/react-router.js +234 -0
  32. package/.vite/deps/react-router.js.map +7 -0
  33. package/.vite/deps/react.js +5 -0
  34. package/.vite/deps/react.js.map +7 -0
  35. package/.vite/deps/react_jsx-dev-runtime.js +470 -0
  36. package/.vite/deps/react_jsx-dev-runtime.js.map +7 -0
  37. package/CHANGELOG.md +420 -0
  38. package/LICENSE +21 -0
  39. package/README.md +129 -0
  40. package/RELEASE_CHEATSHEET.md +93 -0
  41. package/RELEASE_NOTES.md +120 -0
  42. package/components.json +21 -0
  43. package/docs/DEPLOYMENT_WORKFLOW.md +262 -0
  44. package/docs/RELEASE_GUIDE.md +591 -0
  45. package/docs/architecture.md +432 -0
  46. package/docs/components.md +199 -0
  47. package/docs/index.md +69 -0
  48. package/docs/local-release-workflow.md +234 -0
  49. package/docs/routes.md +118 -0
  50. package/docs/sdk-integration.md +325 -0
  51. package/docs/semantic-release.md +124 -0
  52. package/docs/user-flow.md +206 -0
  53. package/eslint.config.js +28 -0
  54. package/index.html +19 -0
  55. package/install.sh +198 -0
  56. package/package.json +115 -0
  57. package/public/images/bank-logo.png +0 -0
  58. package/public/saafe-icon.svg +9 -0
  59. package/src/App.tsx +171 -0
  60. package/src/__tests__/url-parameters.test.ts +82 -0
  61. package/src/assets/brand/applestore.svg +13 -0
  62. package/src/assets/brand/playstore.svg +23 -0
  63. package/src/assets/brand/saafe-color-white-logo.svg +14 -0
  64. package/src/assets/brand/saafe-icon.svg +9 -0
  65. package/src/assets/brand/saafe-logo.svg +18 -0
  66. package/src/assets/icons/check-icon-dark.svg +27 -0
  67. package/src/assets/icons/check-icon.svg +23 -0
  68. package/src/components/ErrorBoundary.tsx +132 -0
  69. package/src/components/alert/alert.tsx +27 -0
  70. package/src/components/auth/AuthGuard.tsx +76 -0
  71. package/src/components/cards/BankCard.stories.tsx +69 -0
  72. package/src/components/cards/BankCard.tsx +227 -0
  73. package/src/components/cards/OuterCard.tsx +109 -0
  74. package/src/components/cards/WrapperCard.tsx +64 -0
  75. package/src/components/documents/PrivacyContent.tsx +1 -0
  76. package/src/components/dummyFooter.tsx +29 -0
  77. package/src/components/icons/github.tsx +12 -0
  78. package/src/components/language/LanguageSwitcher.tsx +44 -0
  79. package/src/components/layouts/FrostedLayout.stories.tsx +42 -0
  80. package/src/components/layouts/FrostedLayout.tsx +333 -0
  81. package/src/components/layouts/MobileLayout.tsx +403 -0
  82. package/src/components/mobile-background.tsx +136 -0
  83. package/src/components/mobileAppDownload.tsx +30 -0
  84. package/src/components/modal/ModalComp.tsx +27 -0
  85. package/src/components/mode-toggle.tsx +36 -0
  86. package/src/components/page-header.tsx +50 -0
  87. package/src/components/session/SessionTimeoutScreen.tsx +134 -0
  88. package/src/components/session/SessionTimer.tsx +173 -0
  89. package/src/components/step-navigation.tsx +87 -0
  90. package/src/components/title/AppBar.stories.tsx +50 -0
  91. package/src/components/title/AppBar.tsx +150 -0
  92. package/src/components/title/SectionTitle.tsx +31 -0
  93. package/src/components/ui/AnimatedButton.module.css +13 -0
  94. package/src/components/ui/alert.tsx +66 -0
  95. package/src/components/ui/animatedButton.tsx +111 -0
  96. package/src/components/ui/avatar.tsx +51 -0
  97. package/src/components/ui/badge.tsx +36 -0
  98. package/src/components/ui/bottom-sheet.tsx +122 -0
  99. package/src/components/ui/button.tsx +59 -0
  100. package/src/components/ui/calendar.tsx +86 -0
  101. package/src/components/ui/card.tsx +92 -0
  102. package/src/components/ui/checkbox.stories.tsx +49 -0
  103. package/src/components/ui/checkbox.tsx +67 -0
  104. package/src/components/ui/collapsible.tsx +45 -0
  105. package/src/components/ui/dialog.tsx +134 -0
  106. package/src/components/ui/document-link.tsx +26 -0
  107. package/src/components/ui/dot-stepper.tsx +57 -0
  108. package/src/components/ui/dropdown-menu.tsx +255 -0
  109. package/src/components/ui/form.tsx +165 -0
  110. package/src/components/ui/frosted-panel.stories.tsx +86 -0
  111. package/src/components/ui/frosted-panel.tsx +276 -0
  112. package/src/components/ui/input.tsx +39 -0
  113. package/src/components/ui/label.stories.tsx +67 -0
  114. package/src/components/ui/label.tsx +23 -0
  115. package/src/components/ui/mobile-footer.tsx +54 -0
  116. package/src/components/ui/modal.tsx +90 -0
  117. package/src/components/ui/otp-input.stories.tsx +62 -0
  118. package/src/components/ui/otp-input.tsx +221 -0
  119. package/src/components/ui/platform-specific-behavior.tsx +28 -0
  120. package/src/components/ui/popover.tsx +46 -0
  121. package/src/components/ui/progress.tsx +103 -0
  122. package/src/components/ui/radio-group.tsx +45 -0
  123. package/src/components/ui/scroll-area.tsx +56 -0
  124. package/src/components/ui/sdk-params-docs.tsx +53 -0
  125. package/src/components/ui/select.tsx +159 -0
  126. package/src/components/ui/separator.tsx +28 -0
  127. package/src/components/ui/sheet.tsx +137 -0
  128. package/src/components/ui/sidebar.tsx +724 -0
  129. package/src/components/ui/skeleton.stories.tsx +50 -0
  130. package/src/components/ui/skeleton.tsx +15 -0
  131. package/src/components/ui/sonner.tsx +23 -0
  132. package/src/components/ui/step.stories.tsx +132 -0
  133. package/src/components/ui/step.tsx +234 -0
  134. package/src/components/ui/stepper-progress.tsx +136 -0
  135. package/src/components/ui/stepper.tsx +259 -0
  136. package/src/components/ui/tabs.tsx +55 -0
  137. package/src/components/ui/tooltip.tsx +61 -0
  138. package/src/components/ui/url-decode-loader.tsx +36 -0
  139. package/src/components/ui/version-display.tsx +104 -0
  140. package/src/components/ui/web-footer.tsx +36 -0
  141. package/src/config/environments.ts +99 -0
  142. package/src/config/urls.ts +53 -0
  143. package/src/const/fiTypeCategoryMap.ts +19 -0
  144. package/src/contexts/LanguageContext.tsx +41 -0
  145. package/src/contexts/RTLContext.tsx +42 -0
  146. package/src/contexts/ThemeContext.tsx +93 -0
  147. package/src/hooks/use-account-discovery.ts +205 -0
  148. package/src/hooks/use-auth-query.ts +141 -0
  149. package/src/hooks/use-fip-query.ts +72 -0
  150. package/src/hooks/use-media-query.ts +32 -0
  151. package/src/hooks/use-mobile.ts +24 -0
  152. package/src/hooks/use-page-title.tsx +48 -0
  153. package/src/hooks/use-platform.ts +52 -0
  154. package/src/hooks/use-trusted-count.ts +21 -0
  155. package/src/hooks/use-url-decode.ts +90 -0
  156. package/src/hooks/useStep.ts +170 -0
  157. package/src/index.css +154 -0
  158. package/src/interfaces/app.interfaces.ts +39 -0
  159. package/src/interfaces/services.interfaces.ts +65 -0
  160. package/src/lib/i18n.ts +68 -0
  161. package/src/lib/utils.ts +6 -0
  162. package/src/locales/en/common.json +167 -0
  163. package/src/locales/hi/common.json +137 -0
  164. package/src/locales/kn/common.json +137 -0
  165. package/src/locales/ml/common.json +137 -0
  166. package/src/locales/ta/common.json +137 -0
  167. package/src/locales/te/common.json +137 -0
  168. package/src/locales/ur/common.json +138 -0
  169. package/src/main.tsx +46 -0
  170. package/src/pages/Login.tsx +363 -0
  171. package/src/pages/accounts/AccountsToProceed.tsx +396 -0
  172. package/src/pages/accounts/Discover.tsx +76 -0
  173. package/src/pages/accounts/DiscoverAccount.tsx +751 -0
  174. package/src/pages/accounts/LinkSelectedAccounts.tsx +638 -0
  175. package/src/pages/accounts/OldUser.tsx +329 -0
  176. package/src/pages/accounts/link-accounts.tsx +913 -0
  177. package/src/pages/consent/ReviewConsent.tsx +836 -0
  178. package/src/pages/consent/rejected.tsx +253 -0
  179. package/src/pages/consent/success.tsx +220 -0
  180. package/src/providers/query-provider.tsx +24 -0
  181. package/src/providers/toast-provider.tsx +26 -0
  182. package/src/services/api/account.service.ts +296 -0
  183. package/src/services/api/auth.service.ts +206 -0
  184. package/src/services/api/axios.ts +138 -0
  185. package/src/services/api/consent.service.ts +142 -0
  186. package/src/services/api/decode.service.ts +53 -0
  187. package/src/services/api/feedback.service.ts +34 -0
  188. package/src/services/api/fip.service.ts +187 -0
  189. package/src/services/api/index.ts +9 -0
  190. package/src/services/api/public.service.ts +18 -0
  191. package/src/services/api.ts +2 -0
  192. package/src/services/postMessage.service.ts +179 -0
  193. package/src/store/NavigationBlockContext.tsx +34 -0
  194. package/src/store/auth.store.ts +79 -0
  195. package/src/store/fip.store.ts +396 -0
  196. package/src/store/mandatoryConsent.store.ts +24 -0
  197. package/src/store/redirect.store.ts +73 -0
  198. package/src/store/step.store.ts +124 -0
  199. package/src/stories/Button.stories.ts +53 -0
  200. package/src/stories/Button.tsx +37 -0
  201. package/src/stories/Configure.mdx +364 -0
  202. package/src/stories/Header.stories.ts +33 -0
  203. package/src/stories/Header.tsx +56 -0
  204. package/src/stories/Page.stories.ts +32 -0
  205. package/src/stories/Page.tsx +73 -0
  206. package/src/stories/button.css +30 -0
  207. package/src/stories/header.css +32 -0
  208. package/src/stories/page.css +68 -0
  209. package/src/styles/rtl-utils.css +90 -0
  210. package/src/styles/rtl.css +105 -0
  211. package/src/utils/api-error.ts +26 -0
  212. package/src/utils/cn.ts +10 -0
  213. package/src/utils/error-callback.ts +116 -0
  214. package/src/utils/formatAccountNumber.ts +9 -0
  215. package/src/utils/handleIdentifiers.ts +90 -0
  216. package/src/utils/posthog.ts +67 -0
  217. package/src/utils/toast-helpers.ts +61 -0
  218. package/src/vite-env.d.ts +1 -0
  219. package/stage-aa-2506251021.zip +0 -0
  220. package/tsconfig.app.json +33 -0
  221. package/tsconfig.json +13 -0
  222. package/tsconfig.node.json +24 -0
  223. package/vite.config.ts +45 -0
  224. package/vitest.shims.d.ts +1 -0
  225. package/vitest.workspace.ts +46 -0
@@ -0,0 +1,836 @@
1
+ import BankCard from '@/components/cards/BankCard'
2
+ import SectionTitle from '@/components/title/SectionTitle'
3
+ import { AnimatedButton } from '@/components/ui/animatedButton'
4
+ import { Checkbox } from '@/components/ui/checkbox'
5
+ import {
6
+ Collapsible,
7
+ CollapsibleContent,
8
+ CollapsibleTrigger
9
+ } from '@/components/ui/collapsible'
10
+ import { Label } from '@/components/ui/label'
11
+ import { MobileFooter } from '@/components/ui/mobile-footer'
12
+ import WebFooter from '@/components/ui/web-footer'
13
+ import {
14
+ BriefcaseBusiness,
15
+ ChevronDown,
16
+ ChevronUp,
17
+ CircleGauge,
18
+ Crosshair,
19
+ Edit2,
20
+ FileText,
21
+ Info,
22
+ InfoIcon,
23
+ PanelTop,
24
+ SquareActivity
25
+ } from 'lucide-react'
26
+ import React, { useEffect, useState } from 'react'
27
+ import { trackEvent, EVENTS } from '@/utils/posthog'
28
+ import { FrostedLayout } from '@/components/layouts/FrostedLayout'
29
+ import { useRedirectStore } from '@/store/redirect.store'
30
+ import {
31
+ consentService,
32
+ LinkedAccount,
33
+ ConsentDetails,
34
+ ApprovalRequest
35
+ } from '@/services/api'
36
+ import { useNavigate } from 'react-router-dom'
37
+ import logo from '../../assets/brand/saafe-logo.svg'
38
+ import Modal from '@/components/ui/modal'
39
+ import { useMediaQuery } from '@/hooks/use-media-query'
40
+ import { useTranslation } from 'react-i18next'
41
+ import { Alert, AlertTitle } from '@/components/ui/alert'
42
+ import { useFipStore } from '@/store/fip.store'
43
+ import { useRTL } from '@/contexts/RTLContext'
44
+ import { useSetPageTitle } from '@/hooks/use-page-title'
45
+ import DummyFooter from '@/components/dummyFooter'
46
+ import { useMandatoryConsentStore } from '@/store/mandatoryConsent.store'
47
+ import { handleApiErrorWithCallback } from '@/utils/error-callback'
48
+
49
+ type ConsentDetail = {
50
+ id: number | string
51
+ modal: boolean
52
+ }
53
+
54
+ type GroupedAccounts = {
55
+ [key: string]: LinkedAccount[]
56
+ }
57
+
58
+ // Add constants for S3 bucket URL
59
+ const S3_LOGO_BASE_URL = import.meta.env.VITE_IMAGE_BASE_URL
60
+
61
+ // Define an interface for DataConsumer
62
+ interface DataConsumer {
63
+ id?: string;
64
+ name?: string;
65
+ }
66
+
67
+ const ReviewConsent = () => {
68
+ const navigate = useNavigate()
69
+ const { decodedInfo } = useRedirectStore()
70
+ const consentHandle = decodedInfo?.srcref || ''
71
+ const fiuId = decodedInfo?.fiuId
72
+ const isMobile = useMediaQuery('(max-width: 768px)')
73
+ const { t } = useTranslation()
74
+ const { mandatoryConsent, setMandatoryConsent } = useMandatoryConsentStore()
75
+
76
+ const ACCOUNT_TYPE_LABELS_TRANSLATED: { [key: string]: string } = {
77
+ BANK_ACCOUNT: t('consent.consentTypes.BANK_ACCOUNT'),
78
+ MUTUAL_FUNDS: t('consent.consentTypes.MUTUAL_FUNDS'),
79
+ GST: t('consent.consentTypes.GST')
80
+ }
81
+
82
+ const [open, setOpen] = React.useState(false)
83
+ const [consentDetailsCollapse, setConsentDetailsCollapse] =
84
+ useState<ConsentDetail>({ id: 0, modal: false })
85
+
86
+ const [linkedAccounts, setLinkedAccounts] = useState<LinkedAccount[]>([])
87
+ const [consentDetails, setConsentDetails] = useState<ConsentDetails[]>([])
88
+ const [selectedConsent, setSelectedConsent] = useState<ConsentDetails[]>([])
89
+ const [loading, setLoading] = useState(true)
90
+ const [error, setError] = useState<string | null>(null)
91
+ const [groupedAccounts, setGroupedAccounts] = useState<GroupedAccounts>({})
92
+ const [showRejectModal, setShowRejectModal] = useState(false)
93
+ const { activeCategory, accountForConsent } = useFipStore()
94
+ const { isRTL } = useRTL()
95
+
96
+ // page title
97
+ useSetPageTitle(consentDetails.length > 1 ? 'Review consents' : 'Review consent');
98
+
99
+ // Fetch mandatory consent data
100
+ useEffect(() => {
101
+ const fetchMandatoryConsent = async () => {
102
+ if (!fiuId) {
103
+ // console.warn('No FIU ID available, using default: pentafox-fiu');
104
+ }
105
+
106
+ try {
107
+ // Use the correct fiuId from decodedInfo
108
+ const fiuIdToUse = fiuId || 'pentafox-fiu';
109
+
110
+ const mandatoryConsentData = await consentService.getMandatoryConsent(fiuIdToUse);
111
+ setMandatoryConsent(mandatoryConsentData);
112
+ } catch (err) {
113
+ // console.error('Error fetching mandatory consent data:', err);
114
+ handleApiErrorWithCallback(err, 'Failed to fetch mandatory consent data', consentHandle, decodedInfo?.redirect);
115
+ // Continue even if this fails, we'll use default values
116
+ }
117
+ };
118
+
119
+ fetchMandatoryConsent();
120
+ }, [fiuId, setMandatoryConsent]);
121
+
122
+ // Fetch data on component mount
123
+ useEffect(() => {
124
+ if (!consentHandle) {
125
+ setError('Consent handle not found')
126
+ setLoading(false)
127
+ return
128
+ }
129
+
130
+ const fetchData = async () => {
131
+ try {
132
+ const fipId = decodedInfo?.fipId;
133
+ const [accountsData, consentData] = await Promise.all([
134
+ consentService.getLinkedAccounts(consentHandle, fipId),
135
+ consentService.getConsentDetails(consentHandle)
136
+ ])
137
+ const result = accountForConsent?.length > 0 ? accountsData.filter(account => accountForConsent.includes(account.linkRefNumber)) : accountsData
138
+
139
+ setLinkedAccounts(result)
140
+ setConsentDetails(consentData || []) // Store all consent details
141
+
142
+
143
+ // Group accounts by fiType
144
+ const grouped = result.reduce(
145
+ (groups: GroupedAccounts, account) => {
146
+ const fiType = account.fiType
147
+ if (!groups[fiType]) {
148
+ groups[fiType] = []
149
+ }
150
+ groups[fiType].push(account)
151
+ return groups
152
+ },
153
+ {}
154
+ )
155
+
156
+ setGroupedAccounts(grouped)
157
+
158
+ // Select consents based on mandatory consent settings
159
+ const initialSelectedConsents: ConsentDetails[] = []
160
+ consentData.forEach(consent => {
161
+ const purposeCode = consent.Purpose?.code
162
+ if (purposeCode && mandatoryConsent[purposeCode]) {
163
+ const { enabled } = mandatoryConsent[purposeCode]
164
+ if (enabled) {
165
+ initialSelectedConsents.push(consent)
166
+ }
167
+ }
168
+ })
169
+
170
+ if (consentData?.length == 1) {
171
+ setSelectedConsent([consentData[0]])
172
+ } else {
173
+ setSelectedConsent(initialSelectedConsents)
174
+ }
175
+
176
+
177
+ setLoading(false)
178
+ } catch (err) {
179
+ // console.error('Error fetching data:', err)
180
+ const errorMessage = 'Failed to load data. Please try again.';
181
+ setError(errorMessage);
182
+ setLoading(false);
183
+ handleApiErrorWithCallback(err, errorMessage, consentHandle, decodedInfo?.redirect);
184
+ }
185
+ }
186
+
187
+ fetchData()
188
+ }, [consentHandle, mandatoryConsent, accountForConsent])
189
+
190
+ const handleViewMore = (id: string) => {
191
+ setConsentDetailsCollapse({ id, modal: !consentDetailsCollapse.modal })
192
+ trackEvent(EVENTS.VIEW_MORE_CONSENT, { consentId: id })
193
+ }
194
+
195
+ const toggleConsentSelection = (consentHandle: string) => {
196
+ const consent = consentDetails.find(c => c.ConsentHandle === consentHandle)
197
+ if (!consent) return
198
+
199
+ // Check if the consent is mandatory
200
+ const purposeCode = consent.Purpose?.code
201
+ if (purposeCode && mandatoryConsent[purposeCode]?.mandatory) {
202
+ // If mandatory, don't allow deselection
203
+ return
204
+ }
205
+
206
+ const isSelected = selectedConsent.some(c => c.ConsentHandle === consentHandle)
207
+
208
+ if (isSelected) {
209
+ setSelectedConsent(selectedConsent.filter(c => c.ConsentHandle !== consentHandle))
210
+ } else {
211
+ setSelectedConsent([...selectedConsent, consent])
212
+ }
213
+ }
214
+ useEffect(() => {
215
+ if (consentDetails?.length) {
216
+ if (consentDetails?.length > 1) {
217
+ const result = []
218
+ consentDetails.forEach((i) => {
219
+ const isMandatory = Boolean(mandatoryConsent[i?.Purpose?.code]?.mandatory || mandatoryConsent[i?.Purpose?.code]?.enabled)
220
+ if (isMandatory) result.push(i)
221
+ })
222
+ setSelectedConsent(result)
223
+ } else {
224
+ setSelectedConsent([consentDetails[0]])
225
+ }
226
+ }
227
+ }, [consentDetails])
228
+
229
+ const handleSelectAll = () => {
230
+ const allSelected = consentDetails.length === selectedConsent.length
231
+
232
+ if (allSelected) {
233
+ // When deselecting all, keep mandatory consents selected
234
+ const mandatoryConsents = consentDetails.filter(consent => {
235
+ const purposeCode = consent.Purpose?.code
236
+ return purposeCode && mandatoryConsent[purposeCode]?.mandatory
237
+ })
238
+
239
+ setSelectedConsent(mandatoryConsents)
240
+ } else {
241
+ setSelectedConsent([...consentDetails])
242
+
243
+ }
244
+
245
+ // Create state with all consents selected or deselected
246
+
247
+ // setSelectedConsents(newSelectedState)
248
+ trackEvent(EVENTS.SELECT_ALL_ACCOUNTS)
249
+ }
250
+
251
+ const getSelectedConsents = () => {
252
+ return selectedConsent || []
253
+ }
254
+
255
+ const handleApproveConsent = async () => {
256
+ trackEvent(EVENTS.APPROVE_CONSENT)
257
+ const selectedConsentsList = getSelectedConsents()
258
+
259
+ if (selectedConsentsList.length === 0) {
260
+ setError('Please select at least one consent')
261
+ return
262
+ }
263
+
264
+ try {
265
+ setLoading(true)
266
+
267
+ const result = accountForConsent?.length > 0 ? linkedAccounts.filter(account => accountForConsent.includes(account.linkRefNumber)) : linkedAccounts
268
+
269
+ // Prepare accounts to be associated with selected consents
270
+ const accountsToApprove = result.map(account => ({
271
+ linkRefNumber: account.linkRefNumber,
272
+ fipHandle: account.fipHandle
273
+ }))
274
+
275
+ const approvalData: ApprovalRequest = {
276
+ consentHandle: selectedConsentsList.map(consent => consent.ConsentHandle),
277
+ constentApprovalStatus: 'APPROVED',
278
+ accounts: accountsToApprove
279
+ }
280
+
281
+ const consentResult = await consentService.submitConsentVerification(approvalData)
282
+ console.log("navigatig")
283
+
284
+ if (consentResult?.length && consentResult?.find(item => item.ConsentStatus === 'FAILED')) {
285
+ navigate('/rejected', { state: { consentResult } })
286
+ return
287
+ } else if (consentResult?.length) {
288
+ navigate('/success')
289
+ } else {
290
+ setError('Failed to approve consent. Please try again.')
291
+ }
292
+
293
+ // if (result.status === 200) {
294
+ // } else {
295
+ // setError('Failed to approve consent. Please try again.')
296
+ // }
297
+
298
+ } catch (err) {
299
+ // console.error('Error approving consent:', err)
300
+ const errorMessage = 'Failed to approve consent. Please try again.';
301
+ setError(errorMessage);
302
+ handleApiErrorWithCallback(err, errorMessage, consentHandle, decodedInfo?.redirect);
303
+ } finally {
304
+ setLoading(false)
305
+ // navigate('/success')<div className="flex flex-col items-center gap-6 mt-10">
306
+ {/* Download section */ }
307
+
308
+
309
+ }
310
+ }
311
+
312
+ const handleRejectConsent = async () => {
313
+ trackEvent(EVENTS.REJECT_CONSENT)
314
+
315
+ try {
316
+ setLoading(true)
317
+
318
+ // Include all accounts in rejection
319
+ const accountsToReject = linkedAccounts.map(account => ({
320
+ linkRefNumber: account.linkRefNumber,
321
+ fipHandle: account.fipHandle
322
+ }))
323
+
324
+ const rejectionData: ApprovalRequest = {
325
+ consentHandle: [consentHandle],
326
+ constentApprovalStatus: 'REJECTED',
327
+ accounts: accountsToReject
328
+ }
329
+
330
+ await consentService.submitConsentVerification(rejectionData)
331
+
332
+ navigate('/rejected')
333
+ } catch (err) {
334
+ // console.error('Error rejecting consent:', err)
335
+ const errorMessage = 'Failed to reject consent. Please try again.';
336
+ setError(errorMessage);
337
+ handleApiErrorWithCallback(err, errorMessage, consentHandle, decodedInfo?.redirect);
338
+ } finally {
339
+ setLoading(false)
340
+ }
341
+ }
342
+ const getFrequencyText = (consent: ConsentDetails) => {
343
+ if (!consent?.Frequency) return 'Onetime'
344
+
345
+ const { unit, value } = consent.Frequency
346
+
347
+ if (!value || value === 0) return 'Onetime'
348
+
349
+ const formattedUnit = unit.toLowerCase()
350
+ // const pluralizedUnit = value > 1 ? `${formattedUnit}s` : formattedUnit
351
+
352
+ return `${value} per ${formattedUnit}`
353
+ }
354
+
355
+ // Get date range text
356
+ const getDataRangeText = (consent: ConsentDetails) => {
357
+ if (!consent?.FIDataRange) return ''
358
+
359
+ const { from, to } = consent.FIDataRange
360
+ const fromDate = new Date(from)
361
+ const toDate = new Date(to)
362
+
363
+ const formatDate = (date: Date) => {
364
+ return date.toLocaleDateString('en-US', {
365
+ day: '2-digit',
366
+ month: 'short',
367
+ year: 'numeric'
368
+ })
369
+ }
370
+
371
+ return `${formatDate(fromDate)} to ${formatDate(toDate)}`
372
+ }
373
+
374
+ // Check if a consent is mandatory
375
+ const isConsentMandatory = (consent: ConsentDetails): boolean => {
376
+ const purposeCode = consent.Purpose?.code
377
+ return purposeCode ? Boolean(mandatoryConsent[purposeCode]?.mandatory) : false
378
+ }
379
+
380
+ // Function to get logo URL for data consumer
381
+ const getDataConsumerLogoUrl = (dataConsumer: DataConsumer | undefined) => {
382
+ if (!dataConsumer || !dataConsumer.id) return logo
383
+ return `${S3_LOGO_BASE_URL}${dataConsumer.id}.jpeg`
384
+ }
385
+
386
+ if (loading) {
387
+ return (
388
+ <FrostedLayout>
389
+ <div className='flex items-center justify-center h-full'>
390
+ <p>Loading...</p>
391
+ </div>
392
+ </FrostedLayout>
393
+ )
394
+ }
395
+
396
+ if (error) {
397
+ return (
398
+ <FrostedLayout>
399
+ <div className='flex flex-col items-center justify-center h-full'>
400
+ <p className='text-red-500'>{error}</p>
401
+ <AnimatedButton
402
+ onClick={() => window.location.reload()}
403
+ className='mt-4'
404
+ >
405
+ Try Again
406
+ </AnimatedButton>
407
+ </div>
408
+ </FrostedLayout>
409
+ )
410
+ }
411
+ const purposeCodeMap: Record<string, string> = {
412
+ "101": "Wealth management service",
413
+ "102": "Customer spending patterns, budget or other reportings",
414
+ "103": "For loan underwriting",
415
+ "104": "For loan monitoring",
416
+ "105": "For account verification",
417
+ // Add more codes as needed
418
+ }
419
+
420
+ return (
421
+ <FrostedLayout>
422
+ <div className='flex flex-col w-full h-full py-4 md:px-14 sm:px4 mb-14'>
423
+ {linkedAccounts.length === 0 ?
424
+ <div className='mb-8'>
425
+ <Alert>
426
+ <AlertTitle className='flex items-center justify-between gap-4'>
427
+ <div className='flex items-center gap-4'>
428
+ <div className='flex items-center justify-center bg-yellow-600 p-2 rounded-full w-fit'>
429
+ <Info
430
+ strokeWidth='2.3'
431
+ className='h-[18px] w-[18px] text-white'
432
+ />
433
+ </div>
434
+ <p className='text-lg font-medium'>
435
+ No accounts selected! You need to select accounts to approve this consent
436
+ </p>
437
+ </div>
438
+ <AnimatedButton
439
+ variant={'outline'}
440
+ size={'lg'}
441
+ className='text-yellow-600 border-yellow-600 border-2 hover:bg-yellow-200 hover:text-yellow-900'
442
+ onClick={() => { trackEvent(EVENTS.DISCOVER_MORE); navigate(`/link-accounts/${activeCategory}`) }}
443
+ >
444
+ Add accounts
445
+ </AnimatedButton>
446
+ </AlertTitle>
447
+ </Alert>
448
+ </div>
449
+ : null}
450
+ {linkedAccounts.length === 0 ? (null
451
+ ) : (
452
+ <>
453
+ <SectionTitle title={linkedAccounts.length === 1 ? t('keywords.account') : t('keywords.accounts')} />
454
+ <div className='bg-white dark:bg-card rounded-md mt-4 p-4 border-1 border-gray-100 dark:border-gray-800'>
455
+ <Collapsible open={open} onOpenChange={setOpen}>
456
+ <CollapsibleTrigger asChild>
457
+ <div className='flex items-center justify-between gap-2 cursor-pointer'>
458
+ <div className='flex items-center justify-center gap-4 font-medium relative '>
459
+ <div className='w-12 h-8'>
460
+ {linkedAccounts.length > 0 && (
461
+ <img
462
+ src={linkedAccounts[0].logoUrl}
463
+ className='absolute w-8 h-8 rounded-md'
464
+ alt='First account logo'
465
+ />
466
+ )}
467
+ {linkedAccounts.length > 1 && (
468
+ <img
469
+ src={linkedAccounts[1].logoUrl}
470
+ className={`absolute w-8 h-8 left-4 ${isRTL ? 'right-4' : 'left-4'} rounded-md`}
471
+ alt='Second account logo'
472
+ />
473
+ )}
474
+ </div>
475
+ <p className='text-content-primary dark:text-gray-400 text-md'>
476
+ {linkedAccounts.length === 1
477
+ ? linkedAccounts[0].fipName
478
+ : linkedAccounts.length > 1
479
+ ? `${linkedAccounts[0].fipName} ${linkedAccounts.length > 2 ? `+${linkedAccounts.length - 1} more` : ''}`
480
+ : 'View more'}
481
+ </p>
482
+ </div>
483
+ </div>
484
+ </CollapsibleTrigger>
485
+ <CollapsibleContent open={open}>
486
+ <div className='flex flex-col gap-4 mt-4 border-t-1 border-gray-300 dark:border-gray-600 border-dashed'>
487
+ {Object.entries(groupedAccounts).map(([fiType, accounts], index) => (
488
+ <div key={fiType}>
489
+ <div className='flex items-center justify-between mt-4 mb-4'>
490
+ <Label className='text-sm md:text-md text-black dark:text-gray-300 font-semibold'>
491
+ {ACCOUNT_TYPE_LABELS_TRANSLATED[fiType] || fiType}
492
+ </Label>
493
+ {index == 0 ? <AnimatedButton
494
+ variant={'text'}
495
+ size={'text'}
496
+ className='text-primary'
497
+ onClick={() => { trackEvent(EVENTS.EDIT_ACCOUNT); navigate(`/link-accounts/old-user`) }}
498
+ >
499
+ <Edit2 /> Edit
500
+ </AnimatedButton> : null}
501
+ </div>
502
+ <div className='flex flex-col gap-4'>
503
+ {accounts.map(account => (
504
+ <BankCard
505
+ key={account.accountRefNumber}
506
+ bankName={account.fipName}
507
+ badgeText={false}
508
+ image={account.logoUrl}
509
+ subText={`${account.accountType
510
+ } | ***${account.maskedAccNumber.slice(-4)}`}
511
+ />
512
+ ))}
513
+ </div>
514
+ </div>
515
+ ))}
516
+ </div>
517
+ </CollapsibleContent>
518
+ </Collapsible>
519
+
520
+ </div>
521
+ </>
522
+ )}
523
+
524
+ {consentDetails.length > 0 && (
525
+ <>
526
+ <p className='text-sm text-consent-secondary mt-4'>{decodedInfo?.fiuName} requests your explicit consent to access your financial data</p>
527
+ <SectionTitle
528
+ title='Consent details'
529
+ className='mt-6'
530
+ rightSection={
531
+ consentDetails.length > 1 && (
532
+ <AnimatedButton
533
+ variant={'text'}
534
+ size={'text'}
535
+ className='text-primary'
536
+ onClick={handleSelectAll}
537
+ >
538
+ {selectedConsent?.length === consentDetails.length
539
+ ? 'Deselect All'
540
+ : 'Select All'}
541
+ </AnimatedButton>
542
+ )
543
+ }
544
+ />
545
+
546
+ <div>
547
+ {consentDetails.map((consent) => {
548
+ const isMandatory = isConsentMandatory(consent)
549
+ const isSelected = selectedConsent.some(c => c.ConsentHandle === consent.ConsentHandle)
550
+ // Get the logo URL based on data consumer ID
551
+ const logoUrl = getDataConsumerLogoUrl(consent.DataConsumer)
552
+
553
+ return (
554
+ <div
555
+ key={consent.ConsentHandle}
556
+ className='bg-white dark:bg-card rounded-lg mt-1 p-4 border-1 border-gray-100 dark:border-gray-800 relative'
557
+ >
558
+ <BankCard
559
+ bankName={consent?.DataConsumer?.name || ''}
560
+ image={logoUrl}
561
+ className={`${consentDetails.length > 1 ? 'cursor-pointer' : ''} ${isMandatory ? 'opacity-90' : ''}`}
562
+ selected={isSelected}
563
+ onClick={() => consentDetails.length > 1 && toggleConsentSelection(consent.ConsentHandle)}
564
+ subText={false}
565
+ rightSection={
566
+ consentDetails.length > 1 ? (
567
+ <Checkbox
568
+ checked={isSelected}
569
+ disabled={isMandatory}
570
+ />
571
+ ) : null
572
+ }
573
+ />
574
+ {consent && (
575
+ <div className='flex flex-col gap-6 mt-4'>
576
+
577
+ <div className={`flex flex-row gap-4 ${isRTL ? 'flex-row-reverse' : ''}`} style={{ flexDirection: 'row' }}>
578
+ <div className='flex items-center justify-around p-2 bg-primary/20 w-fit rounded-full h-fit'>
579
+ <FileText className='text-primary' />
580
+ </div>
581
+ <div>
582
+ <Label className={`text-xs md:text-sm w-fit font-medium ${isRTL ? 'text-left' : ''} text-consent-secondary dark:text-gray-400`}>
583
+ Category & data range
584
+ </Label>
585
+ <Label className={`text-sm md:text-md font-semibold text-consent-primary w-fit ${isRTL ? 'text-left' : ''} mt-1 dark:text-gray-300`}>
586
+ {consent.consentTypes?.join(', ') ||
587
+ 'Profile summary, transactions'}
588
+ {consent.FIDataRange &&
589
+ ` from ${getDataRangeText(consent)}`}
590
+ {/* {consent.DataFilter &&
591
+ consent.DataFilter.length > 0 &&
592
+ ` and transactions ${consent.DataFilter[0].operator} ${consent.DataFilter[0].value}`} */}
593
+ </Label>
594
+ </div>
595
+ </div>
596
+
597
+
598
+
599
+ <div className={`flex flex-row gap-4 ${isRTL ? 'flex-row-reverse' : ''}`} style={{ flexDirection: 'row' }}>
600
+
601
+ <div className='flex items-center justify-around p-2 bg-primary/20 w-fit rounded-full h-fit'>
602
+ <Crosshair className='text-primary' />
603
+ </div>
604
+ <div>
605
+ <Label className={`text-xs md:text-sm w-fit font-medium ${isRTL ? 'text-left' : ''} text-consent-secondary dark:text-gray-400`}>
606
+ Purpose
607
+ </Label>
608
+ <Label className={`text-sm md:text-md font-semibold text-consent-primary w-fit ${isRTL ? 'text-left' : ''} mt-1 dark:text-gray-300`}>
609
+ {consent.Purpose?.text || purposeCodeMap[consent.Purpose?.code] || 'To process the loan application'}
610
+ </Label>
611
+ </div>
612
+
613
+ </div>
614
+ <div className={`flex flex-row gap-4 ${isRTL ? 'flex-row-reverse' : ''}`} style={{ flexDirection: 'row' }}>
615
+ <div className='flex items-center justify-around p-2 bg-primary/20 w-fit rounded-full h-fit'>
616
+ <SquareActivity className='text-primary' />
617
+ </div>
618
+
619
+ <div>
620
+ <Label className={`text-xs md:text-sm w-fit font-medium ${isRTL ? 'text-left' : ''} text-consent-secondary dark:text-gray-400`}>
621
+ Frequency
622
+ </Label>
623
+ <Label className={`text-sm md:text-md font-semibold text-consent-primary w-fit ${isRTL ? 'text-left' : ''} mt-1 dark:text-gray-300`}>
624
+ {getFrequencyText(consent)}
625
+ </Label>
626
+ </div>
627
+ {/* <div>
628
+ <Label className={`text-xs md:text-sm w-fit font-medium ${isRTL ? 'text-left' : ''} text-consent-secondary`}>
629
+ Frequency
630
+ </Label>
631
+ <Label className={`text-sm md:text-md font-semibold text-consent-primary w-fit ${isRTL ? 'text-left' : ''} mt-1`}>
632
+ {children === '1' || children === '1 Onetime' ? 'Onetime' : children}
633
+ </Label>
634
+ </div> */}
635
+
636
+ </div>
637
+
638
+ <Collapsible
639
+ open={
640
+ consentDetailsCollapse.id == consent.ConsentHandle &&
641
+ consentDetailsCollapse.modal
642
+ }
643
+ onOpenChange={() => handleViewMore(consent.ConsentHandle)}
644
+ >
645
+ <CollapsibleContent
646
+ open={
647
+ consentDetailsCollapse.id === consent.ConsentHandle &&
648
+ consentDetailsCollapse.modal
649
+ }
650
+ >
651
+ <div className='flex flex-col gap-6 w-full'>
652
+ <div className={`flex flex-row gap-4 ${isRTL ? 'flex-row-reverse' : ''}`} style={{ flexDirection: 'row' }}>
653
+ <div className='flex items-center justify-around p-2 bg-primary/20 w-fit rounded-full h-fit'>
654
+ <PanelTop className='text-primary' />
655
+ </div>
656
+ <div>
657
+ <Label className={`text-xs md:text-sm w-fit font-medium ${isRTL ? 'text-left' : ''} text-consent-secondary dark:text-gray-400`}>
658
+ Account type
659
+ </Label>
660
+ <Label className={`text-sm md:text-md font-semibold text-consent-primary w-fit ${isRTL ? 'text-left' : ''} mt-1 capitalize dark:text-gray-300`}>
661
+ {consent.fiTypes?.map(
662
+ word => word?.charAt(0)?.toUpperCase() + word?.slice(1)?.toLowerCase()
663
+ )?.join(', ')?.replace(/_/g, ' ') ||
664
+ 'Bank accounts'}
665
+ </Label>
666
+ </div>
667
+ </div>
668
+ <div className={`flex flex-row gap-4 ${isRTL ? 'flex-row-reverse' : ''}`} style={{ flexDirection: 'row' }}>
669
+ <div className='flex items-center justify-around p-2 bg-primary/20 w-fit rounded-full h-fit'>
670
+ <CircleGauge className='text-primary' />
671
+ </div>
672
+ <div>
673
+ <Label className={`text-xs md:text-sm w-fit font-medium ${isRTL ? 'text-left' : ''} text-consent-secondary dark:text-gray-400`}>
674
+ Data life
675
+ </Label>
676
+ <Label className={`text-sm md:text-md font-semibold text-consent-primary w-fit ${isRTL ? 'text-left' : ''} mt-1 dark:text-gray-300`}>
677
+ {consent.DataLife
678
+ ? `${consent.DataLife.value
679
+ } ${consent.DataLife.unit.toLowerCase()}${consent.DataLife.value > 1 ? 's' : ''
680
+ }`
681
+ : '-'}
682
+ </Label>
683
+ </div>
684
+ </div>
685
+ <div className={`flex flex-row gap-4 ${isRTL ? 'flex-row-reverse' : ''}`} style={{ flexDirection: 'row' }}>
686
+ <div className='flex items-center justify-around p-2 bg-primary/20 w-fit rounded-full h-fit'>
687
+ <BriefcaseBusiness className='text-primary' />
688
+ </div>
689
+ <div>
690
+ <Label className={`text-xs md:text-sm w-fit font-medium ${isRTL ? 'text-left' : ''} text-consent-secondary dark:text-gray-400`}>
691
+ Consent validity
692
+ </Label>
693
+ <Label className={`text-sm md:text-md font-semibold text-consent-primary w-fit ${isRTL ? 'text-left' : ''} mt-1 dark:text-gray-300`}>
694
+ {consent.consentStart &&
695
+ consent.consentExpiry
696
+ ? `Upto ${new Date(
697
+ consent.consentExpiry
698
+ ).toLocaleDateString('en-US', {
699
+ day: '2-digit',
700
+ month: 'short',
701
+ year: 'numeric'
702
+ })}`
703
+ : '-'}
704
+ </Label>
705
+ </div>
706
+ </div>
707
+ </div>
708
+ </CollapsibleContent>
709
+ <div className='w-full flex items-center justify-center mt-4 md:mt-1'>
710
+ <CollapsibleTrigger>
711
+ <div className='flex items-center justify-center gap-2 cursor-pointer text-consent-primary dark:text-gray-400 font-semibold text-sm'>
712
+ View{' '}
713
+ {consentDetailsCollapse.id === consent.ConsentHandle &&
714
+ consentDetailsCollapse.modal
715
+ ? 'Less'
716
+ : 'More'}
717
+ {consentDetailsCollapse.id === consent.ConsentHandle &&
718
+ consentDetailsCollapse.modal ? (
719
+ <ChevronUp />
720
+ ) : (
721
+ <ChevronDown />
722
+ )}
723
+ </div>
724
+ </CollapsibleTrigger>
725
+ </div>
726
+ </Collapsible>
727
+
728
+ </div>
729
+
730
+ )}
731
+ </div>
732
+
733
+ )
734
+ })
735
+ }
736
+
737
+
738
+
739
+
740
+ </div>
741
+ </>
742
+ )}
743
+
744
+ <DummyFooter />
745
+ </div>
746
+
747
+ <MobileFooter show={isMobile && (accountForConsent?.length > 0 ? Boolean(linkedAccounts.filter(account => accountForConsent.includes(account.linkRefNumber))?.length) : Boolean(linkedAccounts?.length) && Boolean(selectedConsent?.length))}>
748
+ <div className='flex flex-row items-center w-full gap-4 justify-between'>
749
+ <AnimatedButton
750
+ onClick={() => setShowRejectModal(true)}
751
+ variant={'ghost'}
752
+ size='lg'
753
+ className='text-primary'
754
+ disabled={loading}
755
+ loading={loading}
756
+ >
757
+ Reject
758
+ </AnimatedButton>
759
+ <AnimatedButton
760
+ onClick={handleApproveConsent}
761
+ size='lg'
762
+ className='h-[50px] w-[48%]'
763
+ disabled={
764
+ loading ||
765
+ selectedConsent.length === 0
766
+ }
767
+ >
768
+ Approve
769
+ </AnimatedButton>
770
+ </div>
771
+ </MobileFooter>
772
+
773
+ <WebFooter show={!isMobile && (accountForConsent?.length > 0 ? Boolean(linkedAccounts.filter(account => accountForConsent.includes(account.linkRefNumber))?.length) : Boolean(linkedAccounts?.length) && Boolean(selectedConsent?.length))}>
774
+ <AnimatedButton
775
+ onClick={() => setShowRejectModal(true)}
776
+ variant={'ghost'}
777
+ size='lg'
778
+ className='text-primary'
779
+ disabled={loading}
780
+ >
781
+ Reject
782
+ </AnimatedButton>
783
+ <AnimatedButton
784
+ onClick={handleApproveConsent}
785
+ size='lg'
786
+ disabled={
787
+ loading ||
788
+ selectedConsent.length === 0
789
+ }
790
+ >
791
+ Approve
792
+ </AnimatedButton>
793
+ </WebFooter>
794
+
795
+ <Modal
796
+ open={showRejectModal}
797
+ onOpenChange={open => setShowRejectModal(open)}
798
+ withCloseIcon={false}
799
+ >
800
+ <div className='flex flex-col gap-4 items-center'>
801
+ <div className='flex items-center justify-center bg-red-100 p-2.5 rounded-full'>
802
+ <InfoIcon className='h-[28px] w-[28px] text-red-700' />
803
+ </div>
804
+ <div className='flex flex-col gap-2 items-center text-center mt-2'>
805
+ <p className='text-xl font-semibold dark:text-gray-300'>
806
+ Are you sure you want to reject?
807
+ </p>
808
+ <p className='text-sm text-muted-secondary'>
809
+ Rejecting this consent is a permanent action and cannot be undone.
810
+ However, you can still view the consent details anytime using
811
+ Saafe app.
812
+ </p>
813
+ </div>
814
+ <div className='flex gap-2 w-full items-center justify-center'>
815
+ <AnimatedButton size='lg' onClick={() => setShowRejectModal(false)}>
816
+ No, go back!
817
+ </AnimatedButton>
818
+ <AnimatedButton
819
+ size='lg'
820
+ variant='ghost'
821
+ className='hover:bg-red-100 hover:text-red-700 text-red-700 dark:text-red-500 '
822
+ onClick={handleRejectConsent}
823
+ loading={loading}
824
+ >
825
+ Yes, Reject
826
+ </AnimatedButton>
827
+ </div>
828
+ </div>
829
+ </Modal>
830
+
831
+ </FrostedLayout>
832
+
833
+ )
834
+ }
835
+
836
+ export default ReviewConsent