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,403 @@
1
+ "use client";
2
+
3
+ import React, { useState, useMemo, useEffect } from "react";
4
+ import { motion, AnimatePresence } from "framer-motion";
5
+ import { useLocation } from "react-router-dom";
6
+ import {
7
+ Shield,
8
+ Link,
9
+ FileText,
10
+ Building2,
11
+ Receipt,
12
+ ShieldCheck,
13
+ } from "lucide-react";
14
+ import { StepperProgress } from "@/components/ui/stepper-progress";
15
+ import { useStep } from "@/hooks/useStep";
16
+ import { MobileBackground } from "@/components/mobile-background";
17
+ import AppBar from "../title/AppBar";
18
+ import { LanguageSwitcher } from "@/components/language/LanguageSwitcher";
19
+ import { useTranslation } from "react-i18next";
20
+ import { useFipStore } from "@/store/fip.store";
21
+ import { SessionTimer } from "../session/SessionTimer";
22
+ import { SessionTimeoutScreen } from "../session/SessionTimeoutScreen";
23
+
24
+ // Define step interfaces
25
+ interface BaseStep {
26
+ step: number;
27
+ title: string;
28
+ description: string;
29
+ icon: React.ReactNode;
30
+ path: string;
31
+ parentStep?: number;
32
+ }
33
+
34
+ // Base steps without dynamic child steps
35
+ const baseSteps: BaseStep[] = [
36
+ {
37
+ step: 1,
38
+ title: "Login/Signup",
39
+ description: "Enter the OTP sent to your mobile",
40
+ icon: <Shield className="w-4 h-4" />,
41
+ path: "/login",
42
+ },
43
+ {
44
+ step: 2,
45
+ title: "Link Accounts",
46
+ description: "Select & Link the accounts you'd like to share",
47
+ icon: <Link className="w-4 h-4" />,
48
+ path: "/link-accounts/discover",
49
+ },
50
+ {
51
+ step: 3,
52
+ title: "Review Consent",
53
+ description: "Approve/Reject the consent after reviewing the details",
54
+ icon: <FileText className="w-4 h-4" />,
55
+ path: "/review",
56
+ },
57
+ ];
58
+
59
+ // Map of category icons
60
+ const categoryIcons = {
61
+ "BANK": <Building2 className="w-4 h-4" />,
62
+ "GST": <Receipt className="w-4 h-4" />,
63
+ "INSURANCE": <ShieldCheck className="w-4 h-4" />,
64
+ "INVESTMENTS": <ShieldCheck className="w-4 h-4" />,
65
+ };
66
+
67
+ // Filter steps to only include parent steps (those without parentStep property)
68
+ const getParentSteps = (steps: BaseStep[]) =>
69
+ steps.filter(step => !step.parentStep).map(step => ({
70
+ ...step,
71
+ }));
72
+
73
+ // Mapping of parent steps to their child steps for tracking progress
74
+ const getParentChildMapping = (steps: BaseStep[]) => {
75
+ const mapping: Record<number, number[]> = {};
76
+
77
+ steps.forEach(step => {
78
+ if (step.parentStep) {
79
+ if (!mapping[step.parentStep]) {
80
+ mapping[step.parentStep] = [];
81
+ }
82
+ mapping[step.parentStep].push(step.step);
83
+ }
84
+ });
85
+
86
+ return mapping;
87
+ };
88
+
89
+ interface MobileLayoutProps {
90
+ children: React.ReactNode;
91
+ title?: string;
92
+ }
93
+
94
+ export function MobileLayout({ children, title }: MobileLayoutProps) {
95
+ const { t } = useTranslation();
96
+ const location = useLocation();
97
+ const isLoginPage = location.pathname === "/login";
98
+ const currentPath = location.pathname;
99
+ const [steps, setSteps] = useState<BaseStep[]>(baseSteps);
100
+ const { categories, activeCategory } = useFipStore();
101
+ const [isSessionExpired, setIsSessionExpired] = useState(false);
102
+ const [showSessionTimer, setShowSessionTimer] = useState(true);
103
+
104
+ // Handle session timeout
105
+ const handleTimeout = () => {
106
+ setIsSessionExpired(true);
107
+ };
108
+
109
+ // Dynamically create steps whenever categories change
110
+ useEffect(() => {
111
+ if (categories && categories.length > 0) {
112
+ // Create child steps for Link Accounts
113
+ const childSteps = categories.map((category, index) => ({
114
+ step: 3 + index, // Start numbering after Link Accounts
115
+ title: t(`categories.${category.toLowerCase()}`),
116
+ description: t(`categories.${category.toLowerCase()}Description`),
117
+ icon: categoryIcons[category as keyof typeof categoryIcons] || <Link className="w-4 h-4" />,
118
+ path: `/link-accounts/${category.toLowerCase()}`,
119
+ parentStep: 2, // Link Accounts is step 2,
120
+ isActive: category === activeCategory
121
+ }));
122
+
123
+ // Combine base steps with child steps
124
+ const newSteps = [
125
+ ...baseSteps.slice(0, 2), // Login and Link Accounts
126
+ ...childSteps,
127
+ baseSteps[2], // Review Consent, update the step number
128
+ ];
129
+
130
+ // Update Review Consent step number
131
+ newSteps[newSteps.length - 1] = {
132
+ ...newSteps[newSteps.length - 1],
133
+ step: 3 + childSteps.length
134
+ };
135
+
136
+ setSteps(newSteps);
137
+ } else {
138
+ setSteps(baseSteps);
139
+ }
140
+ }, [categories, activeCategory, t]);
141
+
142
+ // Use the translated parent steps
143
+ const parentSteps = useMemo(() => getParentSteps(steps, t), [steps, t]);
144
+ const parentChildMapping = useMemo(() => getParentChildMapping(steps), [steps]);
145
+
146
+ // Determine the current active step
147
+ const determineActiveStep = () => {
148
+ // First check if we're on an exact path match
149
+ const exactPathMatch = steps.findIndex((step) => step.path === location.pathname);
150
+ if (exactPathMatch >= 0) return exactPathMatch + 1;
151
+
152
+ // Check if we're on a path under link-accounts
153
+ if (location.pathname.includes('/link-accounts/')) {
154
+ const pathSegment = location.pathname.split('/').pop() || '';
155
+
156
+ // Flow paths are all part of the current category step
157
+ const flowPaths = ['discovery', 'link', 'discover-account'];
158
+ if (flowPaths.includes(pathSegment)) {
159
+ // Find which category step is active
160
+ if (activeCategory) {
161
+ // Find the child step matching the active category
162
+ const activeChildStep = steps.findIndex(step => {
163
+ // Look for parent steps that have child steps matching the active category
164
+ const matchesCategory = step.title?.toLowerCase() === activeCategory.toLowerCase();
165
+ const isChildStep = Boolean(step.parentStep);
166
+ return matchesCategory && isChildStep;
167
+ });
168
+
169
+ if (activeChildStep >= 0) {
170
+ return activeChildStep + 1;
171
+ }
172
+ }
173
+
174
+ // Default to Link Accounts if no category match
175
+ return 2;
176
+ }
177
+
178
+ // Check if path matches a category directly - based on the URL path
179
+ const categorySegment = pathSegment.toLowerCase();
180
+ for (let i = 0; i < steps.length; i++) {
181
+ const step = steps[i];
182
+ // Skip non-child steps
183
+ if (!step.parentStep) continue;
184
+
185
+ // Check if this step's title matches the path segment
186
+ if (step.title?.toLowerCase() === categorySegment) {
187
+ return i + 1;
188
+ }
189
+ }
190
+
191
+ // Default to Link Accounts step
192
+ return 2;
193
+ }
194
+
195
+ // We're not on a path in the steps
196
+ return 1; // Default to first step
197
+ };
198
+
199
+ const currentStep = determineActiveStep();
200
+
201
+ // Calculate current parent step and track child progress
202
+ const { activeStep, completedSteps, goToStep } = useStep({
203
+ totalSteps: steps.length,
204
+ initialStep: currentStep > 0 ? currentStep : 1,
205
+ onStepComplete: (step) => {
206
+ // Step completed callback
207
+ },
208
+ });
209
+
210
+ // Find current step based on path
211
+ const currentStepData = steps.find((step) => step.path === currentPath);
212
+
213
+ // Generate title from current step if not provided
214
+ const appTitle = title ||
215
+ t("keywords.select") + " " +
216
+ `${currentStepData?.title || t("categories.bank")}` +
217
+ " & " +
218
+ t("keywords.discover") + " " +
219
+ t("keywords.your") + " " +
220
+ t("keywords.accounts");
221
+
222
+ // Calculate child progress for parent steps
223
+ const childProgress = useMemo(() => {
224
+ const progress: Record<number, number> = {};
225
+
226
+ Object.entries(parentChildMapping).forEach(([parentStepStr, childSteps]) => {
227
+ const parentStep = parseInt(parentStepStr);
228
+ const completedChildSteps = childSteps.filter(step =>
229
+ completedSteps.includes(step) || step === activeStep
230
+ ).length;
231
+
232
+ // Only track progress if at least one child step is active/completed
233
+ if (completedChildSteps > 0) {
234
+ progress[parentStep] = completedChildSteps;
235
+ }
236
+ });
237
+
238
+ return progress;
239
+ }, [completedSteps, activeStep, parentChildMapping]);
240
+
241
+ // Calculate max possible progress for each parent step
242
+ const maxChildProgress = useMemo(() => {
243
+ const maxProgress: Record<number, number> = {};
244
+
245
+ Object.entries(parentChildMapping).forEach(([parentStepStr, childSteps]) => {
246
+ const parentStep = parseInt(parentStepStr);
247
+ maxProgress[parentStep] = childSteps.length;
248
+ });
249
+
250
+ return maxProgress;
251
+ }, [parentChildMapping]);
252
+
253
+ // For the progress bar, calculate the current parent step
254
+ const currentParentStep = useMemo(() => {
255
+ const index = parentSteps.findIndex((step) => {
256
+ // If we're on a child step, find its parent
257
+ if (currentStepData?.parentStep) {
258
+ return step.step === currentStepData.parentStep;
259
+ }
260
+ // Otherwise, check if we're on this parent step
261
+ return step.step === currentStep;
262
+ });
263
+ return index >= 0 ? index + 1 : 1;
264
+ }, [parentSteps, currentStepData, currentStep]);
265
+
266
+ // Return session timeout screen if session expired
267
+ if (isSessionExpired) {
268
+ return <SessionTimeoutScreen />;
269
+ }
270
+
271
+ return (
272
+ <div className="flex flex-col min-h-screen bg-background">
273
+ {/* Special background for login page */}
274
+ {isLoginPage ? (
275
+ <div>
276
+ <MobileBackground>
277
+ <div className="p-6">
278
+ {/* Progress indicator */}
279
+ <div className="mb-6">
280
+ <div className="mb-4">
281
+ <LanguageSwitcher />
282
+ </div>
283
+ <StepperProgress
284
+ steps={parentSteps.length}
285
+ currentStep={currentParentStep || 1}
286
+ childProgress={childProgress}
287
+ maxChildProgress={maxChildProgress}
288
+ className="mb-4"
289
+ />
290
+ </div>
291
+
292
+ {/* Login content with animated entrance */}
293
+ <motion.div
294
+ initial={{ y: 20, opacity: 0 }}
295
+ animate={{ y: 0, opacity: 1 }}
296
+ transition={{ delay: 0.2, duration: 0.5 }}
297
+ className="text-white"
298
+ >
299
+ <div className="mb-6">
300
+ <h2 className="text-xl font-medium mb-4">
301
+ {t('layout.title')}
302
+ </h2>
303
+
304
+ <div>
305
+ {steps.map((step) => (
306
+ <motion.div
307
+ key={step.step}
308
+ initial={{ x: -10, opacity: 0 }}
309
+ animate={{ x: 0, opacity: 1 }}
310
+ transition={{
311
+ delay: step.parentStep ? 0.1 * (step.step % categories.length) : 0.1 * step.step,
312
+ duration: 0.4
313
+ }}
314
+ className="flex items-center gap-4 p-3 rounded-lg py-6"
315
+ >
316
+ <div className="p-2 bg-white rounded-full text-primary">
317
+ {step.icon}
318
+ </div>
319
+ <div>
320
+ <h3 className="font-medium text-white">
321
+ {step.parentStep
322
+ ? step.title
323
+ : t(`stepper.${step.title.toLowerCase().replace('/', '')}`)}
324
+ </h3>
325
+ <p className="text-xs text-white/70">
326
+ {step.parentStep
327
+ ? step.description
328
+ : t(`${step.description.toLowerCase().replace('/', '')}`)}
329
+ </p>
330
+ </div>
331
+ </motion.div>
332
+ ))}
333
+ </div>
334
+ </div>
335
+ </motion.div>
336
+ </div>
337
+ </MobileBackground>
338
+ <div className="overflow-y-scroll h-full px-6 py-6">{children}</div>
339
+ </div>
340
+ ) : (
341
+ // Regular layout for non-login pages
342
+ <>
343
+ {/* Session Timer at the top */}
344
+ {showSessionTimer && !isLoginPage && (
345
+ <div className="px-4 py-2 flex justify-between items-center pt-5">
346
+ <div className="flex items-center gap-2 text-gray-600 w-full">
347
+ {/* <span className="text-sm font-medium">Session timeout:</span> */}
348
+ <SessionTimer onTimeout={handleTimeout} mobile={true} />
349
+ </div>
350
+ </div>
351
+ )}
352
+
353
+ {/* Header with progress bar */}
354
+ <motion.div
355
+ initial={{ y: -10, opacity: 0 }}
356
+ animate={{ y: 0, opacity: 1 }}
357
+ className="px-4 py-4 bg-background"
358
+ >
359
+ <div className="flex items-center justify-between">
360
+ <AppBar title={title || appTitle} />
361
+ {/* <LanguageSwitcher /> */}
362
+ </div>
363
+ {/* Step Progress using the stepper with child progress tracking */}
364
+ <StepperProgress
365
+ steps={parentSteps.length}
366
+ currentStep={currentParentStep || 1}
367
+ childProgress={childProgress}
368
+ maxChildProgress={maxChildProgress}
369
+ className="mt-4"
370
+ onStepChange={(step) => {
371
+ // Only allow navigation to completed steps
372
+ const targetStep = parentSteps[step - 1];
373
+ if (
374
+ targetStep &&
375
+ (completedSteps.includes(targetStep.step) ||
376
+ targetStep.step === activeStep)
377
+ ) {
378
+ goToStep(targetStep.step);
379
+ }
380
+ }}
381
+ />
382
+ </motion.div>
383
+
384
+ {/* Main Content */}
385
+ <div className="flex-1 overflow-auto p-4">
386
+ <AnimatePresence mode="wait">
387
+ <motion.div
388
+ key={location.pathname}
389
+ initial={{ opacity: 0, y: 20 }}
390
+ animate={{ opacity: 1, y: 0 }}
391
+ exit={{ opacity: 0, y: -20 }}
392
+ transition={{ duration: 0.3 }}
393
+ className="flex-1"
394
+ >
395
+ {children}
396
+ </motion.div>
397
+ </AnimatePresence>
398
+ </div>
399
+ </>
400
+ )}
401
+ </div>
402
+ );
403
+ }
@@ -0,0 +1,136 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useRef } from "react";
4
+ import { cn } from "@/lib/utils";
5
+ import { useRedirectStore } from "@/store/redirect.store";
6
+
7
+ interface MobileBackgroundProps extends React.HTMLAttributes<HTMLDivElement> {
8
+ gradientColors?: {
9
+ top: string;
10
+ bottom: string;
11
+ };
12
+ blobConfig?: {
13
+ count: number;
14
+ color: string;
15
+ minSize: number;
16
+ maxSize: number;
17
+ speed: number;
18
+ };
19
+ }
20
+
21
+ export function MobileBackground({
22
+ children,
23
+ className,
24
+ gradientColors = {
25
+ top: "#004d4d",
26
+ bottom: "#00b2c1",
27
+ },
28
+ blobConfig = {
29
+ count: 3,
30
+ color: "rgba(255, 255, 255, 0.4)",
31
+ minSize: 60,
32
+ maxSize: 140,
33
+ speed: 0.9,
34
+ },
35
+ ...props
36
+ }: MobileBackgroundProps) {
37
+ const canvasRef = useRef<HTMLCanvasElement>(null);
38
+ const animationRef = useRef<number | null>(null);
39
+ const { getCustomStyle } = useRedirectStore();
40
+ const customStyles = getCustomStyle();
41
+ const blobsRef = useRef<Array<{
42
+ x: number;
43
+ y: number;
44
+ radius: number;
45
+ xSpeed: number;
46
+ ySpeed: number;
47
+ opacity: number;
48
+ }>>([]);
49
+
50
+ useEffect(() => {
51
+ const canvas = canvasRef.current;
52
+ if (!canvas) return;
53
+
54
+ const ctx = canvas.getContext("2d");
55
+ if (!ctx) return;
56
+
57
+ // Initialize blobs only once
58
+ if (blobsRef.current.length === 0) {
59
+ for (let i = 0; i < blobConfig.count; i++) {
60
+ blobsRef.current.push({
61
+ x: Math.random() * canvas.width,
62
+ y: Math.random() * canvas.height,
63
+ radius: blobConfig.minSize + Math.random() * (blobConfig.maxSize - blobConfig.minSize),
64
+ xSpeed: (Math.random() - 0.5) * blobConfig.speed,
65
+ ySpeed: (Math.random() - 0.5) * blobConfig.speed,
66
+ opacity: 0.4 + Math.random() * 0.4,
67
+ });
68
+ }
69
+ }
70
+
71
+ const resizeCanvas = () => {
72
+ const parent = canvas.parentElement;
73
+ if (!parent) return;
74
+ canvas.width = parent.offsetWidth;
75
+ canvas.height = parent.offsetHeight;
76
+ };
77
+
78
+ resizeCanvas();
79
+ window.addEventListener("resize", resizeCanvas);
80
+
81
+ const animate = () => {
82
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
83
+ blobsRef.current.forEach((blob) => {
84
+ blob.x += blob.xSpeed;
85
+ blob.y += blob.ySpeed;
86
+
87
+ if (blob.x < -blob.radius || blob.x > canvas.width + blob.radius) {
88
+ blob.xSpeed = -blob.xSpeed;
89
+ }
90
+ if (blob.y < -blob.radius || blob.y > canvas.height + blob.radius) {
91
+ blob.ySpeed = -blob.ySpeed;
92
+ }
93
+
94
+ ctx.beginPath();
95
+ ctx.arc(blob.x, blob.y, blob.radius, 0, Math.PI * 2);
96
+ ctx.fillStyle = blobConfig.color
97
+ .replace(")", `, ${blob.opacity})`)
98
+ .replace("rgba", "rgba")
99
+ .replace("rgb", "rgba");
100
+ ctx.fill();
101
+ });
102
+
103
+ animationRef.current = requestAnimationFrame(animate);
104
+ };
105
+
106
+ animate();
107
+
108
+ return () => {
109
+ window.removeEventListener("resize", resizeCanvas);
110
+ if (animationRef.current) {
111
+ cancelAnimationFrame(animationRef.current);
112
+ }
113
+ };
114
+ }, [blobConfig]);
115
+
116
+ return (
117
+ <div
118
+ className={cn(
119
+ "relative w-full overflow-hidden",
120
+ className
121
+ )}
122
+ style={{
123
+ background: `linear-gradient(to bottom, ${customStyles?.primaryButtonColor || gradientColors.top}, ${customStyles?.primaryButtonColor ? `${customStyles?.primaryButtonColor}95` : gradientColors.bottom})`,
124
+ opacity: 1,
125
+ transition: 'opacity 0.6s ease-in-out',
126
+ }}
127
+ {...props}
128
+ >
129
+ <canvas ref={canvasRef} className="absolute inset-0 z-10 w-full h-full" />
130
+ <div className="absolute inset-0 backdrop-blur-[140px] bg-white/10 z-20" />
131
+ <div className="relative z-30 w-full h-full">
132
+ {children}
133
+ </div>
134
+ </div>
135
+ );
136
+ }
@@ -0,0 +1,30 @@
1
+ import appStore from '../assets/brand/applestore.svg'
2
+ import playStore from '../assets/brand/playstore.svg'
3
+
4
+ export const MobileAppDownload = () => (
5
+ <div className="text-center w-full">
6
+ <h3 className="text-sm sm:text-base md:text-lg font-semibold mb-3 sm:mb-4">Download our mobile app</h3>
7
+ <div className="flex flex-wrap gap-2 sm:gap-3 justify-center">
8
+ {/* Android App Link */}
9
+ <a
10
+ href="https://play.google.com/store/apps/details?id=app.saafe.prod"
11
+ target="_blank"
12
+ rel="noopener noreferrer"
13
+ className="flex items-center gap-2 border border-gray-300 rounded-lg px-3 sm:px-4 md:px-5 py-2 sm:py-3 hover:shadow-lg transition bg-white max-w-[160px] sm:max-w-none flex-1 justify-center"
14
+ >
15
+ <img src={playStore} alt="Android" className="w-5 h-5 sm:w-6 sm:h-6" />
16
+ <span className="text-xs sm:text-sm font-medium text-gray-800">Google Play</span>
17
+ </a>
18
+ {/* iOS App Link */}
19
+ <a
20
+ href="https://apps.apple.com/in/app/saafe/id6444038213"
21
+ target="_blank"
22
+ rel="noopener noreferrer"
23
+ className="flex items-center gap-2 border border-gray-300 rounded-lg px-3 sm:px-4 md:px-5 py-2 sm:py-3 hover:shadow-lg transition bg-white max-w-[160px] sm:max-w-none flex-1 justify-center"
24
+ >
25
+ <img src={appStore} alt="iOS" className="w-5 h-5 sm:w-6 sm:h-6" />
26
+ <span className="text-xs sm:text-sm font-medium text-gray-800">App Store</span>
27
+ </a>
28
+ </div>
29
+ </div>
30
+ );
@@ -0,0 +1,27 @@
1
+ import React from 'react'
2
+ import Modal from '../ui/modal'
3
+ import { Button } from '../ui/button'
4
+ import { CircleAlert } from 'lucide-react'
5
+
6
+ const ModalComp = () => {
7
+ return (
8
+ <Modal withCloseIcon={false}>
9
+ <Modal.Trigger>
10
+ <Button>Open Modal</Button>
11
+ </Modal.Trigger>
12
+ <div className="flex flex-col items-center gap-4">
13
+ <CircleAlert className="text-amber-900" />
14
+ <p className="font-bold text-xl">Are you sure you want to reject?</p>
15
+ <p className="text-xs text-muted-foreground text-center">Rejecting this consent is a permanent action and cannot be undone. However, you can still view the consent details anytime using Saafe app. </p>
16
+ </div>
17
+ <Modal.Footer>
18
+ <div className="flex gap-4 justify-center items-center">
19
+ <Button size={"lg"}>No, go back!</Button>
20
+ <Button size={"lg"} variant={'secondary'} className="bg-amber-200 text-amber-900">Yes, Reject</Button>
21
+ </div>
22
+ </Modal.Footer>
23
+ </Modal>
24
+ )
25
+ }
26
+
27
+ export default ModalComp
@@ -0,0 +1,36 @@
1
+ import { useTheme } from '@/contexts/ThemeContext'
2
+ import { Moon, Sun } from 'lucide-react'
3
+ import { Button } from "@/components/ui/button"
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuTrigger,
9
+ } from "@/components/ui/dropdown-menu"
10
+
11
+ export function ModeToggle() {
12
+ const { setTheme } = useTheme()
13
+
14
+ return (
15
+ <DropdownMenu>
16
+ <DropdownMenuTrigger asChild>
17
+ <Button variant="ghost" className="w-9 px-0">
18
+ <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
19
+ <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
20
+ <span className="sr-only">Toggle theme</span>
21
+ </Button>
22
+ </DropdownMenuTrigger>
23
+ <DropdownMenuContent align="end">
24
+ <DropdownMenuItem className="cursor-pointer" onClick={() => setTheme("light")}>
25
+ Light
26
+ </DropdownMenuItem>
27
+ <DropdownMenuItem className="cursor-pointer" onClick={() => setTheme("dark")}>
28
+ Dark
29
+ </DropdownMenuItem>
30
+ <DropdownMenuItem className="cursor-pointer" onClick={() => setTheme("system")}>
31
+ System
32
+ </DropdownMenuItem>
33
+ </DropdownMenuContent>
34
+ </DropdownMenu>
35
+ )
36
+ }
@@ -0,0 +1,50 @@
1
+ import { cn } from "@/lib/utils"
2
+
3
+ function PageHeader({
4
+ className,
5
+ children,
6
+ ...props
7
+ }: React.HTMLAttributes<HTMLDivElement>) {
8
+ return (
9
+ <section
10
+ className={cn(
11
+ "pt-6 pb-4 space-y-2",
12
+ className
13
+ )}
14
+ {...props}>
15
+ {children}
16
+ </section>
17
+ )
18
+ }
19
+
20
+ function PageHeaderHeading({
21
+ className,
22
+ ...props
23
+ }: React.HTMLAttributes<HTMLHeadingElement>) {
24
+ return (
25
+ <h1
26
+ className={cn(
27
+ "text-xl font-semibold leading-tight tracking-tight my-1",
28
+ className
29
+ )}
30
+ {...props}
31
+ />
32
+ )
33
+ }
34
+
35
+ function PageHeaderDescription({
36
+ className,
37
+ ...props
38
+ }: React.HTMLAttributes<HTMLParagraphElement>) {
39
+ return (
40
+ <p
41
+ className={cn(
42
+ "max-w-2xl text-base font-light text-muted-foreground",
43
+ className
44
+ )}
45
+ {...props}
46
+ />
47
+ )
48
+ }
49
+
50
+ export { PageHeader, PageHeaderDescription, PageHeaderHeading }