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,259 @@
|
|
1
|
+
"use client";
|
2
|
+
|
3
|
+
import { cn } from "@/lib/utils";
|
4
|
+
import { LoaderCircle } from "lucide-react";
|
5
|
+
import * as React from "react";
|
6
|
+
import { createContext, useContext } from "react";
|
7
|
+
import { CheckIcon } from "@radix-ui/react-icons";
|
8
|
+
|
9
|
+
// Types
|
10
|
+
type StepperContextValue = {
|
11
|
+
activeStep: number;
|
12
|
+
setActiveStep: (step: number) => void;
|
13
|
+
orientation: "horizontal" | "vertical";
|
14
|
+
};
|
15
|
+
|
16
|
+
type StepItemContextValue = {
|
17
|
+
step: number;
|
18
|
+
state: StepState;
|
19
|
+
isDisabled: boolean;
|
20
|
+
isLoading: boolean;
|
21
|
+
};
|
22
|
+
|
23
|
+
type StepState = "active" | "completed" | "inactive" | "loading";
|
24
|
+
|
25
|
+
// Contexts
|
26
|
+
const StepperContext = createContext<StepperContextValue | undefined>(undefined);
|
27
|
+
const StepItemContext = createContext<StepItemContextValue | undefined>(undefined);
|
28
|
+
|
29
|
+
const useStepper = () => {
|
30
|
+
const context = useContext(StepperContext);
|
31
|
+
if (!context) {
|
32
|
+
throw new Error("useStepper must be used within a Stepper");
|
33
|
+
}
|
34
|
+
return context;
|
35
|
+
};
|
36
|
+
|
37
|
+
const useStepItem = () => {
|
38
|
+
const context = useContext(StepItemContext);
|
39
|
+
if (!context) {
|
40
|
+
throw new Error("useStepItem must be used within a StepperItem");
|
41
|
+
}
|
42
|
+
return context;
|
43
|
+
};
|
44
|
+
|
45
|
+
// Components
|
46
|
+
interface StepperProps extends React.HTMLAttributes<HTMLDivElement> {
|
47
|
+
defaultValue?: number;
|
48
|
+
value?: number;
|
49
|
+
onValueChange?: (value: number) => void;
|
50
|
+
orientation?: "horizontal" | "vertical";
|
51
|
+
}
|
52
|
+
|
53
|
+
const Stepper = React.forwardRef<HTMLDivElement, StepperProps>(
|
54
|
+
(
|
55
|
+
{ defaultValue = 0, value, onValueChange, orientation = "horizontal", className, ...props },
|
56
|
+
ref,
|
57
|
+
) => {
|
58
|
+
const [activeStep, setInternalStep] = React.useState(defaultValue);
|
59
|
+
|
60
|
+
const setActiveStep = React.useCallback(
|
61
|
+
(step: number) => {
|
62
|
+
if (value === undefined) {
|
63
|
+
setInternalStep(step);
|
64
|
+
}
|
65
|
+
onValueChange?.(step);
|
66
|
+
},
|
67
|
+
[value, onValueChange],
|
68
|
+
);
|
69
|
+
|
70
|
+
const currentStep = value ?? activeStep;
|
71
|
+
|
72
|
+
return (
|
73
|
+
<StepperContext.Provider
|
74
|
+
value={{
|
75
|
+
activeStep: currentStep,
|
76
|
+
setActiveStep,
|
77
|
+
orientation,
|
78
|
+
}}
|
79
|
+
>
|
80
|
+
<div
|
81
|
+
ref={ref}
|
82
|
+
className={cn(
|
83
|
+
"group/stepper inline-flex data-[orientation=horizontal]:w-full data-[orientation=horizontal]:flex-row data-[orientation=vertical]:flex-col",
|
84
|
+
className,
|
85
|
+
)}
|
86
|
+
data-orientation={orientation}
|
87
|
+
{...props}
|
88
|
+
/>
|
89
|
+
</StepperContext.Provider>
|
90
|
+
);
|
91
|
+
},
|
92
|
+
);
|
93
|
+
Stepper.displayName = "Stepper";
|
94
|
+
|
95
|
+
// StepperItem
|
96
|
+
interface StepperItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
97
|
+
step: number;
|
98
|
+
completed?: boolean;
|
99
|
+
disabled?: boolean;
|
100
|
+
loading?: boolean;
|
101
|
+
}
|
102
|
+
|
103
|
+
const StepperItem = React.forwardRef<HTMLDivElement, StepperItemProps>(
|
104
|
+
(
|
105
|
+
{ step, completed = false, disabled = false, loading = false, className, children, ...props },
|
106
|
+
ref,
|
107
|
+
) => {
|
108
|
+
const { activeStep } = useStepper();
|
109
|
+
|
110
|
+
const state: StepState =
|
111
|
+
completed || step < activeStep ? "completed" : activeStep === step ? "active" : "inactive";
|
112
|
+
|
113
|
+
const isLoading = loading && step === activeStep;
|
114
|
+
|
115
|
+
return (
|
116
|
+
<StepItemContext.Provider value={{ step, state, isDisabled: disabled, isLoading }}>
|
117
|
+
<div
|
118
|
+
ref={ref}
|
119
|
+
className={cn(
|
120
|
+
"group/step flex items-center group-data-[orientation=horizontal]/stepper:flex-row group-data-[orientation=vertical]/stepper:flex-col",
|
121
|
+
className,
|
122
|
+
)}
|
123
|
+
data-state={state}
|
124
|
+
{...(isLoading ? { "data-loading": true } : {})}
|
125
|
+
{...props}
|
126
|
+
>
|
127
|
+
{children}
|
128
|
+
</div>
|
129
|
+
</StepItemContext.Provider>
|
130
|
+
);
|
131
|
+
},
|
132
|
+
);
|
133
|
+
StepperItem.displayName = "StepperItem";
|
134
|
+
|
135
|
+
// StepperTrigger
|
136
|
+
interface StepperTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
137
|
+
asChild?: boolean;
|
138
|
+
}
|
139
|
+
|
140
|
+
const StepperTrigger = React.forwardRef<HTMLButtonElement, StepperTriggerProps>(
|
141
|
+
({ asChild = false, className, children, ...props }, ref) => {
|
142
|
+
const { setActiveStep } = useStepper();
|
143
|
+
const { step, isDisabled } = useStepItem();
|
144
|
+
|
145
|
+
if (asChild) {
|
146
|
+
return <div className={className}>{children}</div>;
|
147
|
+
}
|
148
|
+
|
149
|
+
return (
|
150
|
+
<button
|
151
|
+
ref={ref}
|
152
|
+
className={cn(
|
153
|
+
"inline-flex items-center gap-3 disabled:pointer-events-none disabled:opacity-50",
|
154
|
+
className,
|
155
|
+
)}
|
156
|
+
onClick={() => setActiveStep(step)}
|
157
|
+
disabled={isDisabled}
|
158
|
+
{...props}
|
159
|
+
>
|
160
|
+
{children}
|
161
|
+
</button>
|
162
|
+
);
|
163
|
+
},
|
164
|
+
);
|
165
|
+
StepperTrigger.displayName = "StepperTrigger";
|
166
|
+
|
167
|
+
// StepperIndicator
|
168
|
+
interface StepperIndicatorProps extends React.HTMLAttributes<HTMLDivElement> {
|
169
|
+
asChild?: boolean;
|
170
|
+
}
|
171
|
+
|
172
|
+
const StepperIndicator = React.forwardRef<HTMLDivElement, StepperIndicatorProps>(
|
173
|
+
({ asChild = false, className, children, ...props }, ref) => {
|
174
|
+
const { state, step, isLoading } = useStepItem();
|
175
|
+
|
176
|
+
return (
|
177
|
+
<div
|
178
|
+
ref={ref}
|
179
|
+
className={cn(
|
180
|
+
"relative flex size-6 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-medium text-muted-foreground data-[state=active]:bg-primary data-[state=completed]:bg-primary data-[state=active]:text-primary-foreground data-[state=completed]:text-primary-foreground",
|
181
|
+
className,
|
182
|
+
)}
|
183
|
+
data-state={state}
|
184
|
+
{...props}
|
185
|
+
>
|
186
|
+
{asChild ? (
|
187
|
+
children
|
188
|
+
) : (
|
189
|
+
<>
|
190
|
+
<span className="transition-all group-data-[loading=true]/step:scale-0 group-data-[state=completed]/step:scale-0 group-data-[loading=true]/step:opacity-0 group-data-[state=completed]/step:opacity-0 group-data-[loading=true]/step:transition-none">
|
191
|
+
{step}
|
192
|
+
</span>
|
193
|
+
<CheckIcon
|
194
|
+
className="absolute scale-0 opacity-0 transition-all group-data-[state=completed]/step:scale-100 group-data-[state=completed]/step:opacity-100"
|
195
|
+
width={16}
|
196
|
+
height={16}
|
197
|
+
aria-hidden="true"
|
198
|
+
/>
|
199
|
+
{isLoading && (
|
200
|
+
<span className="absolute transition-all">
|
201
|
+
<LoaderCircle
|
202
|
+
className="animate-spin"
|
203
|
+
size={14}
|
204
|
+
strokeWidth={2}
|
205
|
+
aria-hidden="true"
|
206
|
+
/>
|
207
|
+
</span>
|
208
|
+
)}
|
209
|
+
</>
|
210
|
+
)}
|
211
|
+
</div>
|
212
|
+
);
|
213
|
+
},
|
214
|
+
);
|
215
|
+
StepperIndicator.displayName = "StepperIndicator";
|
216
|
+
|
217
|
+
// StepperTitle
|
218
|
+
const StepperTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
219
|
+
({ className, ...props }, ref) => (
|
220
|
+
<h3 ref={ref} className={cn("text-sm font-medium", className)} {...props} />
|
221
|
+
),
|
222
|
+
);
|
223
|
+
StepperTitle.displayName = "StepperTitle";
|
224
|
+
|
225
|
+
// StepperDescription
|
226
|
+
const StepperDescription = React.forwardRef<
|
227
|
+
HTMLParagraphElement,
|
228
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
229
|
+
>(({ className, ...props }, ref) => (
|
230
|
+
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
231
|
+
));
|
232
|
+
StepperDescription.displayName = "StepperDescription";
|
233
|
+
|
234
|
+
// StepperSeparator
|
235
|
+
const StepperSeparator = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
236
|
+
({ className, ...props }, ref) => {
|
237
|
+
return (
|
238
|
+
<div
|
239
|
+
ref={ref}
|
240
|
+
className={cn(
|
241
|
+
"m-0.5 bg-muted group-data-[orientation=horizontal]/stepper:h-0.5 group-data-[orientation=vertical]/stepper:h-12 group-data-[orientation=horizontal]/stepper:w-full group-data-[orientation=vertical]/stepper:w-0.5 group-data-[orientation=horizontal]/stepper:flex-1 group-data-[state=completed]/step:bg-primary",
|
242
|
+
className,
|
243
|
+
)}
|
244
|
+
{...props}
|
245
|
+
/>
|
246
|
+
);
|
247
|
+
},
|
248
|
+
);
|
249
|
+
StepperSeparator.displayName = "StepperSeparator";
|
250
|
+
|
251
|
+
export {
|
252
|
+
Stepper,
|
253
|
+
StepperDescription,
|
254
|
+
StepperIndicator,
|
255
|
+
StepperItem,
|
256
|
+
StepperSeparator,
|
257
|
+
StepperTitle,
|
258
|
+
StepperTrigger,
|
259
|
+
};
|
@@ -0,0 +1,55 @@
|
|
1
|
+
"use client"
|
2
|
+
|
3
|
+
import * as React from "react"
|
4
|
+
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
5
|
+
|
6
|
+
import { cn } from "@/lib/utils"
|
7
|
+
|
8
|
+
const Tabs = TabsPrimitive.Root
|
9
|
+
|
10
|
+
const TabsList = React.forwardRef<
|
11
|
+
React.ElementRef<typeof TabsPrimitive.List>,
|
12
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
13
|
+
>(({ className, ...props }, ref) => (
|
14
|
+
<TabsPrimitive.List
|
15
|
+
ref={ref}
|
16
|
+
className={cn(
|
17
|
+
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
18
|
+
className
|
19
|
+
)}
|
20
|
+
{...props}
|
21
|
+
/>
|
22
|
+
))
|
23
|
+
TabsList.displayName = TabsPrimitive.List.displayName
|
24
|
+
|
25
|
+
const TabsTrigger = React.forwardRef<
|
26
|
+
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
27
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
28
|
+
>(({ className, ...props }, ref) => (
|
29
|
+
<TabsPrimitive.Trigger
|
30
|
+
ref={ref}
|
31
|
+
className={cn(
|
32
|
+
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
33
|
+
className
|
34
|
+
)}
|
35
|
+
{...props}
|
36
|
+
/>
|
37
|
+
))
|
38
|
+
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
39
|
+
|
40
|
+
const TabsContent = React.forwardRef<
|
41
|
+
React.ElementRef<typeof TabsPrimitive.Content>,
|
42
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
43
|
+
>(({ className, ...props }, ref) => (
|
44
|
+
<TabsPrimitive.Content
|
45
|
+
ref={ref}
|
46
|
+
className={cn(
|
47
|
+
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
48
|
+
className
|
49
|
+
)}
|
50
|
+
{...props}
|
51
|
+
/>
|
52
|
+
))
|
53
|
+
TabsContent.displayName = TabsPrimitive.Content.displayName
|
54
|
+
|
55
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
@@ -0,0 +1,61 @@
|
|
1
|
+
"use client"
|
2
|
+
|
3
|
+
import * as React from "react"
|
4
|
+
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
5
|
+
|
6
|
+
import { cn } from "@/lib/utils"
|
7
|
+
|
8
|
+
function TooltipProvider({
|
9
|
+
delayDuration = 0,
|
10
|
+
...props
|
11
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
12
|
+
return (
|
13
|
+
<TooltipPrimitive.Provider
|
14
|
+
data-slot="tooltip-provider"
|
15
|
+
delayDuration={delayDuration}
|
16
|
+
{...props}
|
17
|
+
/>
|
18
|
+
)
|
19
|
+
}
|
20
|
+
|
21
|
+
function Tooltip({
|
22
|
+
...props
|
23
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
24
|
+
return (
|
25
|
+
<TooltipProvider>
|
26
|
+
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
27
|
+
</TooltipProvider>
|
28
|
+
)
|
29
|
+
}
|
30
|
+
|
31
|
+
function TooltipTrigger({
|
32
|
+
...props
|
33
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
34
|
+
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
35
|
+
}
|
36
|
+
|
37
|
+
function TooltipContent({
|
38
|
+
className,
|
39
|
+
sideOffset = 0,
|
40
|
+
children,
|
41
|
+
...props
|
42
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
43
|
+
return (
|
44
|
+
<TooltipPrimitive.Portal>
|
45
|
+
<TooltipPrimitive.Content
|
46
|
+
data-slot="tooltip-content"
|
47
|
+
sideOffset={sideOffset}
|
48
|
+
className={cn(
|
49
|
+
"bg-gray-700 text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
50
|
+
className
|
51
|
+
)}
|
52
|
+
{...props}
|
53
|
+
>
|
54
|
+
{children}
|
55
|
+
<TooltipPrimitive.Arrow className="bg-gray-700 fill-gray-700 z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
56
|
+
</TooltipPrimitive.Content>
|
57
|
+
</TooltipPrimitive.Portal>
|
58
|
+
)
|
59
|
+
}
|
60
|
+
|
61
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import logo from "@/assets/brand/saafe-icon.svg";
|
2
|
+
|
3
|
+
interface UrlLoaderProps {
|
4
|
+
message?: string;
|
5
|
+
isError?: boolean;
|
6
|
+
isWarning?: boolean;
|
7
|
+
}
|
8
|
+
|
9
|
+
export function UrlDecodeLoader({ message = "Loading...", isError = false, isWarning = false }: UrlLoaderProps) {
|
10
|
+
return (
|
11
|
+
<div className="flex flex-col items-center justify-center h-screen w-full">
|
12
|
+
<div className="flex flex-col items-center gap-6">
|
13
|
+
<img src={logo} alt="Saafe Logo" className="h-16 w-16" />
|
14
|
+
|
15
|
+
{isError ? (
|
16
|
+
<div className="flex flex-col items-center gap-4">
|
17
|
+
<p className="text-red-500 font-medium text-lg">Error</p>
|
18
|
+
<p className="text-gray-700 dark:text-gray-300">{message}</p>
|
19
|
+
</div>
|
20
|
+
) : isWarning ? (
|
21
|
+
<div className="flex flex-col items-center gap-4">
|
22
|
+
<p className="text-yellow-500 font-medium text-lg">No Consent handler found</p>
|
23
|
+
<p className="text-gray-700 dark:text-gray-300">{message}</p>
|
24
|
+
</div>
|
25
|
+
) : (
|
26
|
+
<div className="flex flex-col items-center gap-4">
|
27
|
+
<div className="flex items-center justify-center">
|
28
|
+
<div className="w-10 h-10 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
|
29
|
+
</div>
|
30
|
+
<p className="text-gray-700 dark:text-gray-300">{message}</p>
|
31
|
+
</div>
|
32
|
+
)}
|
33
|
+
</div>
|
34
|
+
</div>
|
35
|
+
);
|
36
|
+
}
|
@@ -0,0 +1,104 @@
|
|
1
|
+
import { cn } from "@/lib/utils";
|
2
|
+
import { Badge } from "@/components/ui/badge";
|
3
|
+
import { Info } from "lucide-react";
|
4
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
5
|
+
import { useMediaQuery } from "@/hooks/use-media-query";
|
6
|
+
import { getCurrentEnvironment, getEnvironmentConfig } from "@/config/environments";
|
7
|
+
|
8
|
+
interface VersionDisplayProps {
|
9
|
+
className?: string;
|
10
|
+
variant?: "default" | "footer" | "badge" | "minimal";
|
11
|
+
showIcon?: boolean;
|
12
|
+
showLabel?: boolean;
|
13
|
+
}
|
14
|
+
|
15
|
+
export function VersionDisplay({
|
16
|
+
className,
|
17
|
+
variant = "default",
|
18
|
+
showIcon = true,
|
19
|
+
showLabel = true
|
20
|
+
}: VersionDisplayProps) {
|
21
|
+
// Get version from package.json
|
22
|
+
const version = import.meta.env.VITE_APP_VERSION || "1.1.0";
|
23
|
+
const buildDate = import.meta.env.VITE_BUILD_DATE || new Date().toISOString().split('T')[0];
|
24
|
+
const environment = getCurrentEnvironment();
|
25
|
+
const config = getEnvironmentConfig();
|
26
|
+
const isMobile = useMediaQuery("(max-width: 768px)");
|
27
|
+
|
28
|
+
|
29
|
+
const VersionContent = () => {
|
30
|
+
switch (variant) {
|
31
|
+
case "footer":
|
32
|
+
return (
|
33
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
34
|
+
{showIcon && <Info className="h-3 w-3" />}
|
35
|
+
<span>v{version}</span>
|
36
|
+
</div>
|
37
|
+
);
|
38
|
+
|
39
|
+
case "badge":
|
40
|
+
return (
|
41
|
+
<Badge variant="outline" className="text-xs">
|
42
|
+
{showIcon && <Info className="h-3 w-3 mr-1" />}
|
43
|
+
v{version}
|
44
|
+
</Badge>
|
45
|
+
);
|
46
|
+
|
47
|
+
case "minimal":
|
48
|
+
return (
|
49
|
+
<span className={cn("text-xs text-muted-foreground", isMobile ? "text-muted-secondary font-light" : "text-white")}>
|
50
|
+
v{version} {environment.toLowerCase() !== 'production' && environment !== "" ? `| ${environment.toUpperCase()}` : null}
|
51
|
+
</span>
|
52
|
+
);
|
53
|
+
|
54
|
+
default:
|
55
|
+
return (
|
56
|
+
<div className="flex items-center gap-2 text-sm">
|
57
|
+
{showIcon && <Info className="h-4 w-4 text-muted-foreground" />}
|
58
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:gap-2">
|
59
|
+
{showLabel && <span className="text-muted-foreground">Version:</span>}
|
60
|
+
<span className="font-medium">v{version}</span>
|
61
|
+
<span className="text-xs text-muted-foreground hidden sm:inline">
|
62
|
+
({buildDate})
|
63
|
+
</span>
|
64
|
+
</div>
|
65
|
+
</div>
|
66
|
+
);
|
67
|
+
}
|
68
|
+
};
|
69
|
+
|
70
|
+
const tooltipContent = (
|
71
|
+
<div className="min-w-48">
|
72
|
+
<div className="text-center mb-2">
|
73
|
+
<div className="text-sm font-medium">Version {version}</div>
|
74
|
+
</div>
|
75
|
+
<div className="flex flex-col gap-1">
|
76
|
+
<div className="text-xs text-white">Released on {buildDate}</div>
|
77
|
+
{
|
78
|
+
environment !== "production" && (
|
79
|
+
<>
|
80
|
+
<div className="text-xs text-white">Environment: {config.name}</div>
|
81
|
+
<div className="text-xs text-white">API: {config.apiBaseUrl}</div>
|
82
|
+
{config.debug && <div className="text-xs text-yellow-500">Debug Mode Enabled</div>}
|
83
|
+
</>
|
84
|
+
)
|
85
|
+
}
|
86
|
+
</div>
|
87
|
+
</div>
|
88
|
+
);
|
89
|
+
|
90
|
+
return (
|
91
|
+
<TooltipProvider>
|
92
|
+
<Tooltip>
|
93
|
+
<TooltipTrigger asChild>
|
94
|
+
<div className={cn("cursor-help", className)}>
|
95
|
+
<VersionContent />
|
96
|
+
</div>
|
97
|
+
</TooltipTrigger>
|
98
|
+
<TooltipContent>
|
99
|
+
{tooltipContent}
|
100
|
+
</TooltipContent>
|
101
|
+
</Tooltip>
|
102
|
+
</TooltipProvider>
|
103
|
+
);
|
104
|
+
}
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import React from 'react'
|
2
|
+
import { AnimatePresence, motion } from 'framer-motion'
|
3
|
+
import { useMediaQuery } from '@/hooks/use-media-query';
|
4
|
+
import { useRTL } from '@/contexts/RTLContext';
|
5
|
+
const WebFooter = ({
|
6
|
+
show,
|
7
|
+
children
|
8
|
+
}: {
|
9
|
+
show: boolean;
|
10
|
+
children: React.ReactNode;
|
11
|
+
}) => {
|
12
|
+
const isMobile = useMediaQuery("(max-width: 768px)");
|
13
|
+
const { isRTL } = useRTL();
|
14
|
+
return (
|
15
|
+
<div className='w-full'>
|
16
|
+
<AnimatePresence>
|
17
|
+
{(show && !isMobile) ? (
|
18
|
+
<motion.div
|
19
|
+
key={"desktop"}
|
20
|
+
initial={{ y: 100, opacity: 0 }}
|
21
|
+
animate={{ y: 0, opacity: 1 }}
|
22
|
+
exit={{ y: 100, opacity: 0 }}
|
23
|
+
transition={{ duration: 0.3 }}
|
24
|
+
className={`${isRTL ? 'w-[70%] right-[30%]' : 'w-[70%] left-[30%]'} fixed bottom-0`}
|
25
|
+
>
|
26
|
+
<div className='drop-shadow-[0_35px_35px_rgba(0,0,0,0.25)] flex justify-end w-full px-14 gap-4 py-4 items-end bg-white dark:bg-card'>
|
27
|
+
{children}
|
28
|
+
</div>
|
29
|
+
</motion.div>
|
30
|
+
) : null}
|
31
|
+
</AnimatePresence>
|
32
|
+
</div>
|
33
|
+
)
|
34
|
+
}
|
35
|
+
|
36
|
+
export default WebFooter
|
@@ -0,0 +1,99 @@
|
|
1
|
+
export interface EnvironmentConfig {
|
2
|
+
name: string;
|
3
|
+
apiBaseUrl: string;
|
4
|
+
debug: boolean;
|
5
|
+
analyticsEnabled: boolean;
|
6
|
+
features: {
|
7
|
+
[key: string]: boolean;
|
8
|
+
};
|
9
|
+
}
|
10
|
+
|
11
|
+
export const environments: Record<string, EnvironmentConfig> = {
|
12
|
+
production: {
|
13
|
+
name: 'Production',
|
14
|
+
apiBaseUrl: import.meta.env.VITE_API_BASE_URL,
|
15
|
+
debug: false,
|
16
|
+
analyticsEnabled: true,
|
17
|
+
features: {
|
18
|
+
debugPanel: false,
|
19
|
+
testMode: false,
|
20
|
+
mockData: false,
|
21
|
+
},
|
22
|
+
},
|
23
|
+
stage: {
|
24
|
+
name: 'Stage',
|
25
|
+
apiBaseUrl: import.meta.env.VITE_API_BASE_URL,
|
26
|
+
debug: true,
|
27
|
+
analyticsEnabled: true,
|
28
|
+
features: {
|
29
|
+
debugPanel: true,
|
30
|
+
testMode: true,
|
31
|
+
mockData: false,
|
32
|
+
},
|
33
|
+
},
|
34
|
+
sandbox: {
|
35
|
+
name: 'Sandbox',
|
36
|
+
apiBaseUrl: import.meta.env.VITE_API_BASE_URL,
|
37
|
+
debug: true,
|
38
|
+
analyticsEnabled: false,
|
39
|
+
features: {
|
40
|
+
debugPanel: true,
|
41
|
+
testMode: true,
|
42
|
+
mockData: true,
|
43
|
+
},
|
44
|
+
},
|
45
|
+
development: {
|
46
|
+
name: 'Development',
|
47
|
+
apiBaseUrl: 'http://localhost:3000',
|
48
|
+
debug: true,
|
49
|
+
analyticsEnabled: false,
|
50
|
+
features: {
|
51
|
+
debugPanel: true,
|
52
|
+
testMode: true,
|
53
|
+
mockData: true,
|
54
|
+
},
|
55
|
+
},
|
56
|
+
};
|
57
|
+
|
58
|
+
// Get current environment based on various indicators
|
59
|
+
export function getCurrentEnvironment(): string {
|
60
|
+
// Check for explicit environment variable
|
61
|
+
if (import.meta.env.VITE_ENVIRONMENT) {
|
62
|
+
return import.meta.env.VITE_ENVIRONMENT;
|
63
|
+
}
|
64
|
+
|
65
|
+
// Check build mode
|
66
|
+
if (import.meta.env.MODE) {
|
67
|
+
return import.meta.env.MODE;
|
68
|
+
}
|
69
|
+
|
70
|
+
// Check hostname in browser
|
71
|
+
if (typeof window !== 'undefined') {
|
72
|
+
const hostname = window.location.hostname;
|
73
|
+
|
74
|
+
if (hostname.includes('localhost') || hostname.includes('127.0.0.1')) {
|
75
|
+
return 'development';
|
76
|
+
} else if (hostname.includes('sandbox')) {
|
77
|
+
return 'sandbox';
|
78
|
+
} else if (hostname.includes('stage') || hostname.includes('staging')) {
|
79
|
+
return 'stage';
|
80
|
+
} else {
|
81
|
+
return 'production';
|
82
|
+
}
|
83
|
+
}
|
84
|
+
|
85
|
+
// Default to production
|
86
|
+
return 'production';
|
87
|
+
}
|
88
|
+
|
89
|
+
// Get configuration for current environment
|
90
|
+
export function getEnvironmentConfig(): EnvironmentConfig {
|
91
|
+
const currentEnv = getCurrentEnvironment();
|
92
|
+
return environments[currentEnv] || environments.production;
|
93
|
+
}
|
94
|
+
|
95
|
+
// Helper to check if feature is enabled
|
96
|
+
export function isFeatureEnabled(feature: string): boolean {
|
97
|
+
const config = getEnvironmentConfig();
|
98
|
+
return config.features[feature] || false;
|
99
|
+
}
|