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,76 @@
|
|
1
|
+
import { ReactNode, useEffect } from 'react';
|
2
|
+
import { Navigate, useLocation, useNavigate } from 'react-router-dom';
|
3
|
+
import { useAuthStore } from '@/store/auth.store';
|
4
|
+
import { useRedirectStore } from '@/store/redirect.store';
|
5
|
+
import { extractCategories, useFipStore } from '@/store/fip.store';
|
6
|
+
import handleIdentifiers from '@/utils/handleIdentifiers';
|
7
|
+
|
8
|
+
interface AuthGuardProps {
|
9
|
+
children: ReactNode;
|
10
|
+
requireAuth?: boolean;
|
11
|
+
}
|
12
|
+
|
13
|
+
/**
|
14
|
+
* AuthGuard component to protect routes
|
15
|
+
*
|
16
|
+
* @param children - Child components to render if authentication check passes
|
17
|
+
* @param requireAuth - Whether authentication is required (defaults to true)
|
18
|
+
*/
|
19
|
+
const AuthGuard = ({ children, requireAuth = true }: AuthGuardProps) => {
|
20
|
+
const { isAuthenticated, refreshToken } = useAuthStore();
|
21
|
+
const { decodedInfo } = useRedirectStore();
|
22
|
+
const location = useLocation();
|
23
|
+
const { setActiveCategory } = useFipStore();
|
24
|
+
|
25
|
+
// Check if refresh token exists but user is not authenticated
|
26
|
+
useEffect(() => {
|
27
|
+
if (!isAuthenticated && refreshToken) {
|
28
|
+
// Attempt to refresh token automatically
|
29
|
+
const attemptRefresh = async () => {
|
30
|
+
try {
|
31
|
+
const authService = (await import('@/services/api/auth.service')).authService;
|
32
|
+
await authService.refreshToken(refreshToken);
|
33
|
+
}
|
34
|
+
catch (error) {
|
35
|
+
// Token refresh failed, handle silently
|
36
|
+
// console.error('Token refresh failed:', error);
|
37
|
+
}
|
38
|
+
};
|
39
|
+
|
40
|
+
attemptRefresh();
|
41
|
+
}
|
42
|
+
}, [isAuthenticated, refreshToken]);
|
43
|
+
|
44
|
+
// Redirect to login if authentication is required but user is not authenticated
|
45
|
+
if (requireAuth && !isAuthenticated) {
|
46
|
+
return <Navigate to="/login" state={{ from: location }} replace />;
|
47
|
+
}
|
48
|
+
|
49
|
+
// Handle authenticated user navigation based on decodedInfo
|
50
|
+
if (!requireAuth && isAuthenticated && decodedInfo) {
|
51
|
+
// if (decodedInfo.isUserPresent) {
|
52
|
+
// return <Navigate to="/link-accounts/old-user" replace />;
|
53
|
+
// } else {
|
54
|
+
// if (decodedInfo?.fiTypesRequiredForConsent.length > 0) {
|
55
|
+
// const fiTypes = extractCategories(decodedInfo?.fiTypesRequiredForConsent);
|
56
|
+
// return <Navigate to={`/link-accounts/${fiTypes?.[0]?.toLowerCase()}`} replace />;
|
57
|
+
// } else {
|
58
|
+
// return <Navigate to="/link-accounts/banks" replace />;
|
59
|
+
// }
|
60
|
+
// }
|
61
|
+
|
62
|
+
if (decodedInfo.isUserPresent) {
|
63
|
+
return <Navigate to="/link-accounts/old-user" replace />;
|
64
|
+
} else if (decodedInfo?.fiTypesRequiredForConsent.length > 0) {
|
65
|
+
const fiTypes = extractCategories(decodedInfo?.fiTypesRequiredForConsent);
|
66
|
+
setActiveCategory(fiTypes?.[0]);
|
67
|
+
return <Navigate to={`/link-accounts/${fiTypes?.[0]?.toLowerCase()}`} replace />;
|
68
|
+
} else {
|
69
|
+
return <Navigate to="/link-accounts/banks" replace />;
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
return <>{children}</>;
|
74
|
+
};
|
75
|
+
|
76
|
+
export default AuthGuard;
|
@@ -0,0 +1,69 @@
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
2
|
+
import BankCard from "./BankCard";
|
3
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
4
|
+
|
5
|
+
const meta: Meta<typeof BankCard> = {
|
6
|
+
title: "UI/BankCard",
|
7
|
+
component: BankCard,
|
8
|
+
parameters: {
|
9
|
+
layout: "padded",
|
10
|
+
},
|
11
|
+
tags: ["autodocs"],
|
12
|
+
};
|
13
|
+
|
14
|
+
export default meta;
|
15
|
+
type Story = StoryObj<typeof BankCard>;
|
16
|
+
|
17
|
+
export const Default: Story = {
|
18
|
+
args: {
|
19
|
+
bankName: "Axis Bank",
|
20
|
+
image: "/images/bank-logo.png",
|
21
|
+
imageSize: "w-8 h-8",
|
22
|
+
},
|
23
|
+
};
|
24
|
+
|
25
|
+
export const WithSubText: Story = {
|
26
|
+
args: {
|
27
|
+
bankName: "Axis Bank",
|
28
|
+
subText: "Savings Account",
|
29
|
+
image: "/images/bank-logo.png",
|
30
|
+
imageSize: "w-8 h-8",
|
31
|
+
},
|
32
|
+
};
|
33
|
+
|
34
|
+
export const WithBadge: Story = {
|
35
|
+
args: {
|
36
|
+
bankName: "Axis Bank",
|
37
|
+
badgeText: "New",
|
38
|
+
image: "/images/bank-logo.png",
|
39
|
+
imageSize: "w-8 h-8",
|
40
|
+
},
|
41
|
+
};
|
42
|
+
|
43
|
+
export const WithCheckbox: Story = {
|
44
|
+
args: {
|
45
|
+
bankName: "Axis Bank",
|
46
|
+
rightSection: <Checkbox />,
|
47
|
+
image: "/images/bank-logo.png",
|
48
|
+
imageSize: "w-8 h-8",
|
49
|
+
},
|
50
|
+
};
|
51
|
+
|
52
|
+
export const Loading: Story = {
|
53
|
+
args: {
|
54
|
+
bankName: "Axis Bank",
|
55
|
+
subText: "Loading...",
|
56
|
+
loading: true,
|
57
|
+
image: "/images/bank-logo.png",
|
58
|
+
imageSize: "w-8 h-8",
|
59
|
+
},
|
60
|
+
};
|
61
|
+
|
62
|
+
export const CustomClassName: Story = {
|
63
|
+
args: {
|
64
|
+
bankName: "Axis Bank",
|
65
|
+
className: "bg-gray-100 p-4 rounded-lg",
|
66
|
+
image: "/images/bank-logo.png",
|
67
|
+
imageSize: "w-8 h-8",
|
68
|
+
},
|
69
|
+
};
|
@@ -0,0 +1,227 @@
|
|
1
|
+
import React, { ReactNode, useEffect } from 'react'
|
2
|
+
import { Skeleton } from '../ui/skeleton'
|
3
|
+
import { Bell, Building } from 'lucide-react'
|
4
|
+
import { motion } from 'framer-motion'
|
5
|
+
import { useRTL } from '@/contexts/RTLContext'
|
6
|
+
import { AnimatedButton } from '../ui/animatedButton'
|
7
|
+
import { useIsMobile } from '@/hooks/use-mobile'
|
8
|
+
|
9
|
+
const shimmerVariants = {
|
10
|
+
animate: {
|
11
|
+
x: ['-100%', '400%'],
|
12
|
+
transition: {
|
13
|
+
duration: 0.6, // shimmer speed
|
14
|
+
ease: 'easeInOut',
|
15
|
+
repeat: Infinity,
|
16
|
+
repeatDelay: 0.6 // delay after each shimmer
|
17
|
+
}
|
18
|
+
}
|
19
|
+
}
|
20
|
+
|
21
|
+
interface cardContentProps {
|
22
|
+
bankName: string
|
23
|
+
subText?: string | boolean
|
24
|
+
badgeText?: string | boolean
|
25
|
+
rightSection?: ReactNode
|
26
|
+
image?: string
|
27
|
+
className?: string
|
28
|
+
imageSize?: string
|
29
|
+
loading?: boolean
|
30
|
+
selected?: boolean
|
31
|
+
selectedTextColor?: string
|
32
|
+
onClick?: (e: React.MouseEvent) => void
|
33
|
+
commingSoon?: boolean
|
34
|
+
commingSoonFun?: () => void
|
35
|
+
notify?: boolean
|
36
|
+
}
|
37
|
+
|
38
|
+
// Define interface for right section props
|
39
|
+
interface RightSectionProps {
|
40
|
+
checked?: boolean;
|
41
|
+
defaultChecked?: boolean;
|
42
|
+
'data-state'?: string;
|
43
|
+
id?: string;
|
44
|
+
[key: string]: unknown;
|
45
|
+
|
46
|
+
}
|
47
|
+
|
48
|
+
const BankCard = ({
|
49
|
+
bankName,
|
50
|
+
subText,
|
51
|
+
badgeText,
|
52
|
+
rightSection,
|
53
|
+
image,
|
54
|
+
className = '',
|
55
|
+
imageSize = 'w-12 h-12',
|
56
|
+
loading = false,
|
57
|
+
selected = false,
|
58
|
+
selectedTextColor = '',
|
59
|
+
notify = false,
|
60
|
+
commingSoon = false,
|
61
|
+
commingSoonFun = () => { },
|
62
|
+
onClick
|
63
|
+
}: cardContentProps) => {
|
64
|
+
const { isRTL } = useRTL();
|
65
|
+
const isMobile = useIsMobile();
|
66
|
+
const selectedClass = selected ? selectedTextColor : ''
|
67
|
+
|
68
|
+
useEffect(() => {
|
69
|
+
if (selected && rightSection) {
|
70
|
+
const setCheckedState = (element: HTMLElement) => {
|
71
|
+
if (
|
72
|
+
element.getAttribute('role') === 'checkbox' ||
|
73
|
+
element.getAttribute('role') === 'radio'
|
74
|
+
) {
|
75
|
+
if (element.getAttribute('data-state') !== 'checked' && selected) {
|
76
|
+
const checkboxOrRadio = element.querySelector('input')
|
77
|
+
if (checkboxOrRadio) {
|
78
|
+
if (!checkboxOrRadio.checked && selected) {
|
79
|
+
checkboxOrRadio.checked = true
|
80
|
+
}
|
81
|
+
}
|
82
|
+
}
|
83
|
+
}
|
84
|
+
}
|
85
|
+
|
86
|
+
if (React.isValidElement(rightSection)) {
|
87
|
+
const props = rightSection.props as Record<string, unknown>
|
88
|
+
if (props && typeof props === 'object' && 'id' in props) {
|
89
|
+
const rightSectionElement = document.getElementById(props.id as string)
|
90
|
+
if (rightSectionElement) {
|
91
|
+
setCheckedState(rightSectionElement)
|
92
|
+
}
|
93
|
+
}
|
94
|
+
}
|
95
|
+
}
|
96
|
+
}, [selected, rightSection])
|
97
|
+
|
98
|
+
const enhancedRightSection = React.isValidElement(rightSection)
|
99
|
+
? React.cloneElement(rightSection as React.ReactElement<RightSectionProps>, {
|
100
|
+
checked: selected,
|
101
|
+
defaultChecked: selected,
|
102
|
+
'data-state': selected ? 'checked' : undefined
|
103
|
+
})
|
104
|
+
: rightSection
|
105
|
+
|
106
|
+
// Placeholder for missing bank name
|
107
|
+
const displayName = bankName || 'Unknown Bank'
|
108
|
+
|
109
|
+
// Create order of elements based on RTL or LTR
|
110
|
+
const bankCardElements = [
|
111
|
+
// Left element (image)
|
112
|
+
loading ? (
|
113
|
+
<Skeleton key="image" className='w-10 h-9' />
|
114
|
+
) : (
|
115
|
+
<div
|
116
|
+
key="image"
|
117
|
+
className={`flex items-center justify-center ${isMobile ? 'w-10 h-10' : imageSize} rounded-md overflow-hidden`}
|
118
|
+
>
|
119
|
+
{image ? (
|
120
|
+
<img
|
121
|
+
src={image}
|
122
|
+
className={`object-contain ${isMobile ? 'w-10 h-10' : imageSize}`}
|
123
|
+
onError={e => {
|
124
|
+
const target = e.target as HTMLImageElement
|
125
|
+
target.onerror = null
|
126
|
+
target.style.display = 'none'
|
127
|
+
if (target.parentElement) {
|
128
|
+
target.parentElement.innerHTML = `<div class="flex items-center justify-center ${imageSize} bg-gray-200 text-gray-500"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-building"><rect width="16" height="20" x="4" y="2" rx="2" ry="2"/><path d="M9 22v-4h6v4"/><path d="M8 6h.01"/><path d="M16 6h.01"/><path d="M12 6h.01"/><path d="M12 10h.01"/><path d="M12 14h.01"/><path d="M16 10h.01"/><path d="M16 14h.01"/><path d="M8 10h.01"/><path d="M8 14h.01"/></svg></div>`
|
129
|
+
}
|
130
|
+
}}
|
131
|
+
/>
|
132
|
+
) : (
|
133
|
+
<Building size={24} className='text-gray-500' />
|
134
|
+
)}
|
135
|
+
</div>
|
136
|
+
),
|
137
|
+
|
138
|
+
|
139
|
+
// Middle element (content)
|
140
|
+
<div key="content" className={`flex items-center justify-between w-[90%] overflow-hidden`}>
|
141
|
+
<div className={`flex flex-col gap-[8px] flex-1 ${isRTL ? 'items-end' : 'items-start'} justify-center`}>
|
142
|
+
<div className={`flex items-center ${isRTL ? 'w-[96%] justify-end' : 'w-[95%]'} gap-2`}>
|
143
|
+
{loading ? (
|
144
|
+
<Skeleton className='w-[80%] md:w-[30%] overflow-ellipsis h-4 mt-1' />
|
145
|
+
) : (
|
146
|
+
<p
|
147
|
+
className={`font-[500] text-sm md:text-base w-full overflow-ellipsis ${selectedClass} truncate dark:text-gray-300 ${isRTL ? 'text-left' : ''}`}
|
148
|
+
>
|
149
|
+
{displayName}
|
150
|
+
</p>
|
151
|
+
)}
|
152
|
+
{!loading && badgeText ? (
|
153
|
+
<div className={`relative rounded-xl px-2 py-[2px] flex items-center overflow-hidden flex-shrink-0 bg-gradient-to-r from-primary to-secondary ${isRTL ? 'flex-row-reverse' : ''}`}>
|
154
|
+
{/* Shimmer layer */}
|
155
|
+
<motion.div
|
156
|
+
className='absolute top-0 left-0 h-full w-[20px] rounded-md'
|
157
|
+
style={{
|
158
|
+
background:
|
159
|
+
'linear-gradient(to right, transparent, white, transparent)',
|
160
|
+
transform: 'rotate(-70deg)',
|
161
|
+
zIndex: 0
|
162
|
+
}}
|
163
|
+
variants={shimmerVariants}
|
164
|
+
animate='animate'
|
165
|
+
/>
|
166
|
+
{/* Text */}
|
167
|
+
<p className='text-[8px] md:text-sm text-white relative z-10'>{badgeText}</p>
|
168
|
+
</div>
|
169
|
+
) : null}
|
170
|
+
</div>
|
171
|
+
{subText !== false ? (
|
172
|
+
loading ? (
|
173
|
+
<Skeleton className='w-[70%] md:w-[70px] h-4' />
|
174
|
+
) : (
|
175
|
+
<div className={`flex items-center ${isRTL ? 'w-[96%] justify-end' : ' w-[95%]'} gap-2`}>
|
176
|
+
<p
|
177
|
+
className={`font-[500] text-sm md:text-md w-full overflow-ellipsis ${selectedClass} truncate ${isRTL ? 'text-left' : ''} dark:text-gray-400`}
|
178
|
+
>
|
179
|
+
{subText}
|
180
|
+
</p>
|
181
|
+
</div>
|
182
|
+
)
|
183
|
+
) : null}
|
184
|
+
</div>
|
185
|
+
</div>,
|
186
|
+
|
187
|
+
// Right element (checkbox/radio)
|
188
|
+
<div key="rightSection" className='flex items-center'>
|
189
|
+
{commingSoon ?
|
190
|
+
notify ? (
|
191
|
+
<AnimatedButton
|
192
|
+
variant={'outline'}
|
193
|
+
className='text-black dark:text-white border-gray-100 text-xs md:text-md hover:text-black hover:dark:text-white hover:bg-gray-50'
|
194
|
+
>
|
195
|
+
<Bell /> You'll be notified
|
196
|
+
</AnimatedButton>
|
197
|
+
) : (
|
198
|
+
<AnimatedButton
|
199
|
+
variant={'outline'}
|
200
|
+
className='text-black dark:text-white border-gray-100 text-xs md:text-md'
|
201
|
+
onClick={commingSoonFun}
|
202
|
+
>
|
203
|
+
<Bell /> Notify me
|
204
|
+
</AnimatedButton>
|
205
|
+
)
|
206
|
+
: enhancedRightSection
|
207
|
+
}
|
208
|
+
</div>
|
209
|
+
];
|
210
|
+
|
211
|
+
// Reverse the order for RTL languages
|
212
|
+
if (isRTL) {
|
213
|
+
bankCardElements.reverse();
|
214
|
+
}
|
215
|
+
|
216
|
+
return (
|
217
|
+
<div
|
218
|
+
className={`flex items-center w-full ${isRTL ? 'gap-0' : 'gap-4'} ${className} ${isRTL ? 'rtl-direction flex-row-reverse justify-end' : ''}`}
|
219
|
+
onClick={onClick}
|
220
|
+
dir={isRTL ? 'rtl' : 'ltr'}
|
221
|
+
>
|
222
|
+
{bankCardElements}
|
223
|
+
</div>
|
224
|
+
)
|
225
|
+
}
|
226
|
+
|
227
|
+
export default BankCard
|
@@ -0,0 +1,109 @@
|
|
1
|
+
import React, { ReactNode, useState } from 'react'
|
2
|
+
import { useRTL } from '@/contexts/RTLContext'
|
3
|
+
|
4
|
+
interface OuterCardProp {
|
5
|
+
selected?: boolean
|
6
|
+
children: ReactNode
|
7
|
+
className?: string
|
8
|
+
hoverEffect?: boolean
|
9
|
+
loading?: boolean
|
10
|
+
onSelect?: (selected: boolean) => void
|
11
|
+
noAction?: boolean
|
12
|
+
onClick?: (e: React.MouseEvent) => void
|
13
|
+
}
|
14
|
+
|
15
|
+
// Add this interface for child props
|
16
|
+
interface ChildProps {
|
17
|
+
onClick?: (e: React.MouseEvent) => void
|
18
|
+
loading?: boolean
|
19
|
+
selected?: boolean
|
20
|
+
}
|
21
|
+
|
22
|
+
const OuterCard = ({
|
23
|
+
selected = false ,
|
24
|
+
children,
|
25
|
+
className = '',
|
26
|
+
hoverEffect = true,
|
27
|
+
loading = false,
|
28
|
+
onSelect,
|
29
|
+
noAction = false,
|
30
|
+
onClick
|
31
|
+
}: OuterCardProp) => {
|
32
|
+
const { isRTL } = useRTL();
|
33
|
+
const [isSelected, setIsSelected] = useState(selected)
|
34
|
+
|
35
|
+
// Use prop value if provided, otherwise use internal state
|
36
|
+
const currentSelected = onSelect ? selected : isSelected
|
37
|
+
|
38
|
+
const addedStyle = currentSelected
|
39
|
+
? 'border-primary bg-ring dark:text-primary'
|
40
|
+
: `bg-white dark:bg-card ${hoverEffect
|
41
|
+
? 'hover:bg-background hover:shadow-[0_0_2px_0_#1054760F]'
|
42
|
+
: ''
|
43
|
+
} drop-shadow-xs md:drop-shadow-xs`
|
44
|
+
|
45
|
+
const handleCardClick = (e: React.MouseEvent) => {
|
46
|
+
if (onClick) {
|
47
|
+
onClick(e)
|
48
|
+
return
|
49
|
+
}
|
50
|
+
// Don't trigger card selection if clicking on checkbox/radio or if noAction is true
|
51
|
+
if (noAction) return
|
52
|
+
|
53
|
+
// Check if the clicked element is a checkbox, radio, or their label
|
54
|
+
const target = e.target as HTMLElement
|
55
|
+
const isInput = target.tagName === 'INPUT'
|
56
|
+
const isRadioIndicator =
|
57
|
+
target.closest('[data-radix-collection-item]') ||
|
58
|
+
target.closest('[role="radio"]') ||
|
59
|
+
target.closest('.RadioGroupIndicator') ||
|
60
|
+
target.closest('svg')
|
61
|
+
const isCheckbox =
|
62
|
+
target.closest('[role="checkbox"]') || target.closest('[data-state]')
|
63
|
+
|
64
|
+
if (isInput || isRadioIndicator || isCheckbox) {
|
65
|
+
// Let the native checkbox/radio handle the click
|
66
|
+
return
|
67
|
+
}
|
68
|
+
|
69
|
+
if (onSelect) {
|
70
|
+
// If parent is controlling selection
|
71
|
+
onSelect(!selected)
|
72
|
+
|
73
|
+
// Find any radio or checkbox inside and trigger click if needed
|
74
|
+
const radioOrCheckbox = (e.currentTarget as HTMLElement).querySelector(
|
75
|
+
'input[type="radio"], input[type="checkbox"]'
|
76
|
+
) as HTMLInputElement
|
77
|
+
if (radioOrCheckbox && !currentSelected) {
|
78
|
+
radioOrCheckbox.click()
|
79
|
+
}
|
80
|
+
} else {
|
81
|
+
// Internal state management
|
82
|
+
setIsSelected(!isSelected)
|
83
|
+
}
|
84
|
+
}
|
85
|
+
|
86
|
+
return (
|
87
|
+
<div
|
88
|
+
onClick={handleCardClick}
|
89
|
+
className={
|
90
|
+
`flex items-center gap-2 md:gap-4 border-1 border-border-secondary dark:border-border px-2 md:px-3.5 py-2 md:py-3 w-full rounded-lg ${!noAction ? 'cursor-pointer' : ''
|
91
|
+
}` +
|
92
|
+
' ' +
|
93
|
+
addedStyle +
|
94
|
+
' ' +
|
95
|
+
className
|
96
|
+
}
|
97
|
+
dir={isRTL ? 'rtl' : 'ltr'}
|
98
|
+
>
|
99
|
+
{React.isValidElement(children)
|
100
|
+
? React.cloneElement(children as React.ReactElement<ChildProps>, {
|
101
|
+
loading,
|
102
|
+
selected: currentSelected
|
103
|
+
})
|
104
|
+
: children}
|
105
|
+
</div>
|
106
|
+
)
|
107
|
+
}
|
108
|
+
|
109
|
+
export default OuterCard
|
@@ -0,0 +1,64 @@
|
|
1
|
+
import React, { useState } from 'react'
|
2
|
+
|
3
|
+
interface WrapperCardProps {
|
4
|
+
children: React.ReactNode
|
5
|
+
className?: string
|
6
|
+
selected?: boolean
|
7
|
+
onSelect?: (selected: boolean) => void
|
8
|
+
hoverEffect?: boolean
|
9
|
+
noAction?: boolean
|
10
|
+
}
|
11
|
+
|
12
|
+
const WrapperCard = ({
|
13
|
+
children,
|
14
|
+
className = '',
|
15
|
+
selected = false,
|
16
|
+
onSelect,
|
17
|
+
hoverEffect = true,
|
18
|
+
noAction = false
|
19
|
+
}: WrapperCardProps) => {
|
20
|
+
const [isSelected, setIsSelected] = useState(selected);
|
21
|
+
|
22
|
+
// Use prop value if provided, otherwise use internal state
|
23
|
+
const currentSelected = onSelect ? selected : isSelected;
|
24
|
+
|
25
|
+
const addedStyle = currentSelected
|
26
|
+
? "border-primary bg-ring"
|
27
|
+
: `bg-white ${hoverEffect ? 'hover:bg-gray-10 hover:shadow-sm transition duration-50' : ''} drop-shadow-xs md:drop-shadow-xs`;
|
28
|
+
|
29
|
+
const handleCardClick = (e: React.MouseEvent) => {
|
30
|
+
// Don't trigger card selection if noAction is true
|
31
|
+
if (noAction) return;
|
32
|
+
|
33
|
+
// Check if the clicked element is a checkbox, radio, or their label
|
34
|
+
const target = e.target as HTMLElement;
|
35
|
+
if (
|
36
|
+
target.tagName === 'INPUT' ||
|
37
|
+
target.classList.contains('checkbox') ||
|
38
|
+
target.classList.contains('radio') ||
|
39
|
+
target.classList.contains('form-control')
|
40
|
+
) {
|
41
|
+
// Let the native checkbox/radio handle the click
|
42
|
+
return;
|
43
|
+
}
|
44
|
+
|
45
|
+
if (onSelect) {
|
46
|
+
// If parent is controlling selection
|
47
|
+
onSelect(!selected);
|
48
|
+
} else {
|
49
|
+
// Internal state management
|
50
|
+
setIsSelected(!isSelected);
|
51
|
+
}
|
52
|
+
};
|
53
|
+
|
54
|
+
return (
|
55
|
+
<div
|
56
|
+
onClick={handleCardClick}
|
57
|
+
className={`flex items-center gap-4 shadow-xs px-3.5 py-3 w-full rounded-lg ${!noAction ? 'cursor-pointer' : ''} border-1 border-gray-100 ${addedStyle} ${className}`}
|
58
|
+
>
|
59
|
+
{children}
|
60
|
+
</div>
|
61
|
+
)
|
62
|
+
}
|
63
|
+
|
64
|
+
export default WrapperCard
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import React from 'react'
|
2
|
+
import logo from '@/assets/brand/saafe-logo.svg'
|
3
|
+
import { useTrustedCount } from '@/hooks/use-trusted-count'
|
4
|
+
|
5
|
+
const DummyFooter = ({ show = false }: { show?: boolean }) => {
|
6
|
+
const { data: trustedCount, isLoading } = useTrustedCount();
|
7
|
+
|
8
|
+
return (
|
9
|
+
<div>
|
10
|
+
{
|
11
|
+
show ? (
|
12
|
+
<div className='mt-16'>
|
13
|
+
<img src={logo} className='w-48 h-auto filter grayscale opacity-30' />
|
14
|
+
{!isLoading && trustedCount?.displayText && (
|
15
|
+
<h1 className='text-4xl font-semibold text-gray-300 mt-8 dark:text-muted-secondary'>
|
16
|
+
{trustedCount.displayText}
|
17
|
+
</h1>
|
18
|
+
)}
|
19
|
+
</div>
|
20
|
+
|
21
|
+
) : (
|
22
|
+
<div className='mt-16 w-full h-[140px]' />
|
23
|
+
)
|
24
|
+
}
|
25
|
+
</div>
|
26
|
+
)
|
27
|
+
}
|
28
|
+
|
29
|
+
export default DummyFooter
|
@@ -0,0 +1,12 @@
|
|
1
|
+
import { cn } from '@/lib/utils'
|
2
|
+
|
3
|
+
export default function GitHub({ className, ...props }: React.HTMLAttributes<SVGElement>) {
|
4
|
+
return (
|
5
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
6
|
+
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
|
7
|
+
className={cn("icon icon-tabler icons-tabler-outline icon-tabler-brand-github", className)} {...props}>
|
8
|
+
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
9
|
+
<path d="M9 19c-4.3 1.4 -4.3 -2.5 -6 -3m12 5v-3.5c0 -1 .1 -1.4 -.5 -2c2.8 -.3 5.5 -1.4 5.5 -6a4.6 4.6 0 0 0 -1.3 -3.2a4.2 4.2 0 0 0 -.1 -3.2s-1.1 -.3 -3.5 1.3a12.3 12.3 0 0 0 -6.2 0c-2.4 -1.6 -3.5 -1.3 -3.5 -1.3a4.2 4.2 0 0 0 -.1 3.2a4.6 4.6 0 0 0 -1.3 3.2c0 4.6 2.7 5.7 5.5 6c-.6 .6 -.6 1.2 -.5 2v3.5" />
|
10
|
+
</svg>
|
11
|
+
)
|
12
|
+
}
|
@@ -0,0 +1,44 @@
|
|
1
|
+
import { useLanguage } from '@/contexts/LanguageContext';
|
2
|
+
import { useRTL } from '@/contexts/RTLContext';
|
3
|
+
import {
|
4
|
+
DropdownMenu,
|
5
|
+
DropdownMenuContent,
|
6
|
+
DropdownMenuItem,
|
7
|
+
DropdownMenuTrigger,
|
8
|
+
} from '@/components/ui/dropdown-menu';
|
9
|
+
import { Button } from '@/components/ui/button';
|
10
|
+
import { ChevronDown, Globe } from 'lucide-react';
|
11
|
+
|
12
|
+
export function LanguageSwitcher() {
|
13
|
+
const { currentLanguage, changeLanguage, languageOptions } = useLanguage();
|
14
|
+
const { isRTL } = useRTL();
|
15
|
+
|
16
|
+
// Find current language display name
|
17
|
+
const currentLanguageOption = languageOptions.find(option => option.code === currentLanguage);
|
18
|
+
const currentDisplayName = currentLanguageOption ? currentLanguageOption.name : 'English';
|
19
|
+
|
20
|
+
return (
|
21
|
+
<div className={`language-switcher ${isRTL ? 'rtl-language' : ''}`}>
|
22
|
+
<DropdownMenu>
|
23
|
+
<DropdownMenuTrigger asChild>
|
24
|
+
<Button variant="ghost" className="flex items-center gap-2 px-3 py-1.5 h-auto text-white">
|
25
|
+
<Globe className="h-4 w-4 no-flip" />
|
26
|
+
<span className="hidden sm:inline-block">{currentDisplayName}</span>
|
27
|
+
<ChevronDown className="h-4 w-4" />
|
28
|
+
</Button>
|
29
|
+
</DropdownMenuTrigger>
|
30
|
+
<DropdownMenuContent align={isRTL ? "end" : "start"} className="w-40">
|
31
|
+
{languageOptions.map((option) => (
|
32
|
+
<DropdownMenuItem
|
33
|
+
key={option.code}
|
34
|
+
onClick={() => changeLanguage(option.code)}
|
35
|
+
className={`${currentLanguage === option.code ? 'bg-muted' : ''} ${option.code === 'ur' ? 'text-right w-full' : ''}`}
|
36
|
+
>
|
37
|
+
{option.name}
|
38
|
+
</DropdownMenuItem>
|
39
|
+
))}
|
40
|
+
</DropdownMenuContent>
|
41
|
+
</DropdownMenu>
|
42
|
+
</div>
|
43
|
+
);
|
44
|
+
}
|
@@ -0,0 +1,42 @@
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
2
|
+
import { FrostedLayout } from "./FrostedLayout";
|
3
|
+
|
4
|
+
const meta: Meta<typeof FrostedLayout> = {
|
5
|
+
title: "Layouts/FrostedLayout",
|
6
|
+
component: FrostedLayout,
|
7
|
+
parameters: {
|
8
|
+
layout: "fullscreen",
|
9
|
+
},
|
10
|
+
tags: ["autodocs"],
|
11
|
+
};
|
12
|
+
|
13
|
+
export default meta;
|
14
|
+
type Story = StoryObj<typeof FrostedLayout>;
|
15
|
+
|
16
|
+
export const Default: Story = {
|
17
|
+
args: {
|
18
|
+
children: <div className="p-4">Main Content</div>,
|
19
|
+
},
|
20
|
+
};
|
21
|
+
|
22
|
+
export const WithFooter: Story = {
|
23
|
+
args: {
|
24
|
+
children: <div className="p-4">Main Content</div>,
|
25
|
+
footer: <div className="p-4 bg-gray-100">Footer Content</div>,
|
26
|
+
},
|
27
|
+
};
|
28
|
+
|
29
|
+
export const WithLongContent: Story = {
|
30
|
+
args: {
|
31
|
+
children: (
|
32
|
+
<div className="p-4">
|
33
|
+
{Array.from({ length: 20 }).map((_, i) => (
|
34
|
+
<div key={i} className="mb-4 p-4 bg-white rounded shadow">
|
35
|
+
Content Block {i + 1}
|
36
|
+
</div>
|
37
|
+
))}
|
38
|
+
</div>
|
39
|
+
),
|
40
|
+
footer: <div className="p-4 bg-gray-100">Footer Content</div>,
|
41
|
+
},
|
42
|
+
};
|