saafe-redirection-flow 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/.github/workflows/build-and-deploy.yml +41 -0
  2. package/.gitlab-ci.yml +108 -0
  3. package/.releaserc.json +18 -0
  4. package/.storybook/main.ts +28 -0
  5. package/.storybook/preview.ts +16 -0
  6. package/.storybook/vitest.setup.ts +9 -0
  7. package/.vite/deps/@radix-ui_react-avatar.js +230 -0
  8. package/.vite/deps/@radix-ui_react-avatar.js.map +7 -0
  9. package/.vite/deps/@radix-ui_react-slot.js +12 -0
  10. package/.vite/deps/@radix-ui_react-slot.js.map +7 -0
  11. package/.vite/deps/_metadata.json +79 -0
  12. package/.vite/deps/chunk-5VGQBUCU.js +597 -0
  13. package/.vite/deps/chunk-5VGQBUCU.js.map +7 -0
  14. package/.vite/deps/chunk-DC5AMYBS.js +38 -0
  15. package/.vite/deps/chunk-DC5AMYBS.js.map +7 -0
  16. package/.vite/deps/chunk-HUIEPYH7.js +11265 -0
  17. package/.vite/deps/chunk-HUIEPYH7.js.map +7 -0
  18. package/.vite/deps/chunk-TKHB4QMX.js +281 -0
  19. package/.vite/deps/chunk-TKHB4QMX.js.map +7 -0
  20. package/.vite/deps/chunk-YLDSBLSF.js +1139 -0
  21. package/.vite/deps/chunk-YLDSBLSF.js.map +7 -0
  22. package/.vite/deps/class-variance-authority.js +63 -0
  23. package/.vite/deps/class-variance-authority.js.map +7 -0
  24. package/.vite/deps/lucide-react.js +36984 -0
  25. package/.vite/deps/lucide-react.js.map +7 -0
  26. package/.vite/deps/package.json +3 -0
  27. package/.vite/deps/react-dom_client.js +17917 -0
  28. package/.vite/deps/react-dom_client.js.map +7 -0
  29. package/.vite/deps/react-router-dom.js +452 -0
  30. package/.vite/deps/react-router-dom.js.map +7 -0
  31. package/.vite/deps/react-router.js +234 -0
  32. package/.vite/deps/react-router.js.map +7 -0
  33. package/.vite/deps/react.js +5 -0
  34. package/.vite/deps/react.js.map +7 -0
  35. package/.vite/deps/react_jsx-dev-runtime.js +470 -0
  36. package/.vite/deps/react_jsx-dev-runtime.js.map +7 -0
  37. package/CHANGELOG.md +420 -0
  38. package/LICENSE +21 -0
  39. package/README.md +129 -0
  40. package/RELEASE_CHEATSHEET.md +93 -0
  41. package/RELEASE_NOTES.md +120 -0
  42. package/components.json +21 -0
  43. package/docs/DEPLOYMENT_WORKFLOW.md +262 -0
  44. package/docs/RELEASE_GUIDE.md +591 -0
  45. package/docs/architecture.md +432 -0
  46. package/docs/components.md +199 -0
  47. package/docs/index.md +69 -0
  48. package/docs/local-release-workflow.md +234 -0
  49. package/docs/routes.md +118 -0
  50. package/docs/sdk-integration.md +325 -0
  51. package/docs/semantic-release.md +124 -0
  52. package/docs/user-flow.md +206 -0
  53. package/eslint.config.js +28 -0
  54. package/index.html +19 -0
  55. package/install.sh +198 -0
  56. package/package.json +115 -0
  57. package/public/images/bank-logo.png +0 -0
  58. package/public/saafe-icon.svg +9 -0
  59. package/src/App.tsx +171 -0
  60. package/src/__tests__/url-parameters.test.ts +82 -0
  61. package/src/assets/brand/applestore.svg +13 -0
  62. package/src/assets/brand/playstore.svg +23 -0
  63. package/src/assets/brand/saafe-color-white-logo.svg +14 -0
  64. package/src/assets/brand/saafe-icon.svg +9 -0
  65. package/src/assets/brand/saafe-logo.svg +18 -0
  66. package/src/assets/icons/check-icon-dark.svg +27 -0
  67. package/src/assets/icons/check-icon.svg +23 -0
  68. package/src/components/ErrorBoundary.tsx +132 -0
  69. package/src/components/alert/alert.tsx +27 -0
  70. package/src/components/auth/AuthGuard.tsx +76 -0
  71. package/src/components/cards/BankCard.stories.tsx +69 -0
  72. package/src/components/cards/BankCard.tsx +227 -0
  73. package/src/components/cards/OuterCard.tsx +109 -0
  74. package/src/components/cards/WrapperCard.tsx +64 -0
  75. package/src/components/documents/PrivacyContent.tsx +1 -0
  76. package/src/components/dummyFooter.tsx +29 -0
  77. package/src/components/icons/github.tsx +12 -0
  78. package/src/components/language/LanguageSwitcher.tsx +44 -0
  79. package/src/components/layouts/FrostedLayout.stories.tsx +42 -0
  80. package/src/components/layouts/FrostedLayout.tsx +333 -0
  81. package/src/components/layouts/MobileLayout.tsx +403 -0
  82. package/src/components/mobile-background.tsx +136 -0
  83. package/src/components/mobileAppDownload.tsx +30 -0
  84. package/src/components/modal/ModalComp.tsx +27 -0
  85. package/src/components/mode-toggle.tsx +36 -0
  86. package/src/components/page-header.tsx +50 -0
  87. package/src/components/session/SessionTimeoutScreen.tsx +134 -0
  88. package/src/components/session/SessionTimer.tsx +173 -0
  89. package/src/components/step-navigation.tsx +87 -0
  90. package/src/components/title/AppBar.stories.tsx +50 -0
  91. package/src/components/title/AppBar.tsx +150 -0
  92. package/src/components/title/SectionTitle.tsx +31 -0
  93. package/src/components/ui/AnimatedButton.module.css +13 -0
  94. package/src/components/ui/alert.tsx +66 -0
  95. package/src/components/ui/animatedButton.tsx +111 -0
  96. package/src/components/ui/avatar.tsx +51 -0
  97. package/src/components/ui/badge.tsx +36 -0
  98. package/src/components/ui/bottom-sheet.tsx +122 -0
  99. package/src/components/ui/button.tsx +59 -0
  100. package/src/components/ui/calendar.tsx +86 -0
  101. package/src/components/ui/card.tsx +92 -0
  102. package/src/components/ui/checkbox.stories.tsx +49 -0
  103. package/src/components/ui/checkbox.tsx +67 -0
  104. package/src/components/ui/collapsible.tsx +45 -0
  105. package/src/components/ui/dialog.tsx +134 -0
  106. package/src/components/ui/document-link.tsx +26 -0
  107. package/src/components/ui/dot-stepper.tsx +57 -0
  108. package/src/components/ui/dropdown-menu.tsx +255 -0
  109. package/src/components/ui/form.tsx +165 -0
  110. package/src/components/ui/frosted-panel.stories.tsx +86 -0
  111. package/src/components/ui/frosted-panel.tsx +276 -0
  112. package/src/components/ui/input.tsx +39 -0
  113. package/src/components/ui/label.stories.tsx +67 -0
  114. package/src/components/ui/label.tsx +23 -0
  115. package/src/components/ui/mobile-footer.tsx +54 -0
  116. package/src/components/ui/modal.tsx +90 -0
  117. package/src/components/ui/otp-input.stories.tsx +62 -0
  118. package/src/components/ui/otp-input.tsx +221 -0
  119. package/src/components/ui/platform-specific-behavior.tsx +28 -0
  120. package/src/components/ui/popover.tsx +46 -0
  121. package/src/components/ui/progress.tsx +103 -0
  122. package/src/components/ui/radio-group.tsx +45 -0
  123. package/src/components/ui/scroll-area.tsx +56 -0
  124. package/src/components/ui/sdk-params-docs.tsx +53 -0
  125. package/src/components/ui/select.tsx +159 -0
  126. package/src/components/ui/separator.tsx +28 -0
  127. package/src/components/ui/sheet.tsx +137 -0
  128. package/src/components/ui/sidebar.tsx +724 -0
  129. package/src/components/ui/skeleton.stories.tsx +50 -0
  130. package/src/components/ui/skeleton.tsx +15 -0
  131. package/src/components/ui/sonner.tsx +23 -0
  132. package/src/components/ui/step.stories.tsx +132 -0
  133. package/src/components/ui/step.tsx +234 -0
  134. package/src/components/ui/stepper-progress.tsx +136 -0
  135. package/src/components/ui/stepper.tsx +259 -0
  136. package/src/components/ui/tabs.tsx +55 -0
  137. package/src/components/ui/tooltip.tsx +61 -0
  138. package/src/components/ui/url-decode-loader.tsx +36 -0
  139. package/src/components/ui/version-display.tsx +104 -0
  140. package/src/components/ui/web-footer.tsx +36 -0
  141. package/src/config/environments.ts +99 -0
  142. package/src/config/urls.ts +53 -0
  143. package/src/const/fiTypeCategoryMap.ts +19 -0
  144. package/src/contexts/LanguageContext.tsx +41 -0
  145. package/src/contexts/RTLContext.tsx +42 -0
  146. package/src/contexts/ThemeContext.tsx +93 -0
  147. package/src/hooks/use-account-discovery.ts +205 -0
  148. package/src/hooks/use-auth-query.ts +141 -0
  149. package/src/hooks/use-fip-query.ts +72 -0
  150. package/src/hooks/use-media-query.ts +32 -0
  151. package/src/hooks/use-mobile.ts +24 -0
  152. package/src/hooks/use-page-title.tsx +48 -0
  153. package/src/hooks/use-platform.ts +52 -0
  154. package/src/hooks/use-trusted-count.ts +21 -0
  155. package/src/hooks/use-url-decode.ts +90 -0
  156. package/src/hooks/useStep.ts +170 -0
  157. package/src/index.css +154 -0
  158. package/src/interfaces/app.interfaces.ts +39 -0
  159. package/src/interfaces/services.interfaces.ts +65 -0
  160. package/src/lib/i18n.ts +68 -0
  161. package/src/lib/utils.ts +6 -0
  162. package/src/locales/en/common.json +167 -0
  163. package/src/locales/hi/common.json +137 -0
  164. package/src/locales/kn/common.json +137 -0
  165. package/src/locales/ml/common.json +137 -0
  166. package/src/locales/ta/common.json +137 -0
  167. package/src/locales/te/common.json +137 -0
  168. package/src/locales/ur/common.json +138 -0
  169. package/src/main.tsx +46 -0
  170. package/src/pages/Login.tsx +363 -0
  171. package/src/pages/accounts/AccountsToProceed.tsx +396 -0
  172. package/src/pages/accounts/Discover.tsx +76 -0
  173. package/src/pages/accounts/DiscoverAccount.tsx +751 -0
  174. package/src/pages/accounts/LinkSelectedAccounts.tsx +638 -0
  175. package/src/pages/accounts/OldUser.tsx +329 -0
  176. package/src/pages/accounts/link-accounts.tsx +913 -0
  177. package/src/pages/consent/ReviewConsent.tsx +836 -0
  178. package/src/pages/consent/rejected.tsx +253 -0
  179. package/src/pages/consent/success.tsx +220 -0
  180. package/src/providers/query-provider.tsx +24 -0
  181. package/src/providers/toast-provider.tsx +26 -0
  182. package/src/services/api/account.service.ts +296 -0
  183. package/src/services/api/auth.service.ts +206 -0
  184. package/src/services/api/axios.ts +138 -0
  185. package/src/services/api/consent.service.ts +142 -0
  186. package/src/services/api/decode.service.ts +53 -0
  187. package/src/services/api/feedback.service.ts +34 -0
  188. package/src/services/api/fip.service.ts +187 -0
  189. package/src/services/api/index.ts +9 -0
  190. package/src/services/api/public.service.ts +18 -0
  191. package/src/services/api.ts +2 -0
  192. package/src/services/postMessage.service.ts +179 -0
  193. package/src/store/NavigationBlockContext.tsx +34 -0
  194. package/src/store/auth.store.ts +79 -0
  195. package/src/store/fip.store.ts +396 -0
  196. package/src/store/mandatoryConsent.store.ts +24 -0
  197. package/src/store/redirect.store.ts +73 -0
  198. package/src/store/step.store.ts +124 -0
  199. package/src/stories/Button.stories.ts +53 -0
  200. package/src/stories/Button.tsx +37 -0
  201. package/src/stories/Configure.mdx +364 -0
  202. package/src/stories/Header.stories.ts +33 -0
  203. package/src/stories/Header.tsx +56 -0
  204. package/src/stories/Page.stories.ts +32 -0
  205. package/src/stories/Page.tsx +73 -0
  206. package/src/stories/button.css +30 -0
  207. package/src/stories/header.css +32 -0
  208. package/src/stories/page.css +68 -0
  209. package/src/styles/rtl-utils.css +90 -0
  210. package/src/styles/rtl.css +105 -0
  211. package/src/utils/api-error.ts +26 -0
  212. package/src/utils/cn.ts +10 -0
  213. package/src/utils/error-callback.ts +116 -0
  214. package/src/utils/formatAccountNumber.ts +9 -0
  215. package/src/utils/handleIdentifiers.ts +90 -0
  216. package/src/utils/posthog.ts +67 -0
  217. package/src/utils/toast-helpers.ts +61 -0
  218. package/src/vite-env.d.ts +1 -0
  219. package/stage-aa-2506251021.zip +0 -0
  220. package/tsconfig.app.json +33 -0
  221. package/tsconfig.json +13 -0
  222. package/tsconfig.node.json +24 -0
  223. package/vite.config.ts +45 -0
  224. package/vitest.shims.d.ts +1 -0
  225. package/vitest.workspace.ts +46 -0
@@ -0,0 +1,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,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
+ };