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,134 @@
1
+ import { useState, useEffect, useRef } from "react";
2
+ import { useNavigate } from "react-router-dom";
3
+ import { useRedirectStore } from "@/store/redirect.store";
4
+ import { XCircle } from 'lucide-react';
5
+ import logo from "../../assets/brand/saafe-logo.svg";
6
+ import { motion } from "framer-motion";
7
+
8
+ export const SessionTimeoutScreen = () => {
9
+ const navigate = useNavigate();
10
+ const { decodedInfo } = useRedirectStore();
11
+ const [countdown, setCountdown] = useState(10);
12
+ const containerRef = useRef<HTMLDivElement>(null);
13
+
14
+ useEffect(() => {
15
+ // Prevent scrolling when this screen is shown
16
+ document.body.style.overflow = 'hidden';
17
+
18
+ // Prevent back navigation
19
+ const preventNavigation = (e: PopStateEvent) => {
20
+ // Push another state to prevent going back
21
+ window.history.pushState(null, '', window.location.href);
22
+ e.preventDefault();
23
+ e.stopPropagation();
24
+ };
25
+
26
+ // Handle keyboard shortcuts
27
+ const handleKeyDown = (e: KeyboardEvent) => {
28
+ // Prevent navigation keys, refresh, etc.
29
+ if (
30
+ e.key === 'Escape' ||
31
+ e.key === 'Backspace' ||
32
+ (e.key === 'r' && (e.ctrlKey || e.metaKey)) ||
33
+ (e.key === 'w' && (e.ctrlKey || e.metaKey)) ||
34
+ (e.key === 'F5')
35
+ ) {
36
+ e.preventDefault();
37
+ e.stopPropagation();
38
+ return false;
39
+ }
40
+ };
41
+
42
+ // Focus trap - keep focus inside this component
43
+ const handleFocus = () => {
44
+ if (containerRef.current && !containerRef.current.contains(document.activeElement)) {
45
+ containerRef.current.focus();
46
+ }
47
+ };
48
+
49
+ // Block context menu
50
+ const blockContextMenu = (e: MouseEvent) => {
51
+ e.preventDefault();
52
+ return false;
53
+ };
54
+
55
+ // Push current state to history so we can intercept back button
56
+ window.history.pushState(null, '', window.location.href);
57
+
58
+ // Add event listeners
59
+ window.addEventListener('popstate', preventNavigation);
60
+ window.addEventListener('keydown', handleKeyDown, { capture: true });
61
+ document.addEventListener('focus', handleFocus, { capture: true });
62
+ document.addEventListener('contextmenu', blockContextMenu);
63
+
64
+ // Redirect after countdown reaches 0
65
+ if (countdown <= 0) {
66
+ // Check if there's a redirect URL (might be undefined/null)
67
+ const redirectUrl = decodedInfo?.redirect;
68
+ if (redirectUrl) {
69
+ window.location.href = redirectUrl;
70
+ }
71
+ return;
72
+ }
73
+
74
+ const timer = setTimeout(() => {
75
+ setCountdown((prev) => prev - 1);
76
+ }, 1000);
77
+
78
+ return () => {
79
+ clearTimeout(timer);
80
+ document.body.style.overflow = ''; // Re-enable scrolling if component unmounts
81
+
82
+ // Remove event listeners
83
+ window.removeEventListener('popstate', preventNavigation);
84
+ window.removeEventListener('keydown', handleKeyDown, { capture: true });
85
+ document.removeEventListener('focus', handleFocus, { capture: true });
86
+ document.removeEventListener('contextmenu', blockContextMenu);
87
+ };
88
+ }, [countdown, decodedInfo, navigate]);
89
+
90
+ return (
91
+ <div
92
+ ref={containerRef}
93
+ className="fixed inset-0 w-full h-full bg-white z-50 dark:bg-background"
94
+ tabIndex={-1} // Make div focusable
95
+ onContextMenu={(e) => e.preventDefault()} // Extra measure to prevent context menu
96
+ >
97
+ <div className="w-full h-full flex flex-col items-center justify-center">
98
+ {decodedInfo?.redirect && (
99
+ <p className="text-center mb-8 absolute top-8 text-muted-secondary dark:text-muted-foreground">
100
+ Redirecting in <span className="font-semibold">{countdown < 10 ? `0${countdown}` : countdown}</span>
101
+ </p>
102
+ )}
103
+ <motion.div
104
+ initial={{ y: 20, opacity: 0 }}
105
+ animate={{ y: 0, opacity: 1 }}
106
+ transition={{ delay: 0.2, duration: 0.5 }}
107
+ className="flex flex-col items-center dark:text-foreground"
108
+ >
109
+ <div className="rounded-full bg-amber-100 p-6 mb-8">
110
+ <XCircle className="h-16 w-16 text-amber-600" />
111
+ </div>
112
+
113
+ <h1 className="text-3xl font-semibold mb-4">Session Timed Out</h1>
114
+
115
+ {decodedInfo?.redirect ? (
116
+ <p className="text-center mb-8 text-muted-secondary dark:text-muted-foreground">
117
+ Please wait while we redirect you to {decodedInfo?.fiuName || "your bank's website"}.
118
+ </p>
119
+ ) : (
120
+ <p className="text-center mb-8 text-muted-secondary dark:text-muted-foreground">
121
+ Please close the window.
122
+ </p>
123
+ )}
124
+ </motion.div>
125
+ <div className="mt-16 text-center absolute bottom-8">
126
+ <div className="text-sm flex items-center justify-center gap-2 opacity-70">
127
+ <p>Powered by</p>
128
+ <img src={logo} alt="Saafe Logo" className="w-16 h-auto" />
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ );
134
+ };
@@ -0,0 +1,173 @@
1
+ import { useState, useEffect } from "react";
2
+ import { useTranslation } from "react-i18next";
3
+ import { Button } from "../ui/button";
4
+ import { DialogFooter } from "../ui/dialog";
5
+ import { DialogContent } from "../ui/dialog";
6
+ import { Dialog } from "../ui/dialog";
7
+ import { CircleHelp, Clock, Clock4 } from "lucide-react";
8
+ import { useNavigationBlock } from "@/store/NavigationBlockContext";
9
+ import { SESSION } from "@/config/urls";
10
+ import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
11
+
12
+ export const SessionTimer = ({ onTimeout, mobile = false }: { onTimeout: () => void, mobile?: boolean }) => {
13
+ const [timeLeft, setTimeLeft] = useState<number>(30 * 1000); // Default to 30 seconds initially
14
+ const { t } = useTranslation();
15
+ const [showWarningModal, setShowWarningModal] = useState(false);
16
+ const warningThreshold = 5 * 60 * 1000; // 5 minutes in milliseconds
17
+ const SESSION_TIMER_KEY = 'sessionTimer';
18
+ const { allowNextNavigation } = useNavigationBlock();
19
+
20
+ // Get session timeout from env or use default (30 seconds)
21
+ useEffect(() => {
22
+ try {
23
+ // Check if there's a saved timer state
24
+ const savedTimer = sessionStorage.getItem(SESSION_TIMER_KEY);
25
+ if (savedTimer) {
26
+ const { endTime } = JSON.parse(savedTimer);
27
+ const remainingTime = endTime - Date.now();
28
+ if (remainingTime > 0) {
29
+ setTimeLeft(remainingTime);
30
+ return;
31
+ } else {
32
+ // Clear expired session data
33
+ sessionStorage.clear();
34
+ }
35
+ }
36
+
37
+ // If no saved state or expired, set new timer
38
+ const sessionTimeoutSec = SESSION.TIMEOUT ? parseInt(SESSION.TIMEOUT) : 30;
39
+ const sessionTimeout = sessionTimeoutSec * 1000;
40
+ setTimeLeft(sessionTimeout);
41
+
42
+ // Save the end time in session storage
43
+ const endTime = Date.now() + sessionTimeout;
44
+ sessionStorage.setItem(SESSION_TIMER_KEY, JSON.stringify({ endTime }));
45
+ } catch (error) {
46
+ setTimeLeft(30 * 1000);
47
+ }
48
+ }, []);
49
+
50
+ // Timer logic
51
+ useEffect(() => {
52
+ if (timeLeft <= 0) {
53
+ // Clear session storage when timer expires
54
+ allowNextNavigation();
55
+ sessionStorage.clear();
56
+ onTimeout();
57
+ return;
58
+ }
59
+
60
+ // Show warning modal when 5 minutes remaining or less (if we just crossed the threshold)
61
+ if (
62
+ timeLeft <= warningThreshold &&
63
+ timeLeft > warningThreshold - 1000 &&
64
+ !showWarningModal
65
+ ) {
66
+ setShowWarningModal(true);
67
+ }
68
+
69
+ const timer = setTimeout(() => {
70
+ setTimeLeft((prevTime) => prevTime - 1000);
71
+ }, 1000);
72
+
73
+ return () => clearTimeout(timer);
74
+ }, [timeLeft, onTimeout, showWarningModal, warningThreshold, allowNextNavigation]);
75
+
76
+ // Format time as mm:ss
77
+ const formatTime = (ms: number) => {
78
+ const totalSeconds = Math.floor(ms / 1000);
79
+ const hours = Math.floor(totalSeconds / 3600);
80
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
81
+ const seconds = totalSeconds % 60;
82
+
83
+ return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
84
+ };
85
+
86
+ // Extend session by 30 minutes
87
+ const extendSession = () => {
88
+ const newTimeLeft = 30 * 60 * 1000; // 30 minutes
89
+ setTimeLeft(newTimeLeft);
90
+ // Update session storage with new end time
91
+ const endTime = Date.now() + newTimeLeft;
92
+ sessionStorage.setItem(SESSION_TIMER_KEY, JSON.stringify({ endTime }));
93
+ setShowWarningModal(false);
94
+ };
95
+
96
+ // Continue with remaining 5 minutes
97
+ const continueSession = () => {
98
+ setShowWarningModal(false);
99
+ };
100
+
101
+ return (
102
+ <div className={mobile ? "text-consent-secondary w-full" : "text-white"}>
103
+ {mobile ? (
104
+ <div className="flex flex-col">
105
+ <div className="flex items-start gap-1 justify-items-start w-full">
106
+ <Clock4 size={12} className="mt-[1px]" />
107
+ <span className={`font-medium ${mobile ? "text-xs" : ""}`}>{formatTime(timeLeft)}</span>
108
+ </div>
109
+ <Popover>
110
+ <PopoverTrigger>
111
+ <div className="flex items-center gap-1 cursor-pointer">
112
+ <CircleHelp size={12} />
113
+ <span className="text-xs mt-0.5">
114
+ {t("session.help")}
115
+ </span>
116
+ </div>
117
+ </PopoverTrigger>
118
+ <PopoverContent className="w-68">
119
+ <div className="text-xs text-muted-secondary">
120
+ For any questions or concerns, please report it to our Customer Grievance Redressal Officer (<a href="https://saafe.in/contact/" target="_blank" rel="noopener noreferrer"><span className="text-primary cursor-pointer">https://saafe.in/contact/</span></a>) - AA
121
+ </div>
122
+ </PopoverContent>
123
+ </Popover>
124
+ </div>
125
+ ) : (
126
+ <div className="flex justify-between gap-1.5">
127
+ <div className="flex items-center gap-1.5">
128
+ <span className="font-light text-sm">{t("session.timeRemaining")}:</span>
129
+ <span className={`font-medium ${mobile ? "text-sm" : ""}`}>{formatTime(timeLeft)}</span>
130
+ </div>
131
+ <Popover>
132
+ <PopoverTrigger>
133
+ <div className="flex items-center gap-1 cursor-pointer">
134
+ <CircleHelp size={14} />
135
+ <span className="text-sm mt-0.5">
136
+ {t("session.help")}
137
+ </span>
138
+ </div>
139
+ </PopoverTrigger>
140
+ <PopoverContent className="w-68">
141
+ <div className="text-sm text-muted-secondary">
142
+ For any questions or concerns, please report it to our Customer Grievance Redressal Officer (<a href="https://saafe.in/contact/" target="_blank" rel="noopener noreferrer"><span className="text-primary cursor-pointer">https://saafe.in/contact/</span></a>) - AA
143
+ </div>
144
+ </PopoverContent>
145
+ </Popover>
146
+ </div>
147
+ )}
148
+
149
+ <Dialog open={showWarningModal} onOpenChange={setShowWarningModal}>
150
+ <DialogContent className="text-center">
151
+ <div className="flex flex-col items-center justify-center gap-2">
152
+ <div className="bg-amber-100 w-10 h-10 rounded-full flex items-center justify-center">
153
+ <Clock className="w-6 h-6 text-amber-800" />
154
+ </div>
155
+ </div>
156
+ <div className="flex flex-col gap-2 py-4">
157
+ <h1 className="text-[20px] md:text-2xl font-medium">Session Timeout Approaching</h1>
158
+ <p className="text-xs md:text-sm text-muted-secondary">
159
+ Your session will expire in 5 minutes. Would you like to extend it
160
+ by 30 minutes or finish now?
161
+ </p>
162
+ </div>
163
+ <DialogFooter className="flex-col md:flex-row items-center justify-center gap-2">
164
+ <Button onClick={extendSession} size="lg" className="w-full md:w-fit">Extend by 30 mins</Button>
165
+ <Button variant="ghost" onClick={continueSession} size="lg" className="w-full md:w-fit">
166
+ Continue for 5 mins
167
+ </Button>
168
+ </DialogFooter>
169
+ </DialogContent>
170
+ </Dialog>
171
+ </div>
172
+ );
173
+ };
@@ -0,0 +1,87 @@
1
+ import React from "react";
2
+ import { ChevronRight } from "lucide-react";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ export interface StepItem {
6
+ id: string;
7
+ title: string;
8
+ isActive: boolean;
9
+ isCompleted: boolean;
10
+ childSteps?: StepItem[];
11
+ }
12
+
13
+ interface StepNavigationProps {
14
+ steps: StepItem[];
15
+ onStepClick?: (stepId: string) => void;
16
+ onChildStepClick?: (parentId: string, childId: string) => void;
17
+ className?: string;
18
+ }
19
+
20
+ export const StepNavigation: React.FC<StepNavigationProps> = ({
21
+ steps,
22
+ onStepClick,
23
+ onChildStepClick,
24
+ className,
25
+ }) => {
26
+ return (
27
+ <div className={cn("flex flex-col gap-4", className)}>
28
+ {steps.map((step) => (
29
+ <div key={step.id} className="flex flex-col">
30
+ {/* Parent step */}
31
+ <button
32
+ className={cn(
33
+ "flex items-center gap-2 py-2 text-left focus:outline-none transition-colors",
34
+ step.isActive
35
+ ? "text-primary font-semibold"
36
+ : step.isCompleted
37
+ ? "text-muted-foreground"
38
+ : "text-muted-foreground/50",
39
+ onStepClick && "cursor-pointer hover:text-primary"
40
+ )}
41
+ onClick={() => onStepClick?.(step.id)}
42
+ disabled={!onStepClick}
43
+ >
44
+ <div
45
+ className={cn(
46
+ "w-6 h-6 rounded-full flex items-center justify-center text-xs",
47
+ step.isActive
48
+ ? "bg-primary text-white"
49
+ : step.isCompleted
50
+ ? "bg-primary/20 text-primary"
51
+ : "bg-muted text-muted-foreground/50"
52
+ )}
53
+ >
54
+ {step.isCompleted ? "✓" : steps.indexOf(step) + 1}
55
+ </div>
56
+ <span>{step.title}</span>
57
+ </button>
58
+
59
+ {/* Child steps if parent is active and has children */}
60
+ {step.isActive && step.childSteps && step.childSteps.length > 0 && (
61
+ <div className="ml-8 mt-2 flex flex-col gap-2">
62
+ {step.childSteps.map((childStep) => (
63
+ <button
64
+ key={childStep.id}
65
+ className={cn(
66
+ "flex items-center gap-2 py-1 text-left text-sm focus:outline-none transition-colors",
67
+ childStep.isActive
68
+ ? "text-primary font-medium"
69
+ : childStep.isCompleted
70
+ ? "text-muted-foreground"
71
+ : "text-muted-foreground/50",
72
+ onChildStepClick && "cursor-pointer hover:text-primary"
73
+ )}
74
+ onClick={() => onChildStepClick?.(step.id, childStep.id)}
75
+ disabled={!onChildStepClick}
76
+ >
77
+ <ChevronRight size={14} />
78
+ <span>{childStep.title}</span>
79
+ </button>
80
+ ))}
81
+ </div>
82
+ )}
83
+ </div>
84
+ ))}
85
+ </div>
86
+ );
87
+ };
@@ -0,0 +1,50 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import AppBar from "./AppBar";
3
+ import { Settings, Bell } from "lucide-react";
4
+
5
+ const meta: Meta<typeof AppBar> = {
6
+ title: "UI/AppBar",
7
+ component: AppBar,
8
+ parameters: {
9
+ layout: "padded",
10
+ },
11
+ tags: ["autodocs"],
12
+ };
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof AppBar>;
16
+
17
+ export const Default: Story = {
18
+ args: {
19
+ title: "Default AppBar",
20
+ },
21
+ };
22
+
23
+ export const WithCustomLeftSection: Story = {
24
+ args: {
25
+ title: "Custom Left Section",
26
+ leftSection: <Settings className="w-6 h-6 text-consent-primary" />,
27
+ },
28
+ };
29
+
30
+ export const WithCustomRightSection: Story = {
31
+ args: {
32
+ title: "Custom Right Section",
33
+ rightSection: <Bell className="w-6 h-6 text-consent-primary" />,
34
+ },
35
+ };
36
+
37
+ export const WithCustomActions: Story = {
38
+ args: {
39
+ title: "Custom Actions",
40
+ leftSectionAction: () => alert("Left section clicked"),
41
+ rightSectionAction: () => alert("Right section clicked"),
42
+ },
43
+ };
44
+
45
+ export const WithCustomClassName: Story = {
46
+ args: {
47
+ title: "Custom Styling",
48
+ className: "bg-gray-100 p-4 rounded-lg",
49
+ },
50
+ };
@@ -0,0 +1,150 @@
1
+ import { ChevronLeft, ChevronRight, Info } from 'lucide-react'
2
+ import { useLocation, useNavigate } from 'react-router-dom';
3
+ import { usePageTitle } from '@/hooks/use-page-title';
4
+ import { useMediaQuery } from '@/hooks/use-media-query';
5
+ import { cn } from '@/lib/utils';
6
+ import Modal from '../ui/modal';
7
+ import { Label } from '../ui/label';
8
+ import { AnimatedButton } from '../ui/animatedButton';
9
+ import { useFipStore } from '@/store/fip.store';
10
+ import { EVENTS, trackEvent } from '@/utils/posthog';
11
+ import { useNavigationBlock } from '@/store/NavigationBlockContext';
12
+ import { useRTL } from '@/contexts/RTLContext';
13
+ import { useTranslation } from 'react-i18next';
14
+ interface AppBarProps {
15
+ title?: string;
16
+ leftSection?: React.ReactNode;
17
+ leftSectionAction?: () => void;
18
+ rightSection?: React.ReactNode | boolean;
19
+ rightSectionAction?: () => void;
20
+ className?: string;
21
+ }
22
+
23
+ const AppBar = ({
24
+ title: propTitle,
25
+ leftSection,
26
+ leftSectionAction,
27
+ rightSection,
28
+ className = '',
29
+ }: AppBarProps) => {
30
+ const navigate = useNavigate();
31
+ const location = useLocation();
32
+ const [contextTitle] = usePageTitle();
33
+ const isMobile = useMediaQuery("(max-width: 768px)");
34
+ const {
35
+ completeCategory,
36
+ categories,
37
+ setActiveCategory,
38
+ activeCategory,
39
+ completedCategories,
40
+ } = useFipStore()
41
+ // Only navigate back if we have history within our app
42
+ const { allowNextNavigation } = useNavigationBlock();
43
+ const { isRTL } = useRTL();
44
+ const { t } = useTranslation();
45
+ // Use prop title if provided, otherwise use context title
46
+ const title = contextTitle || propTitle;
47
+
48
+ const handleLeftSectionAction = () => {
49
+ if (leftSectionAction) {
50
+ leftSectionAction();
51
+ } else {
52
+ allowNextNavigation(); // mark this as an allowed navigation
53
+ navigate(-1);
54
+ }
55
+ };
56
+
57
+ if (location.pathname?.split('/')?.includes('login')) {
58
+ return null;
59
+ }
60
+
61
+ const handleComplete = () => {
62
+ // Mark this category as complete
63
+ completeCategory(activeCategory || 'BANKS')
64
+ trackEvent(EVENTS.SKIP_LINKING, { activeCategory })
65
+
66
+ // Check if there are more categories to process
67
+ const currentIndex = categories.indexOf(activeCategory || 'BANKS')
68
+ if (currentIndex < categories.length - 1) {
69
+ // Move to the next category
70
+ const nextCategory = categories[currentIndex + 1]
71
+ setActiveCategory(nextCategory)
72
+
73
+ // Navigate directly to the next category path for proper step highlighting
74
+ navigate(`/link-accounts/${nextCategory.toLowerCase()}`, {
75
+ state: {
76
+ completedCategories: [...(completedCategories || []), activeCategory]
77
+ }
78
+ })
79
+ } else {
80
+ // All categories done, move to review consent
81
+ navigate('/review', {
82
+ state: {
83
+ // selectedFips: state?.selectedFips,
84
+ completedCategories: [...(completedCategories || []), activeCategory]
85
+ }
86
+ })
87
+ }
88
+ }
89
+
90
+ return (
91
+ <div className={cn(
92
+ "flex justify-between items-center w-full py-2",
93
+ // isMobile ? "px-4" : "px-6",
94
+ isRTL ? "flex-row-reverse" : "",
95
+ className
96
+ )}>
97
+ <div className="cursor-pointer" onClick={handleLeftSectionAction}>
98
+ {leftSection ? leftSection : <ChevronLeft className='text-consent-primary dark:text-gray-300' />}
99
+ </div>
100
+ <div>
101
+ <p className={cn(
102
+ "font-semibold",
103
+ isMobile ? "text-sm" : "text-lg",
104
+ "text-black text-center dark:text-gray-300"
105
+ )}>
106
+ {title}
107
+ </p>
108
+ </div>
109
+ <Modal className='w-[500px]'>
110
+ <Modal.Trigger>
111
+ <p className={cn(
112
+ isMobile ? "text-sm" : "text-base",
113
+ "text-consent-primary cursor-pointer"
114
+ )}>
115
+ {location.pathname === '/review' ? null : (
116
+ <div className='flex items-center gap-1 dark:text-gray-300'>
117
+ {rightSection ? rightSection : t('next')}
118
+ {!rightSection ? <ChevronRight className='text-consent-primary dark:text-gray-300' /> : null}
119
+ </div>
120
+ )}
121
+ </p>
122
+ </Modal.Trigger>
123
+ <div className='flex flex-col items-center gap-4'>
124
+ <div className='flex items-center justify-center bg-orange-100/50 dark:bg-orange-900/50 p-3 rounded-full w-fit'>
125
+ <Info className='h-[32px] w-[32px] text-yellow-600' />
126
+ </div>
127
+ {
128
+ location.pathname === '/link-accounts/link' ? <>
129
+ <Label className='text-[20px] md:text-2xl font-semibold dark:text-white'>{t('skipModal.title')}</Label>
130
+ <p className='text-sm md:text-md text-gray-500 dark:text-gray-400 text-center'>{t('skipModal.description')}</p>
131
+ </> : <>
132
+ <Label className='text-[20px] md:text-2xl font-semibold dark:text-white text-center'>{t('completeModal.title')}</Label>
133
+ <p className='text-sm md:text-md text-gray-500 dark:text-gray-400 text-center'>{t('completeModal.description').replace('{activeCategory}', activeCategory)}</p>
134
+ </>
135
+ }
136
+ </div>
137
+ <div className='flex justify-center gap-4 mt-6 flex-col md:flex-row'>
138
+ <Modal.Close>
139
+ <AnimatedButton size={'lg'} className='w-full'>{t('skipModal.cancel')}</AnimatedButton>
140
+ </Modal.Close>
141
+ <Modal.Close>
142
+ <AnimatedButton variant={'secondary'} size={'lg'} className='bg-orange-100/50 dark:bg-orange-900/50 text-yellow-600 hover:bg-orange-100/70 dark:hover:bg-orange-900/70 w-full' onClick={handleComplete}>{t('skipModal.confirm')}</AnimatedButton>
143
+ </Modal.Close>
144
+ </div>
145
+ </Modal>
146
+ </div>
147
+ )
148
+ }
149
+
150
+ export default AppBar
@@ -0,0 +1,31 @@
1
+ import React from 'react'
2
+
3
+ const SectionTitle = ({
4
+ title,
5
+ rightSection,
6
+ className = '',
7
+ description
8
+ }: {
9
+ title: string
10
+ rightSection?: React.ReactNode
11
+ className?: string
12
+ description?: string
13
+ }) => {
14
+ return (
15
+ <div className='w-full flex flex-col'>
16
+ <div
17
+ className={`flex items-center gap-4 w-full justify-between ${className}`}
18
+ >
19
+ <p className='text-sm md:text-lg font-[600] text-consent-primary dark:text-muted-foreground'>
20
+ {title}
21
+ </p>
22
+ <div>{rightSection ? rightSection : null}</div>
23
+ </div>
24
+ <div>
25
+ <p className='text-sm text-consent-secondary'>{description}</p>
26
+ </div>
27
+ </div>
28
+ )
29
+ }
30
+
31
+ export default SectionTitle
@@ -0,0 +1,13 @@
1
+ .loader {
2
+ border: 4px solid #f3f3f3;
3
+ border-top: 4px solid #3498db;
4
+ border-radius: 50%;
5
+ width: 24px;
6
+ height: 24px;
7
+ animation: spin 1s linear infinite;
8
+ }
9
+
10
+ @keyframes spin {
11
+ 0% { transform: rotate(0deg); }
12
+ 100% { transform: rotate(360deg); }
13
+ }