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,24 @@
|
|
1
|
+
import * as React from "react"
|
2
|
+
|
3
|
+
const MOBILE_BREAKPOINT = 768
|
4
|
+
|
5
|
+
export function useIsMobile() {
|
6
|
+
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
7
|
+
|
8
|
+
React.useEffect(() => {
|
9
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
10
|
+
const onChange = () => {
|
11
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
12
|
+
}
|
13
|
+
|
14
|
+
// Modern browsers use addEventListener
|
15
|
+
mql.addEventListener("change", onChange)
|
16
|
+
|
17
|
+
// Set initial value
|
18
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
19
|
+
|
20
|
+
return () => mql.removeEventListener("change", onChange)
|
21
|
+
}, [])
|
22
|
+
|
23
|
+
return !!isMobile
|
24
|
+
}
|
@@ -0,0 +1,48 @@
|
|
1
|
+
import { useState, useEffect, useContext, createContext, ReactNode } from 'react';
|
2
|
+
|
3
|
+
interface PageTitleContextType {
|
4
|
+
pageTitle: string;
|
5
|
+
setPageTitle: (title: string) => void;
|
6
|
+
}
|
7
|
+
|
8
|
+
// Create context with default values
|
9
|
+
const PageTitleContext = createContext<PageTitleContextType>({
|
10
|
+
pageTitle: '',
|
11
|
+
setPageTitle: () => {}
|
12
|
+
});
|
13
|
+
|
14
|
+
// Provider component to wrap around App
|
15
|
+
export const PageTitleProvider = ({ children }: { children: ReactNode }) => {
|
16
|
+
const [pageTitle, setPageTitle] = useState<string>('');
|
17
|
+
|
18
|
+
return (
|
19
|
+
<PageTitleContext.Provider value={{ pageTitle, setPageTitle }}>
|
20
|
+
{children}
|
21
|
+
</PageTitleContext.Provider>
|
22
|
+
);
|
23
|
+
};
|
24
|
+
|
25
|
+
// Custom hook to use the page title context
|
26
|
+
export function usePageTitle(): [string, (title: string) => void] {
|
27
|
+
const context = useContext(PageTitleContext);
|
28
|
+
|
29
|
+
if (!context) {
|
30
|
+
throw new Error('usePageTitle must be used within a PageTitleProvider');
|
31
|
+
}
|
32
|
+
|
33
|
+
return [context.pageTitle, context.setPageTitle];
|
34
|
+
}
|
35
|
+
|
36
|
+
// Additional hook to set page title with document.title update
|
37
|
+
export function useSetPageTitle(title: string): void {
|
38
|
+
const [_, setPageTitle] = usePageTitle();
|
39
|
+
|
40
|
+
useEffect(() => {
|
41
|
+
setPageTitle(title);
|
42
|
+
document.title = `${title} | Saafe`;
|
43
|
+
|
44
|
+
return () => {
|
45
|
+
document.title = 'Saafe';
|
46
|
+
};
|
47
|
+
}, [title, setPageTitle]);
|
48
|
+
}
|
@@ -0,0 +1,52 @@
|
|
1
|
+
import { useRedirectStore } from "@/store/redirect.store";
|
2
|
+
|
3
|
+
/**
|
4
|
+
* Supported platform types for SDK integration
|
5
|
+
*/
|
6
|
+
export type PlatformType =
|
7
|
+
| "ios"
|
8
|
+
| "android"
|
9
|
+
| "react-native"
|
10
|
+
| "flutter"
|
11
|
+
| "web"
|
12
|
+
| "unknown";
|
13
|
+
|
14
|
+
/**
|
15
|
+
* Normalized platform values to ensure consistent casing
|
16
|
+
*/
|
17
|
+
const NORMALIZED_PLATFORMS: Record<string, PlatformType> = {
|
18
|
+
'ios': 'ios',
|
19
|
+
'android': 'android',
|
20
|
+
'react-native': 'react-native',
|
21
|
+
'reactnative': 'react-native',
|
22
|
+
'flutter': 'flutter',
|
23
|
+
'web': 'web'
|
24
|
+
};
|
25
|
+
|
26
|
+
/**
|
27
|
+
* Hook to detect and provide platform-specific information
|
28
|
+
* Usage:
|
29
|
+
* const { platform, isMobile, isIOS, isAndroid } = usePlatform();
|
30
|
+
*/
|
31
|
+
export function usePlatform() {
|
32
|
+
const { decodedInfo } = useRedirectStore();
|
33
|
+
|
34
|
+
// Normalize platform value to ensure consistent handling
|
35
|
+
const rawPlatform = decodedInfo?.platform?.toLowerCase() || '';
|
36
|
+
const platform = NORMALIZED_PLATFORMS[rawPlatform] || "unknown";
|
37
|
+
|
38
|
+
const isMobile = platform === "ios" || platform === "android";
|
39
|
+
const isNativeSDK = platform === "ios" || platform === "android" || platform === "react-native" || platform === "flutter";
|
40
|
+
const isWeb = platform === "web" || platform === "unknown";
|
41
|
+
|
42
|
+
return {
|
43
|
+
platform,
|
44
|
+
isMobile,
|
45
|
+
isNativeSDK,
|
46
|
+
isWeb,
|
47
|
+
isIOS: platform === "ios",
|
48
|
+
isAndroid: platform === "android",
|
49
|
+
isReactNative: platform === "react-native",
|
50
|
+
isFlutter: platform === "flutter"
|
51
|
+
};
|
52
|
+
}
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
2
|
+
import { publicService } from '@/services/api';
|
3
|
+
|
4
|
+
/**
|
5
|
+
* Hook to fetch trusted user count
|
6
|
+
*/
|
7
|
+
export function useTrustedCount() {
|
8
|
+
return useQuery({
|
9
|
+
queryKey: ['trusted-count'],
|
10
|
+
queryFn: publicService.getTrustedCount,
|
11
|
+
staleTime: 5 * 60 * 1000, // 5 minutes
|
12
|
+
retry: 2,
|
13
|
+
refetchOnWindowFocus: false,
|
14
|
+
select: (data) => {
|
15
|
+
return {
|
16
|
+
...data,
|
17
|
+
displayText: `Trusted by ${data.trustedCount} Indians`
|
18
|
+
};
|
19
|
+
}
|
20
|
+
});
|
21
|
+
}
|
@@ -0,0 +1,90 @@
|
|
1
|
+
import { useMutation } from '@tanstack/react-query'
|
2
|
+
import { DecodeUrlParams, decodeService } from '@/services/api/decode.service'
|
3
|
+
import { useRedirectStore } from '@/store/redirect.store'
|
4
|
+
import { useEffect } from 'react'
|
5
|
+
|
6
|
+
/**
|
7
|
+
* Parse URL search params to extract required decoding parameters
|
8
|
+
*/
|
9
|
+
export function extractUrlParams(): DecodeUrlParams | null {
|
10
|
+
// Check if we're in a browser environment
|
11
|
+
if (typeof window === 'undefined') return null
|
12
|
+
|
13
|
+
const urlParams = new URLSearchParams(window.location.search)
|
14
|
+
const fi = urlParams.get('fi')
|
15
|
+
const ecreq = urlParams.get('ecreq')
|
16
|
+
const reqdate = urlParams.get('reqdate')
|
17
|
+
const profile = urlParams.get('profile')
|
18
|
+
const platform = urlParams.get('platform')
|
19
|
+
const theme = urlParams.get('theme')
|
20
|
+
|
21
|
+
if (!fi || !ecreq || !reqdate) {
|
22
|
+
return null
|
23
|
+
}
|
24
|
+
|
25
|
+
return {
|
26
|
+
fi,
|
27
|
+
ecreq,
|
28
|
+
reqdate,
|
29
|
+
profile: profile || undefined,
|
30
|
+
platform: platform || undefined,
|
31
|
+
theme: theme || undefined
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
/**
|
36
|
+
* Hook for decoding URL parameters from redirection
|
37
|
+
*/
|
38
|
+
export function useUrlDecode() {
|
39
|
+
const { setDecodedInfo, setDecodingError, isDecoded } = useRedirectStore()
|
40
|
+
|
41
|
+
const decodeMutation = useMutation({
|
42
|
+
mutationFn: async (params: DecodeUrlParams) => {
|
43
|
+
try {
|
44
|
+
const result = await decodeService.decodeUrlParams(params)
|
45
|
+
|
46
|
+
// Handle specific platform and theme flags
|
47
|
+
if (params.platform) {
|
48
|
+
result.platform = params.platform;
|
49
|
+
}
|
50
|
+
|
51
|
+
if (params.theme) {
|
52
|
+
result.theme = params.theme;
|
53
|
+
}
|
54
|
+
|
55
|
+
// Log platform and theme for debugging
|
56
|
+
if (process.env.NODE_ENV !== 'production') {
|
57
|
+
console.log('Platform detected:', result.platform || 'none');
|
58
|
+
console.log('Theme detected:', result.theme || 'system (default)');
|
59
|
+
}
|
60
|
+
|
61
|
+
setDecodedInfo(result)
|
62
|
+
return result
|
63
|
+
} catch (error: Error | unknown) {
|
64
|
+
// console.error('Error decoding URL params:', error)
|
65
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to decode URL parameters'
|
66
|
+
setDecodingError(errorMessage)
|
67
|
+
throw error
|
68
|
+
}
|
69
|
+
}
|
70
|
+
})
|
71
|
+
|
72
|
+
// Automatically decode URL parameters on mount if not already decoded
|
73
|
+
useEffect(() => {
|
74
|
+
if (!isDecoded) {
|
75
|
+
const params = extractUrlParams()
|
76
|
+
if (params && decodeMutation.isIdle) {
|
77
|
+
decodeMutation.mutate(params)
|
78
|
+
}
|
79
|
+
}
|
80
|
+
}, [isDecoded])
|
81
|
+
|
82
|
+
return {
|
83
|
+
decode: (params: DecodeUrlParams) => decodeMutation.mutate(params),
|
84
|
+
isLoading: decodeMutation.isPending,
|
85
|
+
isError: decodeMutation.isError,
|
86
|
+
error: decodeMutation.error,
|
87
|
+
isSuccess: decodeMutation.isSuccess,
|
88
|
+
data: decodeMutation.data
|
89
|
+
}
|
90
|
+
}
|
@@ -0,0 +1,170 @@
|
|
1
|
+
import { useState, useCallback, useEffect } from 'react'
|
2
|
+
import { useLocation, useNavigate } from 'react-router-dom'
|
3
|
+
|
4
|
+
interface Step {
|
5
|
+
step: number
|
6
|
+
title: string
|
7
|
+
path: string
|
8
|
+
}
|
9
|
+
|
10
|
+
const steps: Step[] = [
|
11
|
+
{ step: 1, title: 'Login/Signup', path: '/login' },
|
12
|
+
{ step: 2, title: 'Link Accounts', path: '/link-accounts' },
|
13
|
+
// Category paths - all these are child steps of Link Accounts
|
14
|
+
{ step: 3, title: 'Banks', path: '/link-accounts/banks' },
|
15
|
+
{ step: 4, title: 'GST', path: '/link-accounts/gst' },
|
16
|
+
{ step: 5, title: 'Insurance', path: '/link-accounts/insurance' },
|
17
|
+
// Flow paths within each category
|
18
|
+
{ step: 10, title: 'Discovery', path: '/link-accounts/discovery' },
|
19
|
+
{ step: 11, title: 'Link', path: '/link-accounts/link' },
|
20
|
+
{
|
21
|
+
step: 12,
|
22
|
+
title: 'Discover Account',
|
23
|
+
path: '/link-accounts/discover-account'
|
24
|
+
},
|
25
|
+
// Final step
|
26
|
+
{ step: 13, title: 'Review Consent', path: '/review' }
|
27
|
+
]
|
28
|
+
|
29
|
+
interface UseStepProps {
|
30
|
+
initialStep?: number
|
31
|
+
totalSteps: number
|
32
|
+
onStepComplete?: (step: number) => void
|
33
|
+
}
|
34
|
+
|
35
|
+
export const useStep = ({
|
36
|
+
initialStep = 1,
|
37
|
+
totalSteps,
|
38
|
+
onStepComplete
|
39
|
+
}: UseStepProps) => {
|
40
|
+
const location = useLocation()
|
41
|
+
const navigate = useNavigate()
|
42
|
+
const [activeStep, setActiveStep] = useState(initialStep)
|
43
|
+
const [completedSteps, setCompletedSteps] = useState<number[]>([])
|
44
|
+
|
45
|
+
// Update active step when route changes
|
46
|
+
useEffect(() => {
|
47
|
+
const currentPath = location.pathname
|
48
|
+
// First check for exact matches
|
49
|
+
const exactMatchIndex = steps.findIndex(s => s.path === currentPath)
|
50
|
+
let step = 0
|
51
|
+
if (exactMatchIndex >= 0) {
|
52
|
+
step = steps[exactMatchIndex].step
|
53
|
+
} else {
|
54
|
+
// Handle nested paths like /link-accounts/banks
|
55
|
+
if (currentPath.startsWith('/link-accounts/')) {
|
56
|
+
const pathSegment = currentPath.split('/').pop() || ''
|
57
|
+
|
58
|
+
// Check if this is a category path
|
59
|
+
const categoryStep = steps.find(
|
60
|
+
s =>
|
61
|
+
s.path === `/link-accounts/${pathSegment}` &&
|
62
|
+
s.step >= 3 &&
|
63
|
+
s.step <= 5
|
64
|
+
)
|
65
|
+
|
66
|
+
if (categoryStep) {
|
67
|
+
// It's a category path
|
68
|
+
step = categoryStep.step
|
69
|
+
} else {
|
70
|
+
// It's a flow path (discovery, link, etc.) - set to Link Accounts step
|
71
|
+
step = 2
|
72
|
+
}
|
73
|
+
} else if (currentPath === '/link-accounts') {
|
74
|
+
// Base link accounts path
|
75
|
+
step = 2
|
76
|
+
} else if (currentPath === '/review') {
|
77
|
+
// Review page
|
78
|
+
step = 13
|
79
|
+
} else {
|
80
|
+
// Default to step 1
|
81
|
+
step = 1
|
82
|
+
}
|
83
|
+
}
|
84
|
+
|
85
|
+
if (step > 0 && step <= totalSteps) {
|
86
|
+
setActiveStep(step)
|
87
|
+
|
88
|
+
// Mark previous steps as completed
|
89
|
+
if (step > 1) {
|
90
|
+
setCompletedSteps(prev => {
|
91
|
+
// Add all steps from 1 to current-1 as completed
|
92
|
+
const mainSteps = [1]
|
93
|
+
|
94
|
+
// If we're on a category path (3, 4, 5), mark Link Accounts (2) as completed
|
95
|
+
if (step >= 3 && step <= 5) {
|
96
|
+
mainSteps.push(2)
|
97
|
+
}
|
98
|
+
|
99
|
+
// If we're on review, mark all main steps as completed
|
100
|
+
if (step === 13) {
|
101
|
+
mainSteps.push(2, 3, 4, 5)
|
102
|
+
}
|
103
|
+
|
104
|
+
return [...new Set([...prev, ...mainSteps])]
|
105
|
+
})
|
106
|
+
}
|
107
|
+
}
|
108
|
+
}, [location.pathname, totalSteps])
|
109
|
+
|
110
|
+
const nextStep = useCallback(() => {
|
111
|
+
if (activeStep < totalSteps) {
|
112
|
+
// Mark current step as completed
|
113
|
+
setCompletedSteps(prev => [...prev, activeStep])
|
114
|
+
const nextStepNumber = activeStep + 1
|
115
|
+
setActiveStep(nextStepNumber)
|
116
|
+
onStepComplete?.(activeStep)
|
117
|
+
|
118
|
+
// Navigate to next step's route
|
119
|
+
const nextStep = steps.find(s => s.step === nextStepNumber)
|
120
|
+
if (nextStep) {
|
121
|
+
navigate(nextStep.path)
|
122
|
+
}
|
123
|
+
}
|
124
|
+
}, [activeStep, totalSteps, onStepComplete, navigate])
|
125
|
+
|
126
|
+
const previousStep = useCallback(() => {
|
127
|
+
if (activeStep > 1) {
|
128
|
+
setCompletedSteps(prev => prev.filter(step => step !== activeStep - 1))
|
129
|
+
const prevStepNumber = activeStep - 1
|
130
|
+
setActiveStep(prevStepNumber)
|
131
|
+
|
132
|
+
// Navigate to previous step's route
|
133
|
+
const prevStep = steps.find(s => s.step === prevStepNumber)
|
134
|
+
if (prevStep) {
|
135
|
+
navigate(prevStep.path)
|
136
|
+
}
|
137
|
+
}
|
138
|
+
}, [activeStep, navigate])
|
139
|
+
|
140
|
+
const goToStep = useCallback(
|
141
|
+
(step: number) => {
|
142
|
+
if (step >= 1 && step <= totalSteps) {
|
143
|
+
setActiveStep(step)
|
144
|
+
setCompletedSteps(prev => prev.filter(s => s < step))
|
145
|
+
|
146
|
+
// Navigate to the specified step's route
|
147
|
+
const targetStep = steps.find(s => s.step === step)
|
148
|
+
if (targetStep) {
|
149
|
+
navigate(targetStep.path)
|
150
|
+
}
|
151
|
+
}
|
152
|
+
},
|
153
|
+
[totalSteps, navigate]
|
154
|
+
)
|
155
|
+
|
156
|
+
const resetSteps = useCallback(() => {
|
157
|
+
setActiveStep(initialStep)
|
158
|
+
setCompletedSteps([])
|
159
|
+
navigate('/login')
|
160
|
+
}, [initialStep, navigate])
|
161
|
+
|
162
|
+
return {
|
163
|
+
activeStep,
|
164
|
+
completedSteps,
|
165
|
+
nextStep,
|
166
|
+
previousStep,
|
167
|
+
goToStep,
|
168
|
+
resetSteps
|
169
|
+
}
|
170
|
+
}
|
package/src/index.css
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
@import "tailwindcss";
|
2
|
+
|
3
|
+
@plugin "tailwindcss-animate";
|
4
|
+
|
5
|
+
@custom-variant dark (&:is(.dark *));
|
6
|
+
|
7
|
+
:root {
|
8
|
+
--radius: 0.5rem;
|
9
|
+
--background: oklch(98.46% 0.002 247.84);
|
10
|
+
--foreground: oklch(0.15 0 0);
|
11
|
+
--card: oklch(0.98 0 0);
|
12
|
+
--card-foreground: oklch(0.15 0 0);
|
13
|
+
--popover: oklch(0.98 0 0);
|
14
|
+
--popover-foreground: oklch(0.15 0 0);
|
15
|
+
--primary: oklch(59.02% 0.1008 205.29);
|
16
|
+
--primary-foreground: oklch(0.99 0 0);
|
17
|
+
--secondary: oklch(74.39% 0.1159 203.57);
|
18
|
+
--secondary-foreground: oklch(59.02% 0.1008 205.29);
|
19
|
+
--muted: oklch(0.95 0 0);
|
20
|
+
--muted-foreground: oklch(0.4 0 0);
|
21
|
+
--accent: oklch(0.95 0 0);
|
22
|
+
--accent-foreground: oklch(59.02% 0.1008 205.29);
|
23
|
+
--destructive: oklch(0.5 0.3 30);
|
24
|
+
--border: oklch(0.9 0 0);
|
25
|
+
--input: oklch(0.9 0 0);
|
26
|
+
--ring: oklch(0.7 0 0);
|
27
|
+
--chart-1: oklch(0.6 0.3 187.5);
|
28
|
+
--chart-2: oklch(0.6 0.3 30);
|
29
|
+
--chart-3: oklch(0.6 0.3 240);
|
30
|
+
--chart-4: oklch(0.6 0.3 270);
|
31
|
+
--chart-5: oklch(0.6 0.3 120);
|
32
|
+
--sidebar: oklch(0.98 0 0);
|
33
|
+
--sidebar-foreground: oklch(0.15 0 0);
|
34
|
+
--sidebar-primary: oklch(59.02% 0.1008 205.29);
|
35
|
+
--sidebar-primary-foreground: oklch(0.99 0 0);
|
36
|
+
--sidebar-accent: oklch(0.95 0 0);
|
37
|
+
--sidebar-accent-foreground: oklch(59.02% 0.1008 205.29);
|
38
|
+
--sidebar-border: oklch(0.9 0 0);
|
39
|
+
--sidebar-ring: oklch(0.7 0 0);
|
40
|
+
--ring: oklch(96.42% 0.0181 205.32);
|
41
|
+
--danger: oklch(55.62% 0.2088 24.15);
|
42
|
+
--muted-secondary: oklch(54.44% 0.035 265.11);
|
43
|
+
--consent-primary: oklch(36.95% 0.0381 260.5);
|
44
|
+
--consent-secondary: oklch(0.54 0.035 265.11);
|
45
|
+
--placeholder: oklch(0.71 0.0273 261.12);
|
46
|
+
|
47
|
+
|
48
|
+
--border-primary: oklch(87.15% 0.0123 259.82);
|
49
|
+
--border-secondary: oklch(0.97 0.0045 258.32);
|
50
|
+
|
51
|
+
--surface: oklch(0.98 0.0017 247.84);
|
52
|
+
|
53
|
+
}
|
54
|
+
|
55
|
+
.dark {
|
56
|
+
--background: oklch(20.22% 0.033 232.43);
|
57
|
+
/* --background: oklch(0.15 0 0); */
|
58
|
+
--foreground: oklch(0.95 0 0);
|
59
|
+
--card: oklch(23.81% 0.041 232.18);
|
60
|
+
--card-foreground: oklch(0.95 0 0);
|
61
|
+
--popover: oklch(0.2 0 0);
|
62
|
+
--popover-foreground: oklch(0.95 0 0);
|
63
|
+
--primary: oklch(0.6 0.3 187.5);
|
64
|
+
--primary-foreground: oklch(0.99 0 0);
|
65
|
+
--secondary: oklch(0.25 0 0);
|
66
|
+
--secondary-foreground: oklch(0.95 0 0);
|
67
|
+
--muted: oklch(0.25 0 0);
|
68
|
+
--muted-foreground: oklch(88.91% 0.018 229.03);
|
69
|
+
--accent: oklch(0.25 0 0);
|
70
|
+
--accent-foreground: oklch(0.95 0 0);
|
71
|
+
--destructive: oklch(0.7 0.3 30);
|
72
|
+
--border: oklch(30.13% 0.057 236.60);
|
73
|
+
--input: oklch(30.13% 0.057 236.60);
|
74
|
+
--ring: oklch(0.5 0 0);
|
75
|
+
--chart-1: oklch(0.4 0.3 187.5);
|
76
|
+
--chart-2: oklch(0.4 0.3 30);
|
77
|
+
--chart-3: oklch(0.4 0.3 240);
|
78
|
+
--chart-4: oklch(0.4 0.3 270);
|
79
|
+
--chart-5: oklch(0.4 0.3 120);
|
80
|
+
--sidebar: oklch(0.2 0 0);
|
81
|
+
--sidebar-foreground: oklch(0.95 0 0);
|
82
|
+
--sidebar-primary: oklch(0.4 0.3 187.5);
|
83
|
+
--sidebar-primary-foreground: oklch(0.99 0 0);
|
84
|
+
--sidebar-accent: oklch(0.25 0 0);
|
85
|
+
--sidebar-accent-foreground: oklch(0.95 0 0);
|
86
|
+
--sidebar-border: oklch(0.3 0 0);
|
87
|
+
--sidebar-ring: oklch(0.5 0 0);
|
88
|
+
--muted-secondary: oklch(77.53% 0.036 227.57);
|
89
|
+
--ring: oklch(30.13% 0.057 236.60);
|
90
|
+
}
|
91
|
+
|
92
|
+
@theme inline {
|
93
|
+
--radius-sm: calc(var(--radius) - 4px);
|
94
|
+
--radius-md: calc(var(--radius) - 2px);
|
95
|
+
--radius-lg: var(--radius);
|
96
|
+
--radius-xl: calc(var(--radius) + 4px);
|
97
|
+
--color-background: var(--background);
|
98
|
+
--color-foreground: var(--foreground);
|
99
|
+
--color-card: var(--card);
|
100
|
+
--color-card-foreground: var(--card-foreground);
|
101
|
+
--color-popover: var(--popover);
|
102
|
+
--color-popover-foreground: var(--popover-foreground);
|
103
|
+
--color-primary: var(--primary);
|
104
|
+
--color-primary-foreground: var(--primary-foreground);
|
105
|
+
--color-secondary: var(--secondary);
|
106
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
107
|
+
--color-muted: var(--muted);
|
108
|
+
--color-muted-foreground: var(--muted-foreground);
|
109
|
+
--color-accent: var(--accent);
|
110
|
+
--color-accent-foreground: var(--accent-foreground);
|
111
|
+
--color-destructive: var(--destructive);
|
112
|
+
--color-border: var(--border);
|
113
|
+
--color-input: var(--input);
|
114
|
+
--color-ring: var(--ring);
|
115
|
+
--color-chart-1: var(--chart-1);
|
116
|
+
--color-chart-2: var(--chart-2);
|
117
|
+
--color-chart-3: var(--chart-3);
|
118
|
+
--color-chart-4: var(--chart-4);
|
119
|
+
--color-chart-5: var(--chart-5);
|
120
|
+
--color-sidebar: var(--sidebar);
|
121
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
122
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
123
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
124
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
125
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
126
|
+
--color-sidebar-border: var(--sidebar-border);
|
127
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
128
|
+
--color-danger: var(--danger);
|
129
|
+
--color-muted-secondary: var(--muted-secondary);
|
130
|
+
--color-consent-primary: var(--consent-primary);
|
131
|
+
--color-consent-secondary: var(--consent-secondary);
|
132
|
+
--color-placeholder: var(--placeholder);
|
133
|
+
|
134
|
+
--color-border-primary: var(--border-primary);
|
135
|
+
--color-border-secondary: var(--border-secondary);
|
136
|
+
|
137
|
+
--color-surface: var(--surface);
|
138
|
+
|
139
|
+
}
|
140
|
+
|
141
|
+
@layer base {
|
142
|
+
* {
|
143
|
+
@apply border-border outline-ring/50;
|
144
|
+
}
|
145
|
+
body {
|
146
|
+
@apply bg-background text-foreground;
|
147
|
+
color: #344054;
|
148
|
+
}
|
149
|
+
}
|
150
|
+
|
151
|
+
body {
|
152
|
+
font-family: 'Poppins', sans-serif;
|
153
|
+
|
154
|
+
}
|
@@ -0,0 +1,39 @@
|
|
1
|
+
/**
|
2
|
+
* Type definitions for App component and related functionality
|
3
|
+
*/
|
4
|
+
|
5
|
+
// Props for URL decode handler component
|
6
|
+
export interface UrlDecodeHandlerProps {
|
7
|
+
children: React.ReactNode;
|
8
|
+
}
|
9
|
+
|
10
|
+
// Custom style interface used in redirect store
|
11
|
+
export interface CustomStyle {
|
12
|
+
primaryButtonColor?: string;
|
13
|
+
primaryBackgroundColor?: string;
|
14
|
+
secondaryButtonColor?: string;
|
15
|
+
textColor?: string;
|
16
|
+
backgroundColor?: string;
|
17
|
+
logoUrl?: string;
|
18
|
+
}
|
19
|
+
|
20
|
+
// Decoded information from URL
|
21
|
+
export interface DecodedInfo {
|
22
|
+
redirectUrl?: string;
|
23
|
+
entityId?: string;
|
24
|
+
purpose?: string;
|
25
|
+
customStyle?: CustomStyle;
|
26
|
+
[key: string]: any;
|
27
|
+
}
|
28
|
+
|
29
|
+
// Theme context related interfaces
|
30
|
+
export interface ThemeProviderProps {
|
31
|
+
children: React.ReactNode;
|
32
|
+
defaultTheme: string;
|
33
|
+
}
|
34
|
+
|
35
|
+
// Navigation block context types
|
36
|
+
export interface NavigationBlockContextType {
|
37
|
+
allowNavigation: () => void;
|
38
|
+
shouldAllowNavigation: () => boolean;
|
39
|
+
}
|
@@ -0,0 +1,65 @@
|
|
1
|
+
/**
|
2
|
+
* Type definitions for API services
|
3
|
+
*/
|
4
|
+
import { InternalAxiosRequestConfig } from 'axios';
|
5
|
+
|
6
|
+
// Common API response interfaces
|
7
|
+
export interface ApiResponse<T = any> {
|
8
|
+
data: T;
|
9
|
+
success: boolean;
|
10
|
+
message?: string;
|
11
|
+
}
|
12
|
+
|
13
|
+
// Auth related interfaces
|
14
|
+
export interface LoginRequest {
|
15
|
+
phone: string;
|
16
|
+
otp: string;
|
17
|
+
}
|
18
|
+
|
19
|
+
export interface TokenResponse {
|
20
|
+
access_token: string;
|
21
|
+
refresh_token: string;
|
22
|
+
token_type: string;
|
23
|
+
expires_in: number;
|
24
|
+
}
|
25
|
+
|
26
|
+
export interface UserProfile {
|
27
|
+
id: string;
|
28
|
+
name: string;
|
29
|
+
phone: string;
|
30
|
+
email?: string;
|
31
|
+
}
|
32
|
+
|
33
|
+
// Axios request interface with retry flag
|
34
|
+
export interface RetryableRequest extends InternalAxiosRequestConfig {
|
35
|
+
_retry?: boolean;
|
36
|
+
}
|
37
|
+
|
38
|
+
// Account related interfaces
|
39
|
+
export interface AccountDetails {
|
40
|
+
id: string;
|
41
|
+
accountNumber: string;
|
42
|
+
accountType: string;
|
43
|
+
bankName: string;
|
44
|
+
ifsc?: string;
|
45
|
+
linkedAt?: string;
|
46
|
+
}
|
47
|
+
|
48
|
+
// FIP related interfaces
|
49
|
+
export interface FipDetails {
|
50
|
+
id: string;
|
51
|
+
name: string;
|
52
|
+
logo: string;
|
53
|
+
type: string;
|
54
|
+
category?: string;
|
55
|
+
}
|
56
|
+
|
57
|
+
// Consent related interfaces
|
58
|
+
export interface ConsentDetails {
|
59
|
+
id: string;
|
60
|
+
purpose: string;
|
61
|
+
dataRequested: string[];
|
62
|
+
expiry: string;
|
63
|
+
approvedAt?: string;
|
64
|
+
status: 'pending' | 'approved' | 'rejected';
|
65
|
+
}
|