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.
- package/.github/workflows/build-and-deploy.yml +41 -0
- package/.gitlab-ci.yml +108 -0
- package/.releaserc.json +18 -0
- package/.storybook/main.ts +28 -0
- package/.storybook/preview.ts +16 -0
- package/.storybook/vitest.setup.ts +9 -0
- package/.vite/deps/@radix-ui_react-avatar.js +230 -0
- package/.vite/deps/@radix-ui_react-avatar.js.map +7 -0
- package/.vite/deps/@radix-ui_react-slot.js +12 -0
- package/.vite/deps/@radix-ui_react-slot.js.map +7 -0
- package/.vite/deps/_metadata.json +79 -0
- package/.vite/deps/chunk-5VGQBUCU.js +597 -0
- package/.vite/deps/chunk-5VGQBUCU.js.map +7 -0
- package/.vite/deps/chunk-DC5AMYBS.js +38 -0
- package/.vite/deps/chunk-DC5AMYBS.js.map +7 -0
- package/.vite/deps/chunk-HUIEPYH7.js +11265 -0
- package/.vite/deps/chunk-HUIEPYH7.js.map +7 -0
- package/.vite/deps/chunk-TKHB4QMX.js +281 -0
- package/.vite/deps/chunk-TKHB4QMX.js.map +7 -0
- package/.vite/deps/chunk-YLDSBLSF.js +1139 -0
- package/.vite/deps/chunk-YLDSBLSF.js.map +7 -0
- package/.vite/deps/class-variance-authority.js +63 -0
- package/.vite/deps/class-variance-authority.js.map +7 -0
- package/.vite/deps/lucide-react.js +36984 -0
- package/.vite/deps/lucide-react.js.map +7 -0
- package/.vite/deps/package.json +3 -0
- package/.vite/deps/react-dom_client.js +17917 -0
- package/.vite/deps/react-dom_client.js.map +7 -0
- package/.vite/deps/react-router-dom.js +452 -0
- package/.vite/deps/react-router-dom.js.map +7 -0
- package/.vite/deps/react-router.js +234 -0
- package/.vite/deps/react-router.js.map +7 -0
- package/.vite/deps/react.js +5 -0
- package/.vite/deps/react.js.map +7 -0
- package/.vite/deps/react_jsx-dev-runtime.js +470 -0
- package/.vite/deps/react_jsx-dev-runtime.js.map +7 -0
- package/CHANGELOG.md +420 -0
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/RELEASE_CHEATSHEET.md +93 -0
- package/RELEASE_NOTES.md +120 -0
- package/components.json +21 -0
- package/docs/DEPLOYMENT_WORKFLOW.md +262 -0
- package/docs/RELEASE_GUIDE.md +591 -0
- package/docs/architecture.md +432 -0
- package/docs/components.md +199 -0
- package/docs/index.md +69 -0
- package/docs/local-release-workflow.md +234 -0
- package/docs/routes.md +118 -0
- package/docs/sdk-integration.md +325 -0
- package/docs/semantic-release.md +124 -0
- package/docs/user-flow.md +206 -0
- package/eslint.config.js +28 -0
- package/index.html +19 -0
- package/install.sh +198 -0
- package/package.json +115 -0
- package/public/images/bank-logo.png +0 -0
- package/public/saafe-icon.svg +9 -0
- package/src/App.tsx +171 -0
- package/src/__tests__/url-parameters.test.ts +82 -0
- package/src/assets/brand/applestore.svg +13 -0
- package/src/assets/brand/playstore.svg +23 -0
- package/src/assets/brand/saafe-color-white-logo.svg +14 -0
- package/src/assets/brand/saafe-icon.svg +9 -0
- package/src/assets/brand/saafe-logo.svg +18 -0
- package/src/assets/icons/check-icon-dark.svg +27 -0
- package/src/assets/icons/check-icon.svg +23 -0
- package/src/components/ErrorBoundary.tsx +132 -0
- package/src/components/alert/alert.tsx +27 -0
- package/src/components/auth/AuthGuard.tsx +76 -0
- package/src/components/cards/BankCard.stories.tsx +69 -0
- package/src/components/cards/BankCard.tsx +227 -0
- package/src/components/cards/OuterCard.tsx +109 -0
- package/src/components/cards/WrapperCard.tsx +64 -0
- package/src/components/documents/PrivacyContent.tsx +1 -0
- package/src/components/dummyFooter.tsx +29 -0
- package/src/components/icons/github.tsx +12 -0
- package/src/components/language/LanguageSwitcher.tsx +44 -0
- package/src/components/layouts/FrostedLayout.stories.tsx +42 -0
- package/src/components/layouts/FrostedLayout.tsx +333 -0
- package/src/components/layouts/MobileLayout.tsx +403 -0
- package/src/components/mobile-background.tsx +136 -0
- package/src/components/mobileAppDownload.tsx +30 -0
- package/src/components/modal/ModalComp.tsx +27 -0
- package/src/components/mode-toggle.tsx +36 -0
- package/src/components/page-header.tsx +50 -0
- package/src/components/session/SessionTimeoutScreen.tsx +134 -0
- package/src/components/session/SessionTimer.tsx +173 -0
- package/src/components/step-navigation.tsx +87 -0
- package/src/components/title/AppBar.stories.tsx +50 -0
- package/src/components/title/AppBar.tsx +150 -0
- package/src/components/title/SectionTitle.tsx +31 -0
- package/src/components/ui/AnimatedButton.module.css +13 -0
- package/src/components/ui/alert.tsx +66 -0
- package/src/components/ui/animatedButton.tsx +111 -0
- package/src/components/ui/avatar.tsx +51 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/bottom-sheet.tsx +122 -0
- package/src/components/ui/button.tsx +59 -0
- package/src/components/ui/calendar.tsx +86 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/checkbox.stories.tsx +49 -0
- package/src/components/ui/checkbox.tsx +67 -0
- package/src/components/ui/collapsible.tsx +45 -0
- package/src/components/ui/dialog.tsx +134 -0
- package/src/components/ui/document-link.tsx +26 -0
- package/src/components/ui/dot-stepper.tsx +57 -0
- package/src/components/ui/dropdown-menu.tsx +255 -0
- package/src/components/ui/form.tsx +165 -0
- package/src/components/ui/frosted-panel.stories.tsx +86 -0
- package/src/components/ui/frosted-panel.tsx +276 -0
- package/src/components/ui/input.tsx +39 -0
- package/src/components/ui/label.stories.tsx +67 -0
- package/src/components/ui/label.tsx +23 -0
- package/src/components/ui/mobile-footer.tsx +54 -0
- package/src/components/ui/modal.tsx +90 -0
- package/src/components/ui/otp-input.stories.tsx +62 -0
- package/src/components/ui/otp-input.tsx +221 -0
- package/src/components/ui/platform-specific-behavior.tsx +28 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/progress.tsx +103 -0
- package/src/components/ui/radio-group.tsx +45 -0
- package/src/components/ui/scroll-area.tsx +56 -0
- package/src/components/ui/sdk-params-docs.tsx +53 -0
- package/src/components/ui/select.tsx +159 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +137 -0
- package/src/components/ui/sidebar.tsx +724 -0
- package/src/components/ui/skeleton.stories.tsx +50 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/sonner.tsx +23 -0
- package/src/components/ui/step.stories.tsx +132 -0
- package/src/components/ui/step.tsx +234 -0
- package/src/components/ui/stepper-progress.tsx +136 -0
- package/src/components/ui/stepper.tsx +259 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/tooltip.tsx +61 -0
- package/src/components/ui/url-decode-loader.tsx +36 -0
- package/src/components/ui/version-display.tsx +104 -0
- package/src/components/ui/web-footer.tsx +36 -0
- package/src/config/environments.ts +99 -0
- package/src/config/urls.ts +53 -0
- package/src/const/fiTypeCategoryMap.ts +19 -0
- package/src/contexts/LanguageContext.tsx +41 -0
- package/src/contexts/RTLContext.tsx +42 -0
- package/src/contexts/ThemeContext.tsx +93 -0
- package/src/hooks/use-account-discovery.ts +205 -0
- package/src/hooks/use-auth-query.ts +141 -0
- package/src/hooks/use-fip-query.ts +72 -0
- package/src/hooks/use-media-query.ts +32 -0
- package/src/hooks/use-mobile.ts +24 -0
- package/src/hooks/use-page-title.tsx +48 -0
- package/src/hooks/use-platform.ts +52 -0
- package/src/hooks/use-trusted-count.ts +21 -0
- package/src/hooks/use-url-decode.ts +90 -0
- package/src/hooks/useStep.ts +170 -0
- package/src/index.css +154 -0
- package/src/interfaces/app.interfaces.ts +39 -0
- package/src/interfaces/services.interfaces.ts +65 -0
- package/src/lib/i18n.ts +68 -0
- package/src/lib/utils.ts +6 -0
- package/src/locales/en/common.json +167 -0
- package/src/locales/hi/common.json +137 -0
- package/src/locales/kn/common.json +137 -0
- package/src/locales/ml/common.json +137 -0
- package/src/locales/ta/common.json +137 -0
- package/src/locales/te/common.json +137 -0
- package/src/locales/ur/common.json +138 -0
- package/src/main.tsx +46 -0
- package/src/pages/Login.tsx +363 -0
- package/src/pages/accounts/AccountsToProceed.tsx +396 -0
- package/src/pages/accounts/Discover.tsx +76 -0
- package/src/pages/accounts/DiscoverAccount.tsx +751 -0
- package/src/pages/accounts/LinkSelectedAccounts.tsx +638 -0
- package/src/pages/accounts/OldUser.tsx +329 -0
- package/src/pages/accounts/link-accounts.tsx +913 -0
- package/src/pages/consent/ReviewConsent.tsx +836 -0
- package/src/pages/consent/rejected.tsx +253 -0
- package/src/pages/consent/success.tsx +220 -0
- package/src/providers/query-provider.tsx +24 -0
- package/src/providers/toast-provider.tsx +26 -0
- package/src/services/api/account.service.ts +296 -0
- package/src/services/api/auth.service.ts +206 -0
- package/src/services/api/axios.ts +138 -0
- package/src/services/api/consent.service.ts +142 -0
- package/src/services/api/decode.service.ts +53 -0
- package/src/services/api/feedback.service.ts +34 -0
- package/src/services/api/fip.service.ts +187 -0
- package/src/services/api/index.ts +9 -0
- package/src/services/api/public.service.ts +18 -0
- package/src/services/api.ts +2 -0
- package/src/services/postMessage.service.ts +179 -0
- package/src/store/NavigationBlockContext.tsx +34 -0
- package/src/store/auth.store.ts +79 -0
- package/src/store/fip.store.ts +396 -0
- package/src/store/mandatoryConsent.store.ts +24 -0
- package/src/store/redirect.store.ts +73 -0
- package/src/store/step.store.ts +124 -0
- package/src/stories/Button.stories.ts +53 -0
- package/src/stories/Button.tsx +37 -0
- package/src/stories/Configure.mdx +364 -0
- package/src/stories/Header.stories.ts +33 -0
- package/src/stories/Header.tsx +56 -0
- package/src/stories/Page.stories.ts +32 -0
- package/src/stories/Page.tsx +73 -0
- package/src/stories/button.css +30 -0
- package/src/stories/header.css +32 -0
- package/src/stories/page.css +68 -0
- package/src/styles/rtl-utils.css +90 -0
- package/src/styles/rtl.css +105 -0
- package/src/utils/api-error.ts +26 -0
- package/src/utils/cn.ts +10 -0
- package/src/utils/error-callback.ts +116 -0
- package/src/utils/formatAccountNumber.ts +9 -0
- package/src/utils/handleIdentifiers.ts +90 -0
- package/src/utils/posthog.ts +67 -0
- package/src/utils/toast-helpers.ts +61 -0
- package/src/vite-env.d.ts +1 -0
- package/stage-aa-2506251021.zip +0 -0
- package/tsconfig.app.json +33 -0
- package/tsconfig.json +13 -0
- package/tsconfig.node.json +24 -0
- package/vite.config.ts +45 -0
- package/vitest.shims.d.ts +1 -0
- 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
|
+
}
|