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,913 @@
1
+ import React, { useState, useEffect, useMemo } from 'react'
2
+ import { useNavigate, useLocation } from 'react-router-dom'
3
+ import SectionTitle from '@/components/title/SectionTitle'
4
+ import { PlusIcon, Search, AlertCircle, InfoIcon } from 'lucide-react'
5
+ import OuterCard from '@/components/cards/OuterCard'
6
+ import BankCard from '@/components/cards/BankCard'
7
+ import { Checkbox } from '@/components/ui/checkbox'
8
+ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
9
+ import { Input } from '@/components/ui/input'
10
+ import { AnimatedButton } from '@/components/ui/animatedButton'
11
+ import Modal from '@/components/ui/modal'
12
+ import { OTPInputComponent } from '@/components/ui/otp-input'
13
+ import { useMediaQuery } from '@/hooks/use-media-query'
14
+ import { BottomSheet } from '@/components/ui/bottom-sheet'
15
+ import { MobileFooter } from '@/components/ui/mobile-footer'
16
+ import WebFooter from '@/components/ui/web-footer'
17
+ import { useFipQuery } from '@/hooks/use-fip-query'
18
+ import { useFipStore } from '@/store/fip.store'
19
+ import { useAuthStore } from '@/store/auth.store'
20
+ import { Skeleton } from '@/components/ui/skeleton'
21
+ import { useMutation, useQuery } from '@tanstack/react-query'
22
+ import { accountService } from '@/services/api'
23
+ import { useRedirectStore } from '@/store/redirect.store'
24
+ import { useTranslation } from 'react-i18next'
25
+ import { Label } from '@/components/ui/label'
26
+ import { useSetPageTitle } from '@/hooks/use-page-title'
27
+ import DummyFooter from '@/components/dummyFooter'
28
+ import { EVENTS, trackEvent } from '@/utils/posthog'
29
+ import { fiTypeCategoryMap } from '@/const/fiTypeCategoryMap'
30
+ // Helper functions
31
+ const formatCategoryName = (category: string) => {
32
+ return category
33
+ .split('_')
34
+ .map(word => word.charAt(0) + word.slice(1).toLowerCase())
35
+ .join(' ')
36
+ }
37
+
38
+ function debounce(func, delay) {
39
+ let timeoutId;
40
+
41
+ return function (...args) {
42
+ clearTimeout(timeoutId);
43
+ timeoutId = setTimeout(() => {
44
+ func.apply(this, args);
45
+ }, delay);
46
+ };
47
+ }
48
+
49
+ const AddNumberModalContent = ({ showModal, setShowModal, t, addNewNoQuery, handleAddNumber }: {
50
+ showModal: { number: string; error: string };
51
+ setShowModal: React.Dispatch<React.SetStateAction<{
52
+ isOpen: boolean;
53
+ title: string;
54
+ content: React.ReactNode | null;
55
+ number: string;
56
+ otp: string | null;
57
+ error: string;
58
+ }>>;
59
+ t: (key: string) => string;
60
+ addNewNoQuery: {
61
+ mutate: (data: undefined, options: {
62
+ onSuccess: () => void;
63
+ onError: (error: Error) => void;
64
+ }) => void;
65
+ };
66
+ handleAddNumber: (number: string) => void;
67
+ }) => {
68
+ const [inputValue, setInputValue] = useState({ number: showModal.number, error: showModal.error });
69
+
70
+ useEffect(() => {
71
+ setInputValue({ number: showModal.number, error: showModal.error });
72
+ }, [showModal.number]);
73
+ return useMemo(() => (
74
+ <div className='flex flex-col items-center gap-5'>
75
+ <p className='text-base font-normal dark:text-muted-secondary'>
76
+ {t('pleaseEnterTheNewNumberYouWishToLinkBelow')}
77
+ </p>
78
+ <Input
79
+ type="tel"
80
+ inputMode="numeric"
81
+ className='h-12'
82
+ placeholder={t('mobileNoPlaceholder')}
83
+ maxLength={10}
84
+ value={inputValue.number}
85
+ onChange={e => {
86
+ const newValue = e.target.value;
87
+ const numericValue = newValue.replace(/[^0-9]/g, '');
88
+ setInputValue({ number: numericValue, error: '' });
89
+ setShowModal(prev => ({
90
+ ...prev,
91
+ number: numericValue,
92
+ error: numericValue.length === 10 ? '' : 'Please enter a valid 10-digit mobile number'
93
+ }));
94
+ }}
95
+ />
96
+ {inputValue.error && (
97
+ <p className="text-red-500 text-sm mt-[-20px]">{inputValue.error}</p>
98
+ )}
99
+ <AnimatedButton
100
+ className='w-full h-11 mt-2'
101
+ onClick={() => {
102
+ if (inputValue.number.length !== 10) {
103
+ setInputValue({ number: inputValue.number, error: 'Please enter a valid 10-digit mobile number' });
104
+ setShowModal(prev => ({ ...prev, error: 'Please enter a valid 10-digit mobile number' }));
105
+ return;
106
+ }
107
+ addNewNoQuery.mutate(undefined, {
108
+ onSuccess: () => {
109
+ handleAddNumber(inputValue.number)
110
+ },
111
+ onError: (error: Error) => {
112
+ // console.error(error)
113
+ setInputValue({ number: inputValue.number, error: error.response?.data?.errorCode || 'Something went wrong' });
114
+ }
115
+ })
116
+ }}
117
+ >
118
+ {t('login.getOtp')}
119
+ </AnimatedButton>
120
+ </div>
121
+ ), [inputValue, showModal.error, t, setShowModal, addNewNoQuery, handleAddNumber])
122
+ }
123
+
124
+ const LinkAccounts = () => {
125
+ const { t } = useTranslation()
126
+ useSetPageTitle(t('selectAndDiscoverYourAccounts'))
127
+ const navigate = useNavigate()
128
+ const location = useLocation()
129
+ const { decodedInfo } = useRedirectStore()
130
+ const [searchQuery, setSearchQuery] = useState('')
131
+ const [showModal, setShowModal] = useState<{
132
+ isOpen: boolean;
133
+ title: string;
134
+ content: React.ReactNode | null;
135
+ number: string;
136
+ otp: string | null;
137
+ error: string;
138
+ }>({
139
+ isOpen: false,
140
+ title: t('addNewMobileNumber'),
141
+ content: null,
142
+ number: '',
143
+ otp: null,
144
+ error: ''
145
+ })
146
+ const [showCloseModal, setShowCloseModal] = useState({ isOpen: false })
147
+
148
+ // Extract consent handle from query params or state
149
+ const getConsentHandle = () => {
150
+ return decodedInfo?.srcref
151
+ }
152
+
153
+ const consentHandle = getConsentHandle()
154
+
155
+ // Detect mobile screens
156
+ const isMobile = useMediaQuery('(max-width: 768px)')
157
+
158
+ // Get user's selected mobile
159
+ const { user } = useAuthStore()
160
+ const mobileNumber = user?.phoneNumber
161
+
162
+ // FIP data from store
163
+ const {
164
+ fips,
165
+ groupedFips,
166
+ selectedFips,
167
+ activeCategory,
168
+ categories,
169
+ selectedMobileNumber,
170
+ accountsToNotify,
171
+ isLoading: storeLoading,
172
+ error: storeError,
173
+ setSelectedMobileNumber,
174
+ setActiveCategory,
175
+ addSelectedFip,
176
+ removeSelectedFip,
177
+ setSelectedFips,
178
+ clearSelections,
179
+ setIdentifiers,
180
+ setAccountsToNotify
181
+ } = useFipStore()
182
+
183
+ // Fetch FIP data with React Query - use dynamic consent handle
184
+ const {
185
+ refetch,
186
+ isLoading: queryLoading,
187
+ error: queryError
188
+ } = useFipQuery(consentHandle)
189
+
190
+ // Add state to track when we should navigate
191
+ const [shouldNavigate, setShouldNavigate] = useState(false);
192
+
193
+ const totalCategory = decodedInfo?.fipId?.split(',').map(fip => {
194
+ const fipData = fiTypeCategoryMap[activeCategory]?.map((fiType) => {
195
+ const filteredFips = groupedFips[fiType]?.filter(f => f.id === fip);
196
+ return filteredFips?.[0]?.fiTypeList || [];
197
+ }).filter(Boolean);
198
+ return fipData?.flat() || [];
199
+ }).flat() || [];
200
+
201
+ const requiredCategory = fiTypeCategoryMap[activeCategory]?.filter(fiType => decodedInfo?.fiTypesRequiredForConsent?.includes(fiType)) || [];
202
+
203
+ // Effect to handle setting selected FIPs
204
+ useEffect(() => {
205
+ if (!queryLoading && !queryError && decodedInfo?.fipId && activeCategory && requiredCategory.every(fiType => totalCategory?.includes(fiType))) {
206
+ const fipIds = decodedInfo.fipId.split(',');
207
+ setSelectedFips(activeCategory, fipIds);
208
+ setShouldNavigate(true);
209
+ }
210
+ }, [queryLoading, queryError, decodedInfo?.fipId, activeCategory]);
211
+
212
+ // Effect to handle navigation
213
+ useEffect(() => {
214
+ if (shouldNavigate && selectedFips[activeCategory]?.length > 0) {
215
+ handleNext();
216
+ setShouldNavigate(false);
217
+ }
218
+ }, [shouldNavigate]);
219
+
220
+ const getMobileNumbersQuery = useQuery({
221
+ queryKey: ['mobile-numbers'],
222
+ queryFn: async () => accountService.getMobileNumbers()
223
+ })
224
+
225
+ const getMostUsedBanksQuery = useQuery({
226
+ queryKey: ['most-used-banks'],
227
+ queryFn: async () => accountService.getMostUsedBanks({ type: 'DEPOSIT' as string }),
228
+ enabled: !!activeCategory
229
+ })
230
+
231
+ const addNewNoQuery = useMutation({
232
+ mutationFn: async () =>
233
+ accountService.addNewNumber({
234
+ value: showModal?.number || '',
235
+ type: 'MOBILE',
236
+ categoryType: 'STRONG'
237
+ })
238
+ })
239
+
240
+ const verifyNewNoQuery = useMutation({
241
+ mutationFn: async () =>
242
+ accountService.verifyNewNumber({
243
+ phoneNumber: showModal?.number || '',
244
+ code: showModal?.otp || '',
245
+ otpUniqueID: addNewNoQuery?.data?.mobile?.otpUniqueID || '',
246
+ identifierType: 'MOBILE'
247
+ }),
248
+ onSuccess: () => {
249
+ getMobileNumbersQuery.refetch()
250
+ setShowModal({
251
+ isOpen: false,
252
+ title: t('addNewMobileNumber'),
253
+ content: null,
254
+ number: '',
255
+ otp: null,
256
+ error: ''
257
+ })
258
+ },
259
+ onError: error => {
260
+ setShowModal(old => ({ ...old, error: error.response?.data?.errorCode || 'Something went wrong' }))
261
+ // console.error(error)
262
+ }
263
+ })
264
+
265
+ // Combine loading and error states
266
+ const isLoading = storeLoading || queryLoading
267
+ const error = storeError || (queryError as Error)?.message
268
+
269
+ // Set selected mobile number
270
+ useEffect(() => {
271
+ if (mobileNumber) {
272
+ setSelectedMobileNumber(mobileNumber)
273
+ }
274
+ }, [mobileNumber, setSelectedMobileNumber])
275
+
276
+ useEffect(() => {
277
+ if (searchQuery.length) {
278
+ debounce(trackEvent(EVENTS.SEARCH_BANKS, { searchValue: searchQuery }), 300)
279
+ }
280
+ }, [searchQuery])
281
+
282
+ // Determine the active category from URL if not already set
283
+ useEffect(() => {
284
+ const path = location.pathname
285
+ const categoryFromPath = path.split('/').pop()
286
+
287
+ // If the path includes a category name and it matches one of our categories
288
+ if (categoryFromPath) {
289
+ const matchingCategory = categories.find(
290
+ cat => cat.toLowerCase() === categoryFromPath.toLowerCase()
291
+ )
292
+
293
+ if (matchingCategory && matchingCategory !== activeCategory) {
294
+ setActiveCategory(matchingCategory)
295
+ }
296
+ } else if (categories.length > 0 && !activeCategory) {
297
+ // Set first category as active if none is active and redirect
298
+ const firstCategory = categories[0]
299
+ setActiveCategory(firstCategory)
300
+ navigate(`/link-accounts/${firstCategory.toLowerCase()}`, {
301
+ replace: true
302
+ })
303
+ }
304
+ }, [
305
+ location.pathname,
306
+ categories,
307
+ activeCategory,
308
+ setActiveCategory,
309
+ navigate
310
+ ])
311
+
312
+ // Show bottom sheet only when there are selected FIPs in the active category
313
+ const hasSelectedFips =
314
+ activeCategory && selectedFips[activeCategory]?.length > 0
315
+ const showBottomSheet = hasSelectedFips
316
+
317
+ // Get FIPs for the active category
318
+ const getActiveCategoryFips = () => {
319
+ if (!activeCategory) return []
320
+
321
+ // Create a Map to store unique FIPs by their ID
322
+ const uniqueFipsMap = new Map<string, typeof fips[0]>()
323
+
324
+ // Get FIP types for the active category
325
+ const fipTypes = fiTypeCategoryMap[activeCategory] || []
326
+
327
+ // Add FIPs to the Map using ID as key
328
+ fipTypes.forEach(fipType => {
329
+ const categoryFips = groupedFips[fipType] || []
330
+ categoryFips.forEach(fip => {
331
+ if (fip.name && (!searchQuery || fip.name.toLowerCase().includes(searchQuery.toLowerCase()))) {
332
+ // Only add if we haven't seen this ID before
333
+ if (!uniqueFipsMap.has(fip.id)) {
334
+ uniqueFipsMap.set(fip.id, fip)
335
+ }
336
+ }
337
+ })
338
+ })
339
+
340
+ // Convert Map values to array and sort
341
+ const sortedFips = Array.from(uniqueFipsMap.values()).sort((a, b) => {
342
+ if (a.id < b.id) return -1
343
+ if (a.id > b.id) return 1
344
+ return 0
345
+ })
346
+
347
+ // Return enabled FIPs first, then disabled ones
348
+ return [
349
+ ...sortedFips.filter(fip => fip.isEnabled),
350
+ ...sortedFips.filter(fip => !fip.isEnabled)
351
+ ]
352
+ }
353
+
354
+ const handleNext = () => {
355
+ if (!activeCategory) return
356
+
357
+ // Only proceed if user has selected providers
358
+ console.log(" scroll ")
359
+ if (selectedFips[activeCategory]?.length > 0) {
360
+ let redirect = false
361
+ const identifiersList: any[] = []
362
+ fips
363
+ .filter(fip =>
364
+ selectedFips?.[activeCategory as string]?.includes(fip.id as string)
365
+ )
366
+ .forEach(fip => {
367
+ fip.Identifiers?.forEach(identifier => {
368
+ if (!identifiersList.find(i => i.type === identifier.type)) {
369
+ const value =
370
+ identifier.type == 'PAN'
371
+ ? decodedInfo?.pan
372
+ : identifier.type == 'AADHAAR'
373
+ ? decodedInfo?.aadhaar
374
+ : identifier.type == 'MOBILE'
375
+ ? selectedMobileNumber
376
+ : null
377
+ identifiersList.push({ ...identifier, value })
378
+ }
379
+ })
380
+ })
381
+
382
+ setIdentifiers(identifiersList)
383
+ if (identifiersList?.length > 0 && Boolean(identifiersList.find(val => !val.value)?.type)) {
384
+ if (identifiersList.find(i => i.type === 'PAN')) {
385
+ redirect = true
386
+ }
387
+ if (identifiersList.find(i => i.type === 'AADHAAR')) {
388
+ redirect = true
389
+ }
390
+ }
391
+
392
+ if (redirect) {
393
+ navigate(`/link-accounts/discover-account`, {
394
+ state: {
395
+ category: activeCategory,
396
+ selectedFips: selectedFips[activeCategory]
397
+ }
398
+ })
399
+ return null
400
+ }
401
+ // Navigate to the account discovery screen with category info
402
+ navigate(`/link-accounts/discovery`, {
403
+ state: {
404
+ category: activeCategory,
405
+ selectedFips: selectedFips[activeCategory]
406
+ }
407
+ })
408
+ } else {
409
+ // Show some notification that they need to select providers first
410
+ // console.warn('Please select at least one provider')
411
+ }
412
+ }
413
+
414
+ const handleAddNumber = (value: string) => {
415
+ setShowModal(old => ({
416
+ ...old,
417
+ isOpen: true,
418
+ title: t('verifyNewMobileNumber'),
419
+ content: 'otp',
420
+ otp: null,
421
+ error: ''
422
+ }))
423
+ }
424
+
425
+ const handleTabClose = () => {
426
+ window.close()
427
+ }
428
+
429
+ const openAddNumberModal = (currentNumber: string) => {
430
+ setShowModal(prev => ({
431
+ ...prev,
432
+ isOpen: true,
433
+ title: t('addNewMobileNumber'),
434
+ content: <AddNumberModalContent
435
+ showModal={{ ...showModal, number: currentNumber ? currentNumber : showModal.number }}
436
+ setShowModal={setShowModal}
437
+ t={t}
438
+ addNewNoQuery={addNewNoQuery}
439
+ handleAddNumber={(number: string) => handleAddNumber(number)}
440
+ />,
441
+ otp: null,
442
+ error: ''
443
+ }))
444
+ }
445
+
446
+ const toggleFipSelection = (fipId: string) => {
447
+ if (!activeCategory) return
448
+
449
+ if (selectedFips[activeCategory]?.includes(fipId)) {
450
+ removeSelectedFip(activeCategory, fipId)
451
+ } else {
452
+ addSelectedFip(activeCategory, fipId)
453
+ }
454
+ }
455
+
456
+ // Show skeleton loaders during loading
457
+ if ((decodedInfo?.fipId || (queryLoading && shouldNavigate)) && requiredCategory.every(fiType => totalCategory?.includes(fiType))) {
458
+ return (
459
+ <div className='flex flex-col gap-4 pb-16 md:pb-16 md:p-16 sm:p-4'>
460
+ {Array(3)
461
+ .fill(0)
462
+ .map((_, index) => (
463
+ <OuterCard key={`loading-${index}`} selected={false} loading={true}>
464
+ <BankCard bankName='' />
465
+ </OuterCard>
466
+ ))}
467
+ </div>
468
+ )
469
+ }
470
+ if (isLoading) {
471
+ return (
472
+ <div className='flex flex-col gap-4 pb-16 md:pb-16 md:p-16 sm:p-4'>
473
+ <Skeleton className='h-8 w-72 mb-4' />
474
+ <div className='grid grid-cols-1 md:grid-cols-3 gap-4'>
475
+ <Skeleton className='h-16 w-full' />
476
+ <Skeleton className='h-16 w-full' />
477
+ <Skeleton className='h-16 w-full' />
478
+ </div>
479
+ <Skeleton className='h-8 w-72 mt-8 mb-4' />
480
+ <Skeleton className='h-12 w-full mb-4' />
481
+ <Skeleton className='h-4 w-72 mt-0 mb-0' />
482
+ <div className='grid grid-cols-1 md:grid-cols-3 gap-4'>
483
+ {[...Array(6)].map((_, i) => (
484
+ <Skeleton key={i} className='h-16 w-full' />
485
+ ))}
486
+ </div>
487
+ <Skeleton className='h-4 w-72 mt-4 mb-0' />
488
+ <div className='flex flex-col gap-4'>
489
+ {[...Array(6)].map((_, i) => (
490
+ <Skeleton key={i} className='h-16 w-full' />
491
+ ))}
492
+ </div>
493
+ </div>
494
+ )
495
+ }
496
+
497
+ // Show error message if there was an error
498
+ if (error) {
499
+ return (
500
+ <div className='flex flex-col items-center justify-center p-8 h-64'>
501
+ <div className='flex items-center gap-2 text-red-500 mb-4'>
502
+ <AlertCircle size={20} />
503
+ <p>{error}</p>
504
+ </div>
505
+ <AnimatedButton onClick={() => refetch()}>Retry</AnimatedButton>
506
+ </div>
507
+ )
508
+ }
509
+
510
+ // No categories or no FIPs
511
+ if (!categories.length || !Object.keys(groupedFips).length) {
512
+ return (
513
+ <div className='flex flex-col items-center justify-center p-8 h-64'>
514
+ <div className='flex items-center gap-2 text-amber-500 mb-4'>
515
+ <AlertCircle size={20} />
516
+ <p>No financial information providers available</p>
517
+ </div>
518
+ <AnimatedButton onClick={() => refetch()}>Refresh</AnimatedButton>
519
+ </div>
520
+ )
521
+ }
522
+
523
+ const activeCategoryFips = getActiveCategoryFips()
524
+
525
+ // Render different content based on the active category
526
+ const renderCategoryContent = () => {
527
+ if (!activeCategory) return null
528
+
529
+ return (
530
+ <>
531
+
532
+ <SectionTitle
533
+ className='mt-8 mb-2'
534
+ title={
535
+ t('keywords.select').charAt(0).toUpperCase() + t('keywords.select').slice(1)
536
+ +
537
+ ' ' +
538
+ t(`categories.${activeCategory.toLowerCase()}`) +
539
+ ' to ' +
540
+ t('keywords.discover')
541
+ // +
542
+ // ' ' +
543
+ // t('keywords.your') +
544
+ // ' ' +
545
+ // t('keywords.accounts')
546
+ }
547
+ />
548
+ <Input
549
+ type='text'
550
+ placeholder={t('Search')}
551
+ value={searchQuery}
552
+ onChange={e => setSearchQuery(e.target.value)}
553
+ leftSection={
554
+ <Search
555
+ className='h-[18px] w-[18px] text-consent-secondary'
556
+ name='search'
557
+ />
558
+ }
559
+ className='h-12 focus-visible:border-primary'
560
+ />
561
+
562
+ {activeCategory === 'BANK' && searchQuery?.trim().length === 0 && <div className='flex flex-col gap-2 mt-6'>
563
+ {getMostUsedBanksQuery?.data?.['BANK']?.length > 0 && <Label className='text-sm md:text-md font-medium text-consent-secondary'>Most picked</Label>}
564
+ <div className='grid grid-cols-1 md:grid-cols-3 gap-3'>
565
+ {getMostUsedBanksQuery?.data?.['BANK']?.map((fip: any) => (
566
+ <OuterCard
567
+ hoverEffect={false}
568
+ onSelect={() => {
569
+ if (fip.isEnabled) {
570
+ toggleFipSelection(fip.id)
571
+ }
572
+ }}
573
+ key={`fip-${fip.id}`}
574
+ className='cursor-pointer'
575
+ selected={selectedFips[activeCategory]?.includes(fip.id)}
576
+ >
577
+ <BankCard
578
+ loading={false}
579
+ bankName={fip.name || 'Unknown Provider'}
580
+ selectedTextColor=''
581
+ badgeText={false}
582
+ subText={false}
583
+ commingSoon={false}
584
+ rightSection={
585
+ <Checkbox
586
+ id={`checkbox-${fip.id}`}
587
+ className='cursor-pointer size-6'
588
+ checked={
589
+ selectedFips[activeCategory]?.includes(fip.id) ||
590
+ false
591
+ }
592
+ onCheckedChange={() => toggleFipSelection(fip.id)}
593
+ />
594
+ }
595
+ image={fip.logoUrl || '/images/bank-logo.png'}
596
+ imageSize='w-8 h-8'
597
+ className='cursor-pointer'
598
+ />
599
+
600
+ </OuterCard>
601
+ ))}
602
+
603
+ </div>
604
+ </div>}
605
+
606
+ <div className='flex flex-col gap-2 mt-6'>
607
+ {activeCategoryFips.length > 0 && <Label className='text-sm md:text-md font-medium text-consent-secondary'>All financial information providers</Label>}
608
+ {activeCategoryFips.length === 0 ? (
609
+ <div className='text-center p-4 border rounded-lg border-dashed border-gray-300 text-gray-500'>
610
+ No {formatCategoryName(activeCategory)} providers found
611
+ </div>
612
+ ) : (
613
+ <OuterCard hoverEffect={false} noAction={true}>
614
+ <div className='flex flex-col gap-4 w-full py-2'>
615
+ {activeCategoryFips.map((fip, index) => (
616
+ <>
617
+ <BankCard
618
+ loading={false}
619
+ bankName={fip.name || 'Unknown Provider'}
620
+ key={`fip-${fip.id}`}
621
+ selected={selectedFips[activeCategory]?.includes(fip.id)}
622
+ selectedTextColor=''
623
+ onClick={() => {
624
+ if (fip.isEnabled) {
625
+ toggleFipSelection(fip.id)
626
+ }
627
+ }}
628
+ subText={false}
629
+ badgeText={fip.isbadge ? 'New' : false}
630
+ commingSoon={!fip.isEnabled}
631
+ notify={accountsToNotify.includes(fip?.id)}
632
+ commingSoonFun={() => {
633
+ setAccountsToNotify(fip?.id)
634
+ trackEvent(EVENTS.NOTIFY_UPDATES, { searchValue: searchQuery, fipId: fip?.id, userId: decodedInfo?.userid })
635
+ }}
636
+ rightSection={
637
+ <Checkbox
638
+ id={`checkbox-${fip.id}`}
639
+ className='cursor-pointer size-6'
640
+ checked={
641
+ selectedFips[activeCategory]?.includes(fip.id) ||
642
+ false
643
+ }
644
+ onCheckedChange={() => toggleFipSelection(fip.id)}
645
+ />
646
+ }
647
+ image={fip.logoUrl || '/images/bank-logo.png'}
648
+ imageSize='w-8 h-8'
649
+ className='cursor-pointer'
650
+ />
651
+ {index !== activeCategoryFips.length - 1 && (
652
+ <div className='h-1 border-b border-gray-100 md:border-gray-200 dark:border-border' />
653
+ )}
654
+ </>
655
+ ))}
656
+ </div>
657
+ </OuterCard>
658
+ )}
659
+ </div>
660
+ </>
661
+ )
662
+ }
663
+
664
+ const renderAddNumberButton = () => {
665
+ if (getMobileNumbersQuery?.data?.length >= 3) return null;
666
+
667
+ const button = (
668
+ <AnimatedButton
669
+ variant={'text'}
670
+ size={'text'}
671
+ className='text-primary'
672
+ onClick={() => openAddNumberModal(showModal.number)}
673
+ >
674
+ <PlusIcon /> {t('addNumber')}
675
+ </AnimatedButton>
676
+ );
677
+
678
+ if (isMobile) return button;
679
+
680
+ return (
681
+ <Modal
682
+ title={showModal.title}
683
+ open={showModal.isOpen}
684
+ className='w-[380px]'
685
+ onOpenChange={open =>
686
+ !open &&
687
+ setShowModal({
688
+ isOpen: open,
689
+ title: '',
690
+ content: null,
691
+ number: '',
692
+ otp: null,
693
+ error: ''
694
+ })
695
+ }
696
+ >
697
+ <Modal.Trigger>
698
+ {button}
699
+ </Modal.Trigger>
700
+ {showModal.content == 'otp' ? <div className='flex flex-col items-center gap-4'>
701
+ <OTPInputComponent
702
+ maxLength={4}
703
+ title=''
704
+ editable={true}
705
+ mobileNumber={showModal.number}
706
+ onResend={() => {
707
+ setShowModal(old => ({ ...old, otp: null, error: '' }))
708
+ addNewNoQuery.mutate(undefined, {
709
+ onSuccess: () => {
710
+ setShowModal(old => ({ ...old, otp: null, error: '' }))
711
+ },
712
+ onError: (error: Error) => {
713
+ setShowModal(old => ({ ...old, otp: null, error: error.response?.data?.errorMsg || 'Something went wrong' }))
714
+ // console.error(error)
715
+ }
716
+ })
717
+ }}
718
+ countdownTime={18}
719
+ error={Boolean(showModal.error)}
720
+ errorMessage={showModal.error}
721
+ onChange={otp => {
722
+ setShowModal(old => ({ ...old, otp, error: '' }))
723
+ }}
724
+ handleEdit={() => {
725
+ setShowModal(old => ({ ...old, otp: null, error: '' }))
726
+ openAddNumberModal(showModal.number)
727
+ }}
728
+ />
729
+ <AnimatedButton
730
+ className='w-full h-11'
731
+ onClick={() => {
732
+ verifyNewNoQuery.mutate()
733
+ }}
734
+ >
735
+ Next
736
+ </AnimatedButton>
737
+ </div> : showModal.content}
738
+ </Modal>
739
+ );
740
+ };
741
+
742
+ return (
743
+ <div className='flex flex-col gap-1 pb-16 md:pb-16'>
744
+ <div className='flex flex-col gap-1 w-full md:px-14 md:mt-10 sm:px-4'>
745
+ {/* Mobile number selection */}
746
+ <SectionTitle
747
+ className='md:mt-4 sm:mt-1'
748
+ title={`${t('mobileNo')}*`}
749
+ rightSection={renderAddNumberButton()}
750
+ />
751
+ {/* <Label className='text-sm text-consent-secondary'>{t('mobileSubTitle')}</Label> */}
752
+ <RadioGroup
753
+ value={selectedMobileNumber}
754
+ onValueChange={setSelectedMobileNumber}
755
+ >
756
+ <div className='grid md:grid-cols-3 sm:grid-cols-1 gap-4'>
757
+ {getMobileNumbersQuery?.data?.map(
758
+ (item: { value: string }, index: number) => (
759
+ <OuterCard
760
+ key={`${item.value} - ${index}`}
761
+ selected={selectedMobileNumber === item.value.toString()}
762
+ onSelect={() =>
763
+ setSelectedMobileNumber(item.value.toString())
764
+ }
765
+ >
766
+ <div className='flex items-center justify-between w-full gap-2'>
767
+ <p
768
+ className={`text-md truncate font-medium ${selectedMobileNumber === item.value.toString()
769
+ ? 'text-primary'
770
+ : ' dark:text-gray-400'
771
+ }`}
772
+ >
773
+ +91 {item.value}
774
+ </p>
775
+ <RadioGroupItem
776
+ value={item.value.toString()}
777
+ id={`radio-${item.value}`}
778
+ className={`cursor-pointer`}
779
+ />
780
+ </div>
781
+ </OuterCard>
782
+ )
783
+ )}
784
+ </div>
785
+ </RadioGroup>
786
+ <p className='text-sm md:text-lg font-medium text-placeholder mt-2'>
787
+ {t('youCanAddUpTo3Numbers')}
788
+ </p>
789
+
790
+ {renderCategoryContent()}
791
+ <DummyFooter show={true} />
792
+ </div>
793
+
794
+ {/* Bottom sheet for mobile */}
795
+ <MobileFooter show={!!(isMobile && showBottomSheet)}>
796
+ <div className='flex flex-col items-center w-full gap-4'>
797
+ <AnimatedButton onClick={handleNext} size='lg' className='w-full h-[50px]'>
798
+ {t('discoverAccounts')}
799
+ </AnimatedButton>
800
+ <AnimatedButton
801
+ // onClick={() => setActiveCategory(null)}
802
+ onClick={() => clearSelections()}
803
+ variant={'ghost'}
804
+ >
805
+ {t('cancel')}
806
+ </AnimatedButton>
807
+ </div>
808
+ </MobileFooter>
809
+
810
+ {/* Footer for desktop */}
811
+ <WebFooter show={!!(!isMobile && showBottomSheet)}>
812
+ <AnimatedButton
813
+ onClick={() => clearSelections()}
814
+ variant={'ghost'}
815
+ size={'lg'}
816
+ className='text-primary'
817
+ >
818
+ {t('cancel')}
819
+ </AnimatedButton>
820
+ <AnimatedButton onClick={handleNext} size='lg'>
821
+ {t('discoverAccounts')}
822
+ </AnimatedButton>
823
+ </WebFooter>
824
+
825
+ <BottomSheet
826
+ isOpen={isMobile && showModal.isOpen}
827
+ onClose={() => setShowModal(prev => ({ ...prev, isOpen: false, error: '' }))}
828
+ title={showModal.title}
829
+ height='auto'
830
+ showHandle={true}
831
+ showCloseButton={false}
832
+ >
833
+ {showModal.content == 'otp' ? <div className='flex flex-col items-center gap-4'>
834
+ <OTPInputComponent
835
+ maxLength={4}
836
+ title=''
837
+ editable={true}
838
+ mobileNumber={showModal.number}
839
+ onResend={() => {
840
+ setShowModal(old => ({ ...old, otp: null, error: '' }))
841
+ addNewNoQuery.mutate(undefined, {
842
+ onSuccess: () => {
843
+ setShowModal(old => ({ ...old, otp: null, error: '' }))
844
+ },
845
+ onError: (error: Error) => {
846
+ setShowModal(old => ({ ...old, otp: null, error: error.response?.data?.errorMsg || 'Something went wrong' }))
847
+ // console.error(error)
848
+ }
849
+ })
850
+ }}
851
+ countdownTime={18}
852
+ error={Boolean(showModal.error)}
853
+ errorMessage={showModal.error}
854
+ onChange={otp => {
855
+ setShowModal(old => ({ ...old, otp, error: '' }))
856
+ }}
857
+ handleEdit={() => {
858
+ setShowModal(old => ({ ...old, otp: null, error: '' }))
859
+ openAddNumberModal(showModal.number)
860
+ }}
861
+ />
862
+ <AnimatedButton
863
+ className='w-full h-11'
864
+ onClick={() => {
865
+ verifyNewNoQuery.mutate()
866
+ }}
867
+ >
868
+ Next
869
+ </AnimatedButton>
870
+ </div> : showModal.content}
871
+ </BottomSheet>
872
+
873
+ <Modal
874
+ open={showCloseModal.isOpen}
875
+ withCloseIcon={false}
876
+ onOpenChange={open => !open && setShowCloseModal({ isOpen: open })}
877
+ className='w-[380px]'
878
+ >
879
+ <div className='flex flex-col gap-4 items-center'>
880
+ <div className='flex items-center justify-center bg-red-100 p-2.5 rounded-full'>
881
+ <InfoIcon className='h-[28px] w-[28px] text-red-700' />
882
+ </div>
883
+ <div className='flex flex-col gap-2 items-center text-center mt-2'>
884
+ <p className='text-xl font-semibold'>
885
+ Are you sure you want to exit?
886
+ </p>
887
+ <p className='text-sm text-muted-secondary'>
888
+ By clicking on exit you decide not to share any financial accounts
889
+ </p>
890
+ </div>
891
+ <div className='flex gap-2 w-full items-center justify-center'>
892
+ <AnimatedButton
893
+ size='lg'
894
+ onClick={() => setShowCloseModal({ isOpen: false })}
895
+ >
896
+ No, go back!
897
+ </AnimatedButton>
898
+ <AnimatedButton
899
+ size='lg'
900
+ variant='ghost'
901
+ className='hover:bg-red-100 hover:text-red-700 text-red-700'
902
+ onClick={handleTabClose}
903
+ >
904
+ Yes, exit
905
+ </AnimatedButton>
906
+ </div>
907
+ </div>
908
+ </Modal>
909
+ </div>
910
+ )
911
+ }
912
+
913
+ export default LinkAccounts