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,53 @@
1
+ /**
2
+ * URL configuration file for centralized management of application URLs
3
+ * All URLs used throughout the application should be defined here
4
+ */
5
+
6
+ // API URLs
7
+ export const API_URLS = {
8
+ BASE_URL: import.meta.env.VITE_API_BASE_URL || '',
9
+ AUTH: {
10
+ LOGIN: '/auth/login',
11
+ REFRESH_TOKEN: '/auth/refresh-token',
12
+ LOGOUT: '/auth/logout',
13
+ },
14
+ ACCOUNTS: {
15
+ DISCOVER: '/accounts/discover',
16
+ LINK: '/accounts/link',
17
+ },
18
+ CONSENT: {
19
+ REVIEW: '/consent/review',
20
+ CONFIRM: '/consent/confirm',
21
+ REJECT: '/consent/reject',
22
+ },
23
+ }
24
+
25
+ // Application URLs
26
+ export const APP_URLS = {
27
+ LOGIN: '/login',
28
+ LINK_ACCOUNTS: {
29
+ ROOT: '/link-accounts',
30
+ DISCOVERY: '/link-accounts/discovery',
31
+ PROCEED: '/link-accounts/proceed',
32
+ LINK: '/link-accounts/link',
33
+ DISCOVER_ACCOUNT: '/link-accounts/discover-account',
34
+ OLD_USER: '/link-accounts/old-user',
35
+ CATEGORY: '/link-accounts/:category',
36
+ },
37
+ CONSENT: {
38
+ REVIEW: '/review',
39
+ SUCCESS: '/success',
40
+ REJECTED: '/rejected',
41
+ },
42
+ }
43
+
44
+ // External URLs
45
+ export const EXTERNAL_URLS = {
46
+ POSTHOG_HOST: import.meta.env.VITE_PUBLIC_POSTHOG_HOST || '',
47
+ POSTHOG_KEY: import.meta.env.VITE_PUBLIC_POSTHOG_KEY || '',
48
+ }
49
+
50
+ // Session config
51
+ export const SESSION = {
52
+ TIMEOUT: import.meta.env.VITE_SESSION_TIMEOUT || '900000', // Default 15 minutes
53
+ }
@@ -0,0 +1,19 @@
1
+ export const fiTypeCategoryMap: Record<string, string[]> = {
2
+ BANK: ['DEPOSIT', 'RECURRING_DEPOSIT', 'TERM_DEPOSIT', 'BANK'],
3
+ INVESTMENTS: [
4
+ 'EQUITIES',
5
+ 'MUTUAL_FUNDS',
6
+ 'OTHER_INVESTMENTS',
7
+ 'IDR',
8
+ 'REIT',
9
+ 'CIS',
10
+ 'INVIT',
11
+ 'AIF',
12
+ 'ETF',
13
+ 'SIP',
14
+ 'NPS (Retirement focused)',
15
+ 'PENSION'
16
+ ],
17
+ INSURANCE: ['INSURANCE_POLICIES', 'LIFE_INSURANCE', 'GENERAL_INSURANCE', 'INSURANCE'],
18
+ GST: ['GSTR1_3B', 'GST']
19
+ }
@@ -0,0 +1,41 @@
1
+ import React, { createContext, useContext, useEffect, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { languageOptions } from '@/lib/i18n';
4
+
5
+ // Define the context type
6
+ interface LanguageContextType {
7
+ currentLanguage: string;
8
+ changeLanguage: (code: string) => void;
9
+ languageOptions: typeof languageOptions;
10
+ }
11
+
12
+ // Create the context with default values
13
+ const LanguageContext = createContext<LanguageContextType>({
14
+ currentLanguage: 'en',
15
+ changeLanguage: () => {},
16
+ languageOptions,
17
+ });
18
+
19
+ // Hook to use the language context
20
+ export const useLanguage = () => useContext(LanguageContext);
21
+
22
+ // Provider component
23
+ export const LanguageProvider = ({ children }: { children: React.ReactNode }) => {
24
+ const { i18n } = useTranslation();
25
+ const [currentLanguage, setCurrentLanguage] = useState(i18n.language || 'en');
26
+
27
+ useEffect(() => {
28
+ // Update state when i18n language changes
29
+ setCurrentLanguage(i18n.language);
30
+ }, [i18n.language]);
31
+
32
+ const changeLanguage = (code: string) => {
33
+ i18n.changeLanguage(code);
34
+ };
35
+
36
+ return (
37
+ <LanguageContext.Provider value={{ currentLanguage, changeLanguage, languageOptions }}>
38
+ {children}
39
+ </LanguageContext.Provider>
40
+ );
41
+ };
@@ -0,0 +1,42 @@
1
+ import React, { createContext, useContext, useEffect } from 'react';
2
+ import { useLanguage } from './LanguageContext';
3
+
4
+ // Define RTL languages
5
+ const RTL_LANGUAGES = ['ur']; // Add more RTL languages here if needed
6
+
7
+ // Define the context type
8
+ interface RTLContextType {
9
+ isRTL: boolean;
10
+ }
11
+
12
+ // Create the context with default value
13
+ const RTLContext = createContext<RTLContextType>({
14
+ isRTL: false,
15
+ });
16
+
17
+ // Hook to use the RTL context
18
+ export const useRTL = () => useContext(RTLContext);
19
+
20
+ // Provider component
21
+ export const RTLProvider = ({ children }: { children: React.ReactNode }) => {
22
+ const { currentLanguage } = useLanguage();
23
+ const isRTL = RTL_LANGUAGES.includes(currentLanguage);
24
+
25
+ useEffect(() => {
26
+ // Update HTML dir attribute based on language
27
+ document.documentElement.dir = isRTL ? 'rtl' : 'ltr';
28
+
29
+ // Apply RTL class to body for global styling
30
+ if (isRTL) {
31
+ document.body.classList.add('rtl');
32
+ } else {
33
+ document.body.classList.remove('rtl');
34
+ }
35
+ }, [isRTL]);
36
+
37
+ return (
38
+ <RTLContext.Provider value={{ isRTL }}>
39
+ {children}
40
+ </RTLContext.Provider>
41
+ );
42
+ };
@@ -0,0 +1,93 @@
1
+ import { createContext, ReactNode, use, useEffect, useState } from "react";
2
+ import { useRedirectStore } from "@/store/redirect.store";
3
+
4
+ type ThemeType = {
5
+ theme: string;
6
+ setTheme: (theme: string) => void;
7
+ };
8
+
9
+ export const ThemeContext = createContext<ThemeType | null>(null);
10
+
11
+ export function ThemeProvider({
12
+ children,
13
+ defaultTheme = "system",
14
+ storageKey = "shadcn-ui-theme",
15
+ }: {
16
+ children: ReactNode;
17
+ defaultTheme?: string;
18
+ storageKey?: string;
19
+ }) {
20
+ const { decodedInfo } = useRedirectStore();
21
+ const sdkTheme = decodedInfo?.theme?.toLowerCase();
22
+
23
+ const [theme, setTheme] = useState<string>(
24
+ () => {
25
+ // First priority: SDK-defined theme
26
+ if (sdkTheme && ["light", "dark", "system"].includes(sdkTheme)) {
27
+ if (process.env.NODE_ENV !== 'production') {
28
+ console.log('Using SDK-defined theme:', sdkTheme);
29
+ }
30
+ return sdkTheme;
31
+ }
32
+ // Second priority: Stored theme
33
+ const storedTheme = localStorage.getItem(storageKey);
34
+ if (storedTheme) {
35
+ return storedTheme;
36
+ }
37
+ // Last resort: default theme
38
+ return defaultTheme;
39
+ }
40
+ );
41
+
42
+ // Update theme if SDK theme changes
43
+ useEffect(() => {
44
+ if (sdkTheme && ["light", "dark", "system"].includes(sdkTheme)) {
45
+ if (process.env.NODE_ENV !== 'production') {
46
+ console.log('Theme updated from SDK:', sdkTheme);
47
+ }
48
+ setTheme(sdkTheme);
49
+ }
50
+ }, [sdkTheme]);
51
+
52
+ useEffect(() => {
53
+ const root = window.document.documentElement;
54
+
55
+ root.classList.remove("light", "dark");
56
+
57
+ if (theme === "system") {
58
+ const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
59
+ .matches
60
+ ? "dark"
61
+ : "light";
62
+
63
+ root.classList.add(systemTheme);
64
+ return;
65
+ }
66
+
67
+ root.classList.add(theme);
68
+ }, [theme]);
69
+
70
+ return (
71
+ <ThemeContext.Provider
72
+ value={{
73
+ theme,
74
+ setTheme: (theme: string) => {
75
+ localStorage.setItem(storageKey, theme);
76
+ setTheme(theme);
77
+ },
78
+ }}
79
+ >
80
+ {children}
81
+ </ThemeContext.Provider>
82
+ );
83
+ }
84
+
85
+ export function useTheme(): ThemeType {
86
+ const context = use(ThemeContext);
87
+
88
+ if (context === null) {
89
+ throw new Error("useTheme must be used within a ThemeProvider");
90
+ }
91
+
92
+ return context;
93
+ }
@@ -0,0 +1,205 @@
1
+ import { useMutation } from '@tanstack/react-query'
2
+ import {
3
+ accountService,
4
+ AutoDiscoveryResponse,
5
+ AccountDiscoveryResponse
6
+ } from '@/services/api/account.service'
7
+ import { useAuthStore } from '@/store/auth.store'
8
+ import { useState } from 'react'
9
+ import { useFipStore } from '@/store/fip.store'
10
+ import { fiTypeCategoryMap } from '@/const/fiTypeCategoryMap'
11
+ import { useRedirectStore } from '@/store/redirect.store'
12
+
13
+ // Map API account format to our internal format
14
+ export interface ProcessedDiscoveryResult {
15
+ originalAccounts: AutoDiscoveryResponse['Accounts']
16
+ groupedAccounts: {
17
+ [key: string]: {
18
+ id: string
19
+ type: string
20
+ maskedAccountNumber: string
21
+ bankName: string
22
+ logoUrl?: string | null
23
+ isNew?: boolean
24
+ fipId: string
25
+ }[]
26
+ }
27
+ accounts: {
28
+ id: string
29
+ type: string
30
+ maskedAccountNumber: string
31
+ bankName: string
32
+ logoUrl?: string | null
33
+ isNew?: boolean
34
+ fipId: string
35
+ }[]
36
+ signature: string
37
+ }
38
+
39
+ /**
40
+ * Hook for auto discovering accounts using FIPs
41
+ */
42
+ export function useAccountDiscovery() {
43
+ const { user } = useAuthStore()
44
+ const { identifiers, activeCategory } = useFipStore()
45
+ const { decodedInfo } = useRedirectStore()
46
+ const [errorMessage, setErrorMessage] = useState<string | null>(null)
47
+
48
+ return {
49
+ ...useMutation({
50
+ mutationFn: async (
51
+ fipIds: string[]
52
+ ): Promise<ProcessedDiscoveryResult> => {
53
+ if (!user?.phoneNumber) {
54
+ const error = new Error('Mobile number is not available')
55
+ setErrorMessage(error.message)
56
+ throw error
57
+ }
58
+
59
+ if (!activeCategory) {
60
+ const error = new Error('Active category is not available')
61
+ setErrorMessage(error.message)
62
+ throw error
63
+ }
64
+
65
+ if (!decodedInfo?.fiuId) {
66
+ const error = new Error('FIU ID is not available')
67
+ setErrorMessage(error.message)
68
+ throw error
69
+ }
70
+
71
+ try {
72
+ const fiuId = decodedInfo.fiuId
73
+ const fiTypes = Array.isArray(decodedInfo.fiTypesRequiredForConsent)
74
+ ? decodedInfo.fiTypesRequiredForConsent.filter(i => fiTypeCategoryMap[activeCategory].includes(i))
75
+ : []
76
+
77
+ if (fiTypes.length === 0) {
78
+ // Fallback to category map if no FI types are provided
79
+ fiTypes.push(...fiTypeCategoryMap[activeCategory])
80
+ }
81
+
82
+ const commonIdentifiers = identifiers.map(i => ({
83
+ type: i.type,
84
+ value: i.value,
85
+ categoryType: i.category
86
+ }))
87
+
88
+ // If multiple FIP IDs, we need to call account discovery for each
89
+ const accountPromises = fipIds.map(async (fipId) => {
90
+ // Use account discovery for individual FIP
91
+ return accountService.accountDiscovery({
92
+ Identifiers: [...commonIdentifiers],
93
+ FiuId: fiuId,
94
+ FipId: fipId,
95
+ FITypes: fiTypes
96
+ })
97
+ })
98
+
99
+ // Wait for all account discovery calls to complete (including failures)
100
+ const results = await Promise.allSettled(accountPromises)
101
+
102
+ // Separate successful and failed results
103
+ const successfulResults: { fipId: string; result: AccountDiscoveryResponse }[] = []
104
+ const failedFips: string[] = []
105
+ const errorMessages: string[] = []
106
+
107
+ results.forEach((settledResult, index) => {
108
+ const fipId = fipIds[index]
109
+ if (settledResult.status === 'fulfilled') {
110
+ successfulResults.push({ fipId, result: settledResult.value })
111
+ } else {
112
+ failedFips.push(fipId)
113
+ const error = settledResult.reason as Error & {
114
+ response?: {
115
+ status?: number;
116
+ data?: { message?: string }
117
+ }
118
+ }
119
+ if (error?.response?.data?.message) {
120
+ errorMessages.push(`${fipId}: ${error.response.data.message}`)
121
+ } else {
122
+ errorMessages.push(`${fipId}: Failed to discover accounts`)
123
+ }
124
+ }
125
+ })
126
+
127
+ // If no successful results, throw an error
128
+ if (successfulResults.length === 0) {
129
+ const combinedError = errorMessages.join('; ')
130
+ setErrorMessage(combinedError)
131
+ throw new Error(combinedError)
132
+ }
133
+
134
+ // Process and merge the accounts from successful FIPs only
135
+ const originalAccounts = successfulResults.map(({ fipId, result }) => ({
136
+ fipId,
137
+ fipName: fipId, // We'll need to get the actual name from somewhere
138
+ DiscoveredAccounts: result.DiscoveredAccounts
139
+ }))
140
+
141
+ // Process the accounts from successful API responses
142
+ const processedAccounts = originalAccounts.flatMap(fipData =>
143
+ fipData.DiscoveredAccounts.map(account => ({
144
+ id: account.accRefNumber,
145
+ type: account.FIType,
146
+ maskedAccountNumber: account.maskedAccNumber,
147
+ bankName: fipData.fipName,
148
+ logoUrl: account.logoUrl,
149
+ isNew: false, // Could determine this if needed
150
+ fipId: fipData.fipId
151
+ }))
152
+ )
153
+
154
+ // Group accounts by bank/FIP
155
+ const groupedAccounts = processedAccounts.reduce((acc, account) => {
156
+ const key = account.fipId
157
+ if (!acc[key]) {
158
+ acc[key] = []
159
+ }
160
+ acc[key].push(account)
161
+ return acc
162
+ }, {} as ProcessedDiscoveryResult['groupedAccounts'])
163
+
164
+ // Use the signature from the first successful result
165
+ const signature = successfulResults[0].result.signature
166
+
167
+ // Set partial error message if some FIPs failed
168
+ if (failedFips.length > 0) {
169
+ const partialErrorMsg = `Some banks failed to load: ${failedFips.join(', ')}`
170
+ setErrorMessage(partialErrorMsg)
171
+ }
172
+
173
+ return {
174
+ originalAccounts,
175
+ accounts: processedAccounts,
176
+ groupedAccounts,
177
+ signature
178
+ }
179
+ } catch (error) {
180
+ // Handle specific error types
181
+ const typedError = error as Error & {
182
+ response?: {
183
+ status?: number;
184
+ data?: { message?: string }
185
+ }
186
+ }
187
+
188
+ if (typedError.response?.status === 401) {
189
+ setErrorMessage('Authentication error. Please log in again.')
190
+ } else if (typedError.response?.status === 403) {
191
+ setErrorMessage("You don't have permission to discover accounts.")
192
+ } else if (typedError.response?.data?.message) {
193
+ setErrorMessage(typedError.response.data.message)
194
+ } else {
195
+ setErrorMessage('Failed to discover accounts. Please try again.')
196
+ }
197
+ // console.error('Error during account discovery:', error)
198
+ throw error
199
+ }
200
+ }
201
+ }),
202
+ errorMessage,
203
+ clearError: () => setErrorMessage(null)
204
+ }
205
+ }
@@ -0,0 +1,141 @@
1
+ import { useMutation, useQueryClient } from '@tanstack/react-query'
2
+ import {
3
+ authService,
4
+ InitOtpResponse,
5
+ ApiErrorType
6
+ } from '@/services/api/auth.service'
7
+ import { useAuthStore } from '@/store/auth.store'
8
+ import { handleApiError } from '@/utils/toast-helpers'
9
+
10
+ /**
11
+ * Hook for initiating OTP flow
12
+ */
13
+ export function useInitiateOtp() {
14
+ const { setOtpUniqueId, setIsLoading, setError } = useAuthStore()
15
+
16
+ return useMutation({
17
+ mutationFn: async ({
18
+ phoneNumber,
19
+ isTermsAndConditionAgreed
20
+ }: {
21
+ phoneNumber: string
22
+ isTermsAndConditionAgreed: boolean
23
+ }): Promise<InitOtpResponse> => {
24
+ setIsLoading(true)
25
+ try {
26
+ const response = await authService.initiateOtp(
27
+ phoneNumber,
28
+ isTermsAndConditionAgreed
29
+ )
30
+
31
+ // Store OTP unique ID if available - use otpUniqueID (with capital ID)
32
+ if (response.otpUniqueID) {
33
+ setOtpUniqueId(response.otpUniqueID)
34
+ }
35
+
36
+ // Store tokens if available
37
+ if (response.access_token) {
38
+ useAuthStore
39
+ .getState()
40
+ .setTokens(response.access_token, response.refresh_token || '')
41
+ }
42
+
43
+ return response
44
+ } finally {
45
+ setIsLoading(false)
46
+ }
47
+ },
48
+ onError: (error: ApiErrorType | Error) => {
49
+ const errorMessage =
50
+ 'errorData' in error
51
+ ? error.errorData?.errorCode || 'Failed to send OTP'
52
+ : 'Failed to send OTP'
53
+ setError(errorMessage)
54
+ }
55
+ })
56
+ }
57
+
58
+ /**
59
+ * Hook for verifying OTP
60
+ */
61
+ export function useVerifyOtp() {
62
+ const queryClient = useQueryClient()
63
+ const { setUser, setTokens, setIsLoading, setError } = useAuthStore()
64
+
65
+ return useMutation({
66
+ mutationFn: async ({
67
+ phoneNumber,
68
+ code,
69
+ otpUniqueID
70
+ }: {
71
+ phoneNumber: string
72
+ code: string
73
+ otpUniqueID: string
74
+ }) => {
75
+ setIsLoading(true)
76
+ try {
77
+ const response = await authService.verifyOtp(
78
+ phoneNumber,
79
+ code,
80
+ otpUniqueID
81
+ )
82
+
83
+ // Set tokens if available
84
+ if (response.access_token) {
85
+ setTokens(response.access_token, response.refresh_token || '')
86
+ }
87
+
88
+ // Set user if available
89
+ if (response.phoneNumber) {
90
+ setUser({
91
+ firstName: response.firstName || '',
92
+ lastName: response.lastName || '',
93
+ vuaId: response.vuaId || '',
94
+ phoneNumber: response.phoneNumber,
95
+ isTwoFactorEnabled: response.isTwoFactorEnabled ?? null,
96
+ isBioMetricsEnabled: response.isBioMetricsEnabled ?? null
97
+ })
98
+ }
99
+
100
+ return response
101
+ } catch (error) {
102
+ throw error
103
+ } finally {
104
+ setIsLoading(false)
105
+ }
106
+ },
107
+ onSuccess: () => {
108
+ // Invalidate relevant queries after successful authentication
109
+ queryClient.invalidateQueries({ queryKey: ['user'] })
110
+ },
111
+ onError: (error: ApiErrorType | Error) => {
112
+ const errorMessage =
113
+ 'errorData' in error
114
+ ? error.errorData?.errorCode || 'Failed to verify OTP'
115
+ : 'Failed to verify OTP'
116
+ setError(errorMessage)
117
+ }
118
+ })
119
+ }
120
+
121
+ /**
122
+ * Hook for refreshing token
123
+ */
124
+ export function useRefreshToken() {
125
+ const { setTokens, logout } = useAuthStore()
126
+
127
+ return useMutation({
128
+ mutationFn: async (refreshToken: string) => {
129
+ const response = await authService.refreshToken(refreshToken)
130
+ return response
131
+ },
132
+ onSuccess: data => {
133
+ setTokens(data.access_token, data.refresh_token)
134
+ },
135
+ onError: error => {
136
+ // If refresh token fails, logout user
137
+ handleApiError(error, 'Session expired. Please login again.')
138
+ logout()
139
+ }
140
+ })
141
+ }
@@ -0,0 +1,72 @@
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import { fipService, FipResponseData } from "@/services/api/fip.service";
3
+ import { useFipStore, FipData } from "@/store/fip.store";
4
+ import { useRedirectStore } from "@/store/redirect.store";
5
+
6
+ /**
7
+ * Helper function to extract categories from API response
8
+ */
9
+ const extractCategoriesFromResponse = (data: FipResponseData): string[] => {
10
+ // If response is not an object, return empty array
11
+ if (!data || typeof data !== "object") return [];
12
+
13
+ // Get direct keys from the response object (GST, BANK, etc.)
14
+ const categories = Object.keys(data).filter((key) => {
15
+ // Exclude metadata or non-array keys
16
+ return Array.isArray(data[key]);
17
+ });
18
+
19
+ return categories;
20
+ };
21
+
22
+ /**
23
+ * Hook for fetching FIP data
24
+ */
25
+ export function useFipQuery(consentHandle?: string) {
26
+ const { setFips, setIsLoading, setError } = useFipStore();
27
+ const { decodedInfo } = useRedirectStore();
28
+
29
+ return useQuery({
30
+ queryKey: ["fips", consentHandle],
31
+ queryFn: async () => {
32
+ setIsLoading(true);
33
+ try {
34
+ // Get raw response containing category structure
35
+ const response = await fipService.getRawFipResponse(consentHandle);
36
+
37
+ // Extract categories from response (GST, BANK, etc)
38
+ const categories = extractCategoriesFromResponse(response);
39
+
40
+ // Create a map of category => FIPs for that category only
41
+ const categorizedFips: Record<string, FipData[]> = {};
42
+
43
+ // Populate each category with its specific FIPs
44
+ categories.forEach(category => {
45
+ const categoryData = response[category];
46
+ if (Array.isArray(categoryData)) {
47
+ categorizedFips[category] = categoryData as FipData[];
48
+ }
49
+ });
50
+
51
+ // Flatten for backward compatibility - but maintain categorization
52
+ const allFips = Object.values(categorizedFips).flat();
53
+
54
+ if (allFips.length > 0) {
55
+ // Pass both the flattened FIPs, categories, and the categorized map
56
+ setFips(allFips, decodedInfo?.fiTypesRequiredForConsent, categorizedFips);
57
+ } else {
58
+ setError("No FIP data available");
59
+ }
60
+
61
+ return allFips;
62
+ } catch (error) {
63
+ setError("Failed to fetch FIP data");
64
+ throw error;
65
+ } finally {
66
+ setIsLoading(false);
67
+ }
68
+ },
69
+ staleTime: 5 * 60 * 1000, // 5 minutes
70
+ refetchOnWindowFocus: false,
71
+ });
72
+ }
@@ -0,0 +1,32 @@
1
+ import { useState, useEffect } from 'react';
2
+
3
+ /**
4
+ * Custom hook to check if a media query matches
5
+ * @param query Media query string to check
6
+ * @returns boolean indicating if the query matches
7
+ */
8
+ export function useMediaQuery(query: string): boolean {
9
+ const [matches, setMatches] = useState(false);
10
+
11
+ useEffect(() => {
12
+ const mediaQuery = window.matchMedia(query);
13
+
14
+ // Set initial value
15
+ setMatches(mediaQuery.matches);
16
+
17
+ // Create event listener to update state when media query changes
18
+ const updateMatches = (e: MediaQueryListEvent) => {
19
+ setMatches(e.matches);
20
+ };
21
+
22
+ // Add event listener
23
+ mediaQuery.addEventListener('change', updateMatches);
24
+
25
+ // Clean up when component unmounts
26
+ return () => {
27
+ mediaQuery.removeEventListener('change', updateMatches);
28
+ };
29
+ }, [query]);
30
+
31
+ return matches;
32
+ }