saafe-redirection-flow 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/build-and-deploy.yml +41 -0
- package/.gitlab-ci.yml +108 -0
- package/.releaserc.json +18 -0
- package/.storybook/main.ts +28 -0
- package/.storybook/preview.ts +16 -0
- package/.storybook/vitest.setup.ts +9 -0
- package/.vite/deps/@radix-ui_react-avatar.js +230 -0
- package/.vite/deps/@radix-ui_react-avatar.js.map +7 -0
- package/.vite/deps/@radix-ui_react-slot.js +12 -0
- package/.vite/deps/@radix-ui_react-slot.js.map +7 -0
- package/.vite/deps/_metadata.json +79 -0
- package/.vite/deps/chunk-5VGQBUCU.js +597 -0
- package/.vite/deps/chunk-5VGQBUCU.js.map +7 -0
- package/.vite/deps/chunk-DC5AMYBS.js +38 -0
- package/.vite/deps/chunk-DC5AMYBS.js.map +7 -0
- package/.vite/deps/chunk-HUIEPYH7.js +11265 -0
- package/.vite/deps/chunk-HUIEPYH7.js.map +7 -0
- package/.vite/deps/chunk-TKHB4QMX.js +281 -0
- package/.vite/deps/chunk-TKHB4QMX.js.map +7 -0
- package/.vite/deps/chunk-YLDSBLSF.js +1139 -0
- package/.vite/deps/chunk-YLDSBLSF.js.map +7 -0
- package/.vite/deps/class-variance-authority.js +63 -0
- package/.vite/deps/class-variance-authority.js.map +7 -0
- package/.vite/deps/lucide-react.js +36984 -0
- package/.vite/deps/lucide-react.js.map +7 -0
- package/.vite/deps/package.json +3 -0
- package/.vite/deps/react-dom_client.js +17917 -0
- package/.vite/deps/react-dom_client.js.map +7 -0
- package/.vite/deps/react-router-dom.js +452 -0
- package/.vite/deps/react-router-dom.js.map +7 -0
- package/.vite/deps/react-router.js +234 -0
- package/.vite/deps/react-router.js.map +7 -0
- package/.vite/deps/react.js +5 -0
- package/.vite/deps/react.js.map +7 -0
- package/.vite/deps/react_jsx-dev-runtime.js +470 -0
- package/.vite/deps/react_jsx-dev-runtime.js.map +7 -0
- package/CHANGELOG.md +420 -0
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/RELEASE_CHEATSHEET.md +93 -0
- package/RELEASE_NOTES.md +120 -0
- package/components.json +21 -0
- package/docs/DEPLOYMENT_WORKFLOW.md +262 -0
- package/docs/RELEASE_GUIDE.md +591 -0
- package/docs/architecture.md +432 -0
- package/docs/components.md +199 -0
- package/docs/index.md +69 -0
- package/docs/local-release-workflow.md +234 -0
- package/docs/routes.md +118 -0
- package/docs/sdk-integration.md +325 -0
- package/docs/semantic-release.md +124 -0
- package/docs/user-flow.md +206 -0
- package/eslint.config.js +28 -0
- package/index.html +19 -0
- package/install.sh +198 -0
- package/package.json +115 -0
- package/public/images/bank-logo.png +0 -0
- package/public/saafe-icon.svg +9 -0
- package/src/App.tsx +171 -0
- package/src/__tests__/url-parameters.test.ts +82 -0
- package/src/assets/brand/applestore.svg +13 -0
- package/src/assets/brand/playstore.svg +23 -0
- package/src/assets/brand/saafe-color-white-logo.svg +14 -0
- package/src/assets/brand/saafe-icon.svg +9 -0
- package/src/assets/brand/saafe-logo.svg +18 -0
- package/src/assets/icons/check-icon-dark.svg +27 -0
- package/src/assets/icons/check-icon.svg +23 -0
- package/src/components/ErrorBoundary.tsx +132 -0
- package/src/components/alert/alert.tsx +27 -0
- package/src/components/auth/AuthGuard.tsx +76 -0
- package/src/components/cards/BankCard.stories.tsx +69 -0
- package/src/components/cards/BankCard.tsx +227 -0
- package/src/components/cards/OuterCard.tsx +109 -0
- package/src/components/cards/WrapperCard.tsx +64 -0
- package/src/components/documents/PrivacyContent.tsx +1 -0
- package/src/components/dummyFooter.tsx +29 -0
- package/src/components/icons/github.tsx +12 -0
- package/src/components/language/LanguageSwitcher.tsx +44 -0
- package/src/components/layouts/FrostedLayout.stories.tsx +42 -0
- package/src/components/layouts/FrostedLayout.tsx +333 -0
- package/src/components/layouts/MobileLayout.tsx +403 -0
- package/src/components/mobile-background.tsx +136 -0
- package/src/components/mobileAppDownload.tsx +30 -0
- package/src/components/modal/ModalComp.tsx +27 -0
- package/src/components/mode-toggle.tsx +36 -0
- package/src/components/page-header.tsx +50 -0
- package/src/components/session/SessionTimeoutScreen.tsx +134 -0
- package/src/components/session/SessionTimer.tsx +173 -0
- package/src/components/step-navigation.tsx +87 -0
- package/src/components/title/AppBar.stories.tsx +50 -0
- package/src/components/title/AppBar.tsx +150 -0
- package/src/components/title/SectionTitle.tsx +31 -0
- package/src/components/ui/AnimatedButton.module.css +13 -0
- package/src/components/ui/alert.tsx +66 -0
- package/src/components/ui/animatedButton.tsx +111 -0
- package/src/components/ui/avatar.tsx +51 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/bottom-sheet.tsx +122 -0
- package/src/components/ui/button.tsx +59 -0
- package/src/components/ui/calendar.tsx +86 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/checkbox.stories.tsx +49 -0
- package/src/components/ui/checkbox.tsx +67 -0
- package/src/components/ui/collapsible.tsx +45 -0
- package/src/components/ui/dialog.tsx +134 -0
- package/src/components/ui/document-link.tsx +26 -0
- package/src/components/ui/dot-stepper.tsx +57 -0
- package/src/components/ui/dropdown-menu.tsx +255 -0
- package/src/components/ui/form.tsx +165 -0
- package/src/components/ui/frosted-panel.stories.tsx +86 -0
- package/src/components/ui/frosted-panel.tsx +276 -0
- package/src/components/ui/input.tsx +39 -0
- package/src/components/ui/label.stories.tsx +67 -0
- package/src/components/ui/label.tsx +23 -0
- package/src/components/ui/mobile-footer.tsx +54 -0
- package/src/components/ui/modal.tsx +90 -0
- package/src/components/ui/otp-input.stories.tsx +62 -0
- package/src/components/ui/otp-input.tsx +221 -0
- package/src/components/ui/platform-specific-behavior.tsx +28 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/progress.tsx +103 -0
- package/src/components/ui/radio-group.tsx +45 -0
- package/src/components/ui/scroll-area.tsx +56 -0
- package/src/components/ui/sdk-params-docs.tsx +53 -0
- package/src/components/ui/select.tsx +159 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +137 -0
- package/src/components/ui/sidebar.tsx +724 -0
- package/src/components/ui/skeleton.stories.tsx +50 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/sonner.tsx +23 -0
- package/src/components/ui/step.stories.tsx +132 -0
- package/src/components/ui/step.tsx +234 -0
- package/src/components/ui/stepper-progress.tsx +136 -0
- package/src/components/ui/stepper.tsx +259 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/tooltip.tsx +61 -0
- package/src/components/ui/url-decode-loader.tsx +36 -0
- package/src/components/ui/version-display.tsx +104 -0
- package/src/components/ui/web-footer.tsx +36 -0
- package/src/config/environments.ts +99 -0
- package/src/config/urls.ts +53 -0
- package/src/const/fiTypeCategoryMap.ts +19 -0
- package/src/contexts/LanguageContext.tsx +41 -0
- package/src/contexts/RTLContext.tsx +42 -0
- package/src/contexts/ThemeContext.tsx +93 -0
- package/src/hooks/use-account-discovery.ts +205 -0
- package/src/hooks/use-auth-query.ts +141 -0
- package/src/hooks/use-fip-query.ts +72 -0
- package/src/hooks/use-media-query.ts +32 -0
- package/src/hooks/use-mobile.ts +24 -0
- package/src/hooks/use-page-title.tsx +48 -0
- package/src/hooks/use-platform.ts +52 -0
- package/src/hooks/use-trusted-count.ts +21 -0
- package/src/hooks/use-url-decode.ts +90 -0
- package/src/hooks/useStep.ts +170 -0
- package/src/index.css +154 -0
- package/src/interfaces/app.interfaces.ts +39 -0
- package/src/interfaces/services.interfaces.ts +65 -0
- package/src/lib/i18n.ts +68 -0
- package/src/lib/utils.ts +6 -0
- package/src/locales/en/common.json +167 -0
- package/src/locales/hi/common.json +137 -0
- package/src/locales/kn/common.json +137 -0
- package/src/locales/ml/common.json +137 -0
- package/src/locales/ta/common.json +137 -0
- package/src/locales/te/common.json +137 -0
- package/src/locales/ur/common.json +138 -0
- package/src/main.tsx +46 -0
- package/src/pages/Login.tsx +363 -0
- package/src/pages/accounts/AccountsToProceed.tsx +396 -0
- package/src/pages/accounts/Discover.tsx +76 -0
- package/src/pages/accounts/DiscoverAccount.tsx +751 -0
- package/src/pages/accounts/LinkSelectedAccounts.tsx +638 -0
- package/src/pages/accounts/OldUser.tsx +329 -0
- package/src/pages/accounts/link-accounts.tsx +913 -0
- package/src/pages/consent/ReviewConsent.tsx +836 -0
- package/src/pages/consent/rejected.tsx +253 -0
- package/src/pages/consent/success.tsx +220 -0
- package/src/providers/query-provider.tsx +24 -0
- package/src/providers/toast-provider.tsx +26 -0
- package/src/services/api/account.service.ts +296 -0
- package/src/services/api/auth.service.ts +206 -0
- package/src/services/api/axios.ts +138 -0
- package/src/services/api/consent.service.ts +142 -0
- package/src/services/api/decode.service.ts +53 -0
- package/src/services/api/feedback.service.ts +34 -0
- package/src/services/api/fip.service.ts +187 -0
- package/src/services/api/index.ts +9 -0
- package/src/services/api/public.service.ts +18 -0
- package/src/services/api.ts +2 -0
- package/src/services/postMessage.service.ts +179 -0
- package/src/store/NavigationBlockContext.tsx +34 -0
- package/src/store/auth.store.ts +79 -0
- package/src/store/fip.store.ts +396 -0
- package/src/store/mandatoryConsent.store.ts +24 -0
- package/src/store/redirect.store.ts +73 -0
- package/src/store/step.store.ts +124 -0
- package/src/stories/Button.stories.ts +53 -0
- package/src/stories/Button.tsx +37 -0
- package/src/stories/Configure.mdx +364 -0
- package/src/stories/Header.stories.ts +33 -0
- package/src/stories/Header.tsx +56 -0
- package/src/stories/Page.stories.ts +32 -0
- package/src/stories/Page.tsx +73 -0
- package/src/stories/button.css +30 -0
- package/src/stories/header.css +32 -0
- package/src/stories/page.css +68 -0
- package/src/styles/rtl-utils.css +90 -0
- package/src/styles/rtl.css +105 -0
- package/src/utils/api-error.ts +26 -0
- package/src/utils/cn.ts +10 -0
- package/src/utils/error-callback.ts +116 -0
- package/src/utils/formatAccountNumber.ts +9 -0
- package/src/utils/handleIdentifiers.ts +90 -0
- package/src/utils/posthog.ts +67 -0
- package/src/utils/toast-helpers.ts +61 -0
- package/src/vite-env.d.ts +1 -0
- package/stage-aa-2506251021.zip +0 -0
- package/tsconfig.app.json +33 -0
- package/tsconfig.json +13 -0
- package/tsconfig.node.json +24 -0
- package/vite.config.ts +45 -0
- package/vitest.shims.d.ts +1 -0
- package/vitest.workspace.ts +46 -0
@@ -0,0 +1,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 }
|