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,751 @@
1
+ import AlertComp from '@/components/alert/alert'
2
+ import OuterCard from '@/components/cards/OuterCard'
3
+ import SectionTitle from '@/components/title/SectionTitle'
4
+ import { AnimatedButton } from '@/components/ui/animatedButton'
5
+ import { Input } from '@/components/ui/input'
6
+ import { Label } from '@/components/ui/label'
7
+ import { MobileFooter } from '@/components/ui/mobile-footer'
8
+ import Modal from '@/components/ui/modal'
9
+ import { OTPInputComponent } from '@/components/ui/otp-input'
10
+ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
11
+ import WebFooter from '@/components/ui/web-footer'
12
+ import { Info, PlusIcon, X } from 'lucide-react'
13
+ import { useEffect, useMemo, useState } from 'react'
14
+ import { useNavigate } from 'react-router-dom'
15
+ import { trackEvent, EVENTS } from '@/utils/posthog'
16
+ import { useTranslation } from 'react-i18next'
17
+ import { useMutation, useQuery } from '@tanstack/react-query'
18
+ import { accountService } from '@/services/api'
19
+ import { useMediaQuery } from '@/hooks/use-media-query'
20
+ import { useFipStore } from '@/store/fip.store'
21
+ import logo from '../../assets/brand/saafe-logo.svg'
22
+ import { z } from 'zod'
23
+ import { Calendar } from '@/components/ui/calendar'
24
+ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
25
+ import { CalendarIcon } from 'lucide-react'
26
+ import { DropdownNavProps } from 'react-day-picker'
27
+ import {
28
+ Select,
29
+ SelectContent,
30
+ SelectItem,
31
+ SelectTrigger,
32
+ SelectValue,
33
+ } from "@/components/ui/select";
34
+ import { DropdownProps } from 'react-day-picker'
35
+ import { useSetPageTitle } from '@/hooks/use-page-title'
36
+ import { useRedirectStore } from '@/store/redirect.store'
37
+ import { BottomSheet } from '@/components/ui/bottom-sheet'
38
+
39
+ interface DiscoverAccountProps {
40
+ state?: {
41
+ category: string
42
+ selectedFips?: string[]
43
+ }
44
+ currentCategory?: string | null
45
+ }
46
+
47
+ const AddNumberModalContent = ({ showModal, setShowModal, t, addNewNoQuery, handleAddNumber }: {
48
+ showModal: { number: string; error: string };
49
+ setShowModal: React.Dispatch<React.SetStateAction<{
50
+ isOpen: boolean;
51
+ title: string;
52
+ content: React.ReactNode | null;
53
+ number: string;
54
+ otp: string | null;
55
+ error: string;
56
+ }>>;
57
+ t: (key: string) => string;
58
+ addNewNoQuery: {
59
+ mutate: (data: undefined, options: {
60
+ onSuccess: () => void;
61
+ onError: (error: Error) => void;
62
+ }) => void;
63
+ };
64
+ handleAddNumber: (number: string) => void;
65
+ }) => {
66
+ const [inputValue, setInputValue] = useState({ number: showModal.number, error: showModal.error });
67
+
68
+ useEffect(() => {
69
+ setInputValue({ number: showModal.number, error: showModal.error });
70
+ }, [showModal.number]);
71
+
72
+ return useMemo(() => (
73
+ <div className='flex flex-col items-center gap-5'>
74
+ <p className='text-base font-normal dark:text-muted-secondary'>
75
+ {t('pleaseEnterTheNewNumberYouWishToLinkBelow')}
76
+ </p>
77
+ <Input
78
+ type="tel"
79
+ inputMode="numeric"
80
+ className='h-12'
81
+ placeholder={t('mobileNoPlaceholder')}
82
+ maxLength={10}
83
+ value={inputValue.number}
84
+ onChange={e => {
85
+ const newValue = e.target.value;
86
+ const numericValue = newValue.replace(/[^0-9]/g, '');
87
+ setInputValue({ number: numericValue, error: '' });
88
+ setShowModal(prev => ({
89
+ ...prev,
90
+ number: numericValue,
91
+ error: numericValue.length === 10 ? '' : 'Please enter a valid 10-digit mobile number'
92
+ }));
93
+ }}
94
+ />
95
+ {inputValue.error && (
96
+ <p className="text-red-500 text-sm mt-[-20px]">{inputValue.error}</p>
97
+ )}
98
+ <AnimatedButton
99
+ className='w-full h-11 mt-2'
100
+ onClick={() => {
101
+ if (inputValue.number.length !== 10) {
102
+ setInputValue({ number: inputValue.number, error: 'Please enter a valid 10-digit mobile number' });
103
+ setShowModal(prev => ({ ...prev, error: 'Please enter a valid 10-digit mobile number' }));
104
+ return;
105
+ }
106
+ addNewNoQuery.mutate(undefined, {
107
+ onSuccess: () => {
108
+ handleAddNumber(inputValue.number)
109
+ },
110
+ onError: (error: Error) => {
111
+ // console.error(error)
112
+ setInputValue({ number: inputValue.number, error: error.response?.data?.errorCode || 'Something went wrong' });
113
+ }
114
+ })
115
+ }}
116
+ >
117
+ {t('login.getOtp')}
118
+ </AnimatedButton>
119
+ </div>
120
+ ), [inputValue, showModal.error, t, setShowModal, addNewNoQuery, handleAddNumber])
121
+ }
122
+
123
+ const aadhaarSchema = z.object({
124
+ value: z.string()
125
+ .min(12, 'Aadhaar number must be 12 digits')
126
+ .max(12, 'Aadhaar number must be 12 digits')
127
+ .regex(/^\d+$/, 'Aadhaar number must contain only digits')
128
+ })
129
+
130
+ const panSchema = z.object({
131
+ value: z.string()
132
+ .min(10, 'PAN number must be 10 characters')
133
+ .max(10, 'PAN number must be 10 characters')
134
+ .regex(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/, 'Invalid PAN format. Format: ABCDE1234F')
135
+ })
136
+
137
+ const DiscoverAccount = ({ state, currentCategory }: DiscoverAccountProps) => {
138
+ useSetPageTitle('Discover accounts')
139
+ const navigate = useNavigate()
140
+ const { decodedInfo } = useRedirectStore()
141
+ const category = state?.category || currentCategory || 'BANKS'
142
+ const { t } = useTranslation()
143
+ const [validationError, setValidationError] = useState<{ [key: string]: string | null }>({
144
+ PAN: null,
145
+ AADHAAR: null
146
+ })
147
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false)
148
+ const [showAlert, setShowAlert] = useState(true)
149
+
150
+ // Format category name for display
151
+ const formatCategoryName = (name: string) => {
152
+ return name
153
+ .split('_')
154
+ .map(word => word.charAt(0) + word.slice(1).toLowerCase())
155
+ .join(' ')
156
+ }
157
+ const [showModal, setShowModal] = useState({
158
+ isOpen: false,
159
+ title: t('addNewMobileNumber'),
160
+ content: null as React.ReactNode,
161
+ number: '',
162
+ otp: null as string | null
163
+ })
164
+
165
+ // Detect mobile screens
166
+ const isMobile = useMediaQuery('(max-width: 768px)')
167
+
168
+ // FIP data from store
169
+ const {
170
+ selectedMobileNumber,
171
+ setSelectedMobileNumber,
172
+ identifiers,
173
+ setIdentifiers
174
+ } = useFipStore()
175
+
176
+ const getMobileNumbersQuery = useQuery({
177
+ queryKey: ['mobile-numbers'],
178
+ queryFn: async () => accountService.getMobileNumbers()
179
+ })
180
+
181
+ const addNewNoQuery = useMutation({
182
+ mutationFn: async () =>
183
+ accountService.addNewNumber({
184
+ value: showModal?.number || '',
185
+ type: 'MOBILE',
186
+ categoryType: 'STRONG'
187
+ }),
188
+ onSuccess: () => {
189
+ getMobileNumbersQuery.refetch()
190
+ setShowModal(old => ({ ...old, otp: null, error: '' }))
191
+ },
192
+ onError: error => {
193
+ setShowModal(old => ({ ...old, otp: null, error: error.response?.data?.errorCode || 'Something went wrong' }))
194
+ }
195
+ })
196
+
197
+ const verifyNewNoQuery = useMutation({
198
+ mutationFn: async () =>
199
+ accountService.verifyNewNumber({
200
+ phoneNumber: showModal?.number || '',
201
+ code: showModal?.otp || '',
202
+ otpUniqueID: addNewNoQuery?.data?.mobile?.otpUniqueID || '',
203
+ identifierType: 'MOBILE'
204
+ }),
205
+ onSuccess: () => {
206
+ getMobileNumbersQuery.refetch()
207
+ setShowModal({
208
+ isOpen: false,
209
+ title: 'Add new mobile number',
210
+ content: null,
211
+ number: '',
212
+ otp: null
213
+ })
214
+ },
215
+ onError: error => {
216
+ setShowModal(old => ({ ...old, otp: null, error: error.response?.data?.errorCode || 'Something went wrong' }))
217
+ // console.error(error)
218
+ }
219
+ })
220
+
221
+
222
+ const handleAddNumber = (value: string) => {
223
+
224
+ setShowModal(old => ({
225
+ ...old,
226
+ isOpen: true,
227
+ title: t('verifyNewMobileNumber'),
228
+ content: 'otp',
229
+ otp: null,
230
+ error: ''
231
+ }))
232
+ }
233
+
234
+ const openAddNumberModal = (currentNumber: string) => {
235
+ setShowModal(prev => ({
236
+ ...prev,
237
+ isOpen: true,
238
+ title: t('addNewMobileNumber'),
239
+ content: <AddNumberModalContent
240
+ showModal={{ ...showModal, number: currentNumber ? currentNumber : showModal.number }}
241
+ setShowModal={setShowModal}
242
+ t={t}
243
+ addNewNoQuery={addNewNoQuery}
244
+ handleAddNumber={(number: string) => handleAddNumber(number)}
245
+ />,
246
+ otp: null,
247
+ error: ''
248
+ }))
249
+ }
250
+
251
+ const handleDiscoverAccount = () => {
252
+ trackEvent(EVENTS.DISCOVER_ACCOUNT, { category })
253
+ // After discovering accounts, go back to account selection page
254
+ const result = [...identifiers]
255
+ const mobileIndex = result.findIndex(i => i.type === 'MOBILE')
256
+ result.splice(mobileIndex, 1, {
257
+ ...result[mobileIndex],
258
+ value: selectedMobileNumber
259
+ })
260
+ navigate('/link-accounts/discovery', {
261
+ state: {
262
+ category,
263
+ selectedFips: state?.selectedFips,
264
+ fromDiscovery: true
265
+ }
266
+ })
267
+ }
268
+
269
+ const handleCancel = () => {
270
+ // Go back to account selection
271
+ navigate('/link-accounts/banks', {
272
+ state: {
273
+ category,
274
+ selectedFips: state?.selectedFips
275
+ }
276
+ })
277
+ }
278
+
279
+ const handleCalendarChange = (
280
+ _value: string | number,
281
+ _e: React.ChangeEventHandler<HTMLSelectElement>,
282
+ ) => {
283
+ const _event = {
284
+ target: {
285
+ value: String(_value),
286
+ },
287
+ } as React.ChangeEvent<HTMLSelectElement>;
288
+ _e(_event);
289
+ };
290
+
291
+ const renderAddNumberButton = () => {
292
+ if (getMobileNumbersQuery?.data?.length >= 3) return null;
293
+
294
+ const button = (
295
+ <AnimatedButton
296
+ variant={'text'}
297
+ size={'text'}
298
+ className='text-primary'
299
+ onClick={() => openAddNumberModal(showModal.number)}
300
+ >
301
+ <PlusIcon /> {t('addNumber')}
302
+ </AnimatedButton>
303
+ );
304
+
305
+ if (isMobile) return button;
306
+
307
+ return (
308
+ <Modal
309
+ title={showModal.title}
310
+ open={showModal.isOpen}
311
+ className='w-[380px]'
312
+ onOpenChange={open =>
313
+ !open &&
314
+ setShowModal({
315
+ isOpen: open,
316
+ title: '',
317
+ content: null,
318
+ number: '',
319
+ otp: null,
320
+ error: ''
321
+ })
322
+ }
323
+ >
324
+ <Modal.Trigger>
325
+ {button}
326
+ </Modal.Trigger>
327
+ {showModal.content == 'otp' ? <div className='flex flex-col items-center gap-4'>
328
+ <OTPInputComponent
329
+ maxLength={4}
330
+ title=''
331
+ editable={true}
332
+ mobileNumber={showModal.number}
333
+ onResend={() => {
334
+ setShowModal(old => ({ ...old, otp: null, error: '' }))
335
+ addNewNoQuery.mutate(undefined, {
336
+ onSuccess: () => {
337
+ setShowModal(old => ({ ...old, otp: null, error: '' }))
338
+ },
339
+ onError: (error: Error) => {
340
+ setShowModal(old => ({ ...old, otp: null, error: error.response?.data?.errorMsg || 'Something went wrong' }))
341
+ // console.error(error)
342
+ }
343
+ })
344
+ }}
345
+ countdownTime={18}
346
+ error={Boolean(showModal.error)}
347
+ errorMessage={showModal.error}
348
+ onChange={otp => {
349
+ setShowModal(old => ({ ...old, otp, error: '' }))
350
+ }}
351
+ handleEdit={() => {
352
+ setShowModal(old => ({ ...old, otp: null, error: '' }))
353
+ openAddNumberModal(showModal.number)
354
+ }}
355
+ />
356
+ <AnimatedButton
357
+ className='w-full h-11'
358
+ onClick={() => {
359
+ verifyNewNoQuery.mutate()
360
+ }}
361
+ >
362
+ Next
363
+ </AnimatedButton>
364
+ </div> : showModal.content}
365
+ </Modal>
366
+ );
367
+ };
368
+
369
+ return (
370
+ <div>
371
+ <div className='flex flex-col gap-1 w-full md:px-14 px-0 gap-2 md:mt-6 mt-0'>
372
+ {showAlert &&
373
+ <div className='mb-4'>
374
+ <AlertComp
375
+ icon={
376
+ <div className='flex items-center justify-center bg-yellow-600 p-2 rounded-full'>
377
+ <Info
378
+ strokeWidth='2.3'
379
+ className='h-[18px] w-[18px] text-white'
380
+ />
381
+ </div>
382
+ }
383
+ title={t('noAccountsFound')}
384
+ description={t('noAccountsFoundDescription')}
385
+ rightSection={
386
+ <AnimatedButton variant={'ghost'} size={'sm'} onClick={() => setShowAlert(false)}>
387
+ <X />
388
+ </AnimatedButton>
389
+ }
390
+ />
391
+ </div>
392
+ }
393
+ <div className='flex items-center justify-between gap-2'>
394
+ <p className='text-md md:text-2xl font-semibold text-black dark:text-white'>
395
+ {t('keywords.Discover') +
396
+ ' ' +
397
+ t('keywords.your') +
398
+ ' ' +
399
+ t(`keywords.accounts`)}
400
+ </p>
401
+ <Modal title={false} withCloseIcon={true} className='w-[450px]'>
402
+ <Modal.Trigger>
403
+ <AnimatedButton variant={'ghost'} size={'sm'}>
404
+ <Info />
405
+ </AnimatedButton>
406
+ </Modal.Trigger>
407
+ <div className='flex flex-col items-center gap-4'>
408
+ <div className='flex items-center justify-center bg-gray-200/50 dark:bg-gray-100/50 p-3 rounded-full'>
409
+ <Info className='h-[32px] w-[32px]' />
410
+ </div>
411
+ <p className='text-[20px] md:text-2xl text-gray-900 font-semibold dark:text-white'>
412
+ Secure & Temporary Data Use
413
+ </p>
414
+ <Label className='text-sm md:text-lg text-gray-500 dark:text-gray-400 font-light'>
415
+ We use your PAN and Date of Birth only to discover your
416
+ accounts. This information is not stored and is used solely for
417
+ this process
418
+ </Label>
419
+ <Modal.Close className='w-full'>
420
+ <AnimatedButton className='w-full'>Yes, I Understand</AnimatedButton>
421
+ </Modal.Close>
422
+ </div>
423
+ </Modal>
424
+ </div>
425
+ <p className='text-sm md:text-lg text-consent-secondary mt-0 md:mt-1'>
426
+ Enter the following details as linked to your{' '}
427
+ {formatCategoryName(category).toLowerCase()} accounts
428
+ </p>
429
+ <SectionTitle
430
+ className='mt-4 mb-1'
431
+ title={t('mobileNo')}
432
+ rightSection={renderAddNumberButton()}
433
+ />
434
+ <Label className='text-sm text-consent-secondary font-light'>{t('mobileSubTitle')}</Label>
435
+ <div className='flex flex-col gap-1 w-full gap-2'>
436
+ <RadioGroup
437
+ value={selectedMobileNumber}
438
+ onValueChange={setSelectedMobileNumber}
439
+ >
440
+ <div className='grid md:grid-cols-3 sm:grid-cols-1 gap-4'>
441
+ {getMobileNumbersQuery?.data?.map(
442
+ (item: { value: string }, index: number) => (
443
+ <OuterCard
444
+ key={`${item.value} - ${index}`}
445
+ selected={selectedMobileNumber === item.value.toString()}
446
+ onSelect={() =>
447
+ setSelectedMobileNumber(item.value.toString())
448
+ }
449
+ >
450
+ <div className='flex items-center justify-between w-full gap-2'>
451
+ <p
452
+ className={`text-md truncate font-medium ${selectedMobileNumber == item.value.toString()
453
+ ? 'text-primary'
454
+ : ''
455
+ }`}
456
+ >
457
+ {item.value}
458
+ </p>
459
+ <RadioGroupItem
460
+ value={item.value.toString()}
461
+ id={`radio-${item.value}`}
462
+ />
463
+ </div>
464
+ </OuterCard>
465
+ )
466
+ )}
467
+ </div>
468
+ </RadioGroup>
469
+ <p className='text-sm text-muted-secondary font-light mt-1'>
470
+ {t('youCanAddUpTo3Numbers')}
471
+ </p>
472
+ </div>
473
+ {identifiers?.find(i => i.type == 'DOB') ? (
474
+ <>
475
+ <SectionTitle className='mt-6' title={`DOB*`} />
476
+ <div className='flex flex-col gap-1 md:w-[50%] w-full gap-2'>
477
+ <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
478
+ <PopoverTrigger asChild>
479
+ <div className='flex h-[56px] w-full items-center justify-between rounded-md border bg-white border-gray-200 dark:border-gray-700 dark:bg-input/30 px-3 py-2 text-consent-primary dark:border-gray-700'>
480
+ {identifiers?.find(i => i.type === 'DOB')?.value ? (
481
+ <span>{new Date(identifiers.find(i => i.type === 'DOB')?.value || '').toLocaleDateString('en-IN', { year: 'numeric', month: 'long', day: 'numeric' })}</span>
482
+ ) : (
483
+ <span className='text-muted-foreground'>Pick a date</span>
484
+ )}
485
+ <CalendarIcon className='h-4 w-4 opacity-50' />
486
+ </div>
487
+
488
+ </PopoverTrigger>
489
+ <PopoverContent className='w-auto p-0' align='start'>
490
+ <Calendar
491
+ mode='single'
492
+ captionLayout="dropdown"
493
+ hideNavigation
494
+
495
+ components={{
496
+ DropdownNav: (props: DropdownNavProps) => {
497
+ return <div className="flex w-full items-center gap-2">{props.children}</div>;
498
+ },
499
+
500
+ Dropdown: (props: DropdownProps) => {
501
+ return (
502
+ <Select
503
+ value={String(props.value)}
504
+ onValueChange={(value) => {
505
+ if (props.onChange) {
506
+ handleCalendarChange(value, props.onChange);
507
+ }
508
+ }}
509
+ >
510
+ <SelectTrigger className="h-8 w-fit font-medium first:grow">
511
+ <SelectValue />
512
+ </SelectTrigger>
513
+ <SelectContent className="max-h-[min(26rem,var(--radix-select-content-available-height))]">
514
+ {props.options?.map((option) => (
515
+
516
+ <SelectItem
517
+ key={option.value}
518
+ value={String(option.value)}
519
+ disabled={option.disabled}
520
+ >
521
+ {option.label}
522
+ </SelectItem>
523
+ ))}
524
+ </SelectContent>
525
+ </Select>
526
+ );
527
+ },
528
+ }}
529
+ selected={identifiers?.find(i => i.type === 'DOB')?.value ? new Date(identifiers.find(i => i.type === 'DOB')?.value || '') : undefined}
530
+ onSelect={(date) => {
531
+ if (date) {
532
+ const result = [...identifiers]
533
+ result.splice(
534
+ result.findIndex(i => i.type === 'DOB'),
535
+ 1,
536
+ {
537
+ ...identifiers?.find(i => i.type === 'DOB'),
538
+ value: date.toLocaleString().split(',')[0]
539
+ }
540
+ )
541
+ setIdentifiers(result)
542
+ setIsPopoverOpen(false)
543
+ }
544
+ }}
545
+ initialFocus
546
+ />
547
+ </PopoverContent>
548
+ </Popover>
549
+ </div>
550
+ </>
551
+ ) : null}
552
+ {identifiers?.find(i => i.type == 'PAN') ? (
553
+ <>
554
+ <SectionTitle className='mt-6' title={`PAN*`} />
555
+ <div className='flex flex-col gap-1 md:w-[50%] w-full gap-2'>
556
+ <Input
557
+ type='text'
558
+ className={`h-[56px] bg-white border-gray-200 dark:border-gray-700 text-consent-primary text-md ${validationError.PAN ? 'border-red-500' : ''}`}
559
+ placeholder={`Enter PAN`}
560
+ value={identifiers?.find(i => i.type === 'PAN')?.value}
561
+ readOnly={decodedInfo?.pan ? true : false}
562
+ onChange={e => {
563
+ setValidationError(prev => ({ ...prev, PAN: null }))
564
+ const panValue = e.target.value.replace(/[^A-Za-z0-9]/g, '').toUpperCase()
565
+ const result = [...identifiers]
566
+ result.splice(
567
+ result.findIndex(i => i.type === 'PAN'),
568
+ 1,
569
+ {
570
+ ...identifiers?.find(i => i.type === 'PAN'),
571
+ value: panValue
572
+ }
573
+ )
574
+ setIdentifiers(result)
575
+
576
+ // Only validate when input reaches max length
577
+ if (panValue.length >= 10) {
578
+ try {
579
+ panSchema.parse({ value: panValue })
580
+ } catch (error) {
581
+ if (error instanceof z.ZodError) {
582
+ setValidationError(prev => ({ ...prev, PAN: error.errors[0].message }))
583
+ // console.error('PAN validation error:', error.errors[0].message)
584
+ }
585
+ }
586
+ }
587
+ }}
588
+ />
589
+ {validationError.PAN && (
590
+ <p className="text-sm text-red-500 mt-1">{validationError.PAN}</p>
591
+ )}
592
+ </div>
593
+ </>
594
+ ) : null}
595
+ {identifiers?.find(i => i.type == 'AADHAAR') ? (
596
+ <>
597
+ <SectionTitle className='mt-6' title={`AADHAAR*`} />
598
+ <div className='flex flex-col gap-1 md:w-[50%] w-full gap-2'>
599
+ <Input
600
+ type='text'
601
+ className={`h-[56px] bg-white border-gray-200 dark:border-gray-700 text-consent-primary text-md ${validationError.AADHAAR ? 'border-red-500' : ''}`}
602
+ placeholder={`Enter AADHAAR`}
603
+ readOnly={decodedInfo?.aadhaar ? true : false}
604
+ value={identifiers?.find(i => i.type === 'AADHAAR')?.value}
605
+ onChange={e => {
606
+ setValidationError(prev => ({ ...prev, AADHAAR: null }))
607
+ const aadhaarValue = e.target.value.replace(/[^0-9]/g, '')
608
+ const result = [...identifiers]
609
+ result.splice(
610
+ result.findIndex(i => i.type === 'AADHAAR'),
611
+ 1,
612
+ {
613
+ ...identifiers?.find(i => i.type === 'AADHAAR'),
614
+ value: aadhaarValue
615
+ }
616
+ )
617
+ setIdentifiers(result)
618
+
619
+ // Only validate when input reaches max length
620
+ if (aadhaarValue.length >= 12) {
621
+ try {
622
+ aadhaarSchema.parse({ value: aadhaarValue })
623
+ } catch (error) {
624
+ if (error instanceof z.ZodError) {
625
+ setValidationError(prev => ({ ...prev, AADHAAR: error.errors[0].message }))
626
+ // console.error('Aadhaar validation error:', error.errors[0].message)
627
+ }
628
+ }
629
+ }
630
+ }}
631
+ />
632
+ {validationError.AADHAAR && (
633
+ <p className="text-sm text-red-500 mt-1">{validationError.AADHAAR}</p>
634
+ )}
635
+ </div>
636
+ </>
637
+ ) : null}
638
+
639
+ {/* <div className='mt-16'>
640
+ <img src={logo} className='w-48 h-auto filter grayscale opacity-30' />
641
+ <h1 className='text-4xl font-semibold text-gray-300 mt-8 dark:text-muted-secondary'>
642
+ Trusted by over 5,00,000+ Indians
643
+ </h1>
644
+ </div> */}
645
+ </div>
646
+
647
+ <MobileFooter show={identifiers?.filter(i => !i.value || i.value === '').length === 0 && (() => {
648
+ try {
649
+ identifiers.find(i => i.type === 'PAN') && panSchema.parse({ value: identifiers?.find(i => i.type === 'PAN')?.value || '' })
650
+ identifiers.find(i => i.type === 'AADHAAR') && aadhaarSchema.parse({ value: identifiers?.find(i => i.type === 'AADHAAR')?.value || '' })
651
+ } catch (error) {
652
+ return false
653
+ }
654
+ return true
655
+ })()}>
656
+ <div className='flex flex-col items-center w-full gap-4'>
657
+ <AnimatedButton
658
+ onClick={handleDiscoverAccount}
659
+ size='lg'
660
+ className='w-full h-[50px]'
661
+ >
662
+ {t('discoverAccounts')}
663
+ </AnimatedButton>
664
+ <AnimatedButton
665
+ onClick={handleCancel}
666
+ variant={'ghost'}
667
+ size='lg'
668
+ className='text-primary'
669
+ >
670
+ {t('cancel')}
671
+ </AnimatedButton>
672
+ </div>
673
+ </MobileFooter>
674
+
675
+ <WebFooter show={identifiers?.filter(i => !i.value || i.value === '').length === 0 && (() => {
676
+ try {
677
+ identifiers.find(i => i.type === 'PAN') && panSchema.parse({ value: identifiers?.find(i => i.type === 'PAN')?.value || '' })
678
+ identifiers.find(i => i.type === 'AADHAAR') && aadhaarSchema.parse({ value: identifiers?.find(i => i.type === 'AADHAAR')?.value || '' })
679
+ } catch (error) {
680
+ return false
681
+ }
682
+ return true
683
+ })()}>
684
+ <AnimatedButton
685
+ onClick={handleCancel}
686
+ variant={'ghost'}
687
+ size='lg'
688
+ className='text-primary'
689
+ >
690
+ {t('cancel')}
691
+ </AnimatedButton>
692
+ <AnimatedButton onClick={handleDiscoverAccount} size='lg'>
693
+ {t('discoverAccounts')}
694
+ </AnimatedButton>
695
+ </WebFooter>
696
+
697
+
698
+
699
+ <BottomSheet
700
+ isOpen={isMobile && showModal.isOpen}
701
+ onClose={() => setShowModal(prev => ({ ...prev, isOpen: false, error: '' }))}
702
+ title={showModal.title}
703
+ height='auto'
704
+ showHandle={true}
705
+ showCloseButton={false}
706
+ >
707
+ {showModal.content == 'otp' ? <div className='flex flex-col items-center gap-4'>
708
+ <OTPInputComponent
709
+ maxLength={4}
710
+ title=''
711
+ editable={true}
712
+ mobileNumber={showModal.number}
713
+ onResend={() => {
714
+ setShowModal(old => ({ ...old, otp: null, error: '' }))
715
+ addNewNoQuery.mutate(undefined, {
716
+ onSuccess: () => {
717
+ setShowModal(old => ({ ...old, otp: null, error: '' }))
718
+ },
719
+ onError: (error: Error) => {
720
+ setShowModal(old => ({ ...old, otp: null, error: error.response?.data?.errorMsg || 'Something went wrong' }))
721
+ // console.error(error)
722
+ }
723
+ })
724
+ }}
725
+ countdownTime={18}
726
+ error={Boolean(showModal.error)}
727
+ errorMessage={showModal.error}
728
+ onChange={otp => {
729
+ setShowModal(old => ({ ...old, otp, error: '' }))
730
+ }}
731
+ handleEdit={() => {
732
+ setShowModal(old => ({ ...old, otp: null, error: '' }))
733
+ openAddNumberModal(showModal.number)
734
+ }}
735
+ />
736
+ <AnimatedButton
737
+ className='w-full h-11'
738
+ onClick={() => {
739
+ verifyNewNoQuery.mutate()
740
+ }}
741
+ >
742
+ Next
743
+ </AnimatedButton>
744
+ </div> : showModal.content}
745
+ </BottomSheet>
746
+
747
+ </div >
748
+ )
749
+ }
750
+
751
+ export default DiscoverAccount