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,165 @@
|
|
1
|
+
import * as React from "react"
|
2
|
+
import * as LabelPrimitive from "@radix-ui/react-label"
|
3
|
+
import { Slot } from "@radix-ui/react-slot"
|
4
|
+
import {
|
5
|
+
Controller,
|
6
|
+
FormProvider,
|
7
|
+
useFormContext,
|
8
|
+
useFormState,
|
9
|
+
type ControllerProps,
|
10
|
+
type FieldPath,
|
11
|
+
type FieldValues,
|
12
|
+
} from "react-hook-form"
|
13
|
+
|
14
|
+
import { cn } from "@/lib/utils"
|
15
|
+
import { Label } from "@/components/ui/label"
|
16
|
+
|
17
|
+
const Form = FormProvider
|
18
|
+
|
19
|
+
type FormFieldContextValue<
|
20
|
+
TFieldValues extends FieldValues = FieldValues,
|
21
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
22
|
+
> = {
|
23
|
+
name: TName
|
24
|
+
}
|
25
|
+
|
26
|
+
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
27
|
+
{} as FormFieldContextValue
|
28
|
+
)
|
29
|
+
|
30
|
+
const FormField = <
|
31
|
+
TFieldValues extends FieldValues = FieldValues,
|
32
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
33
|
+
>({
|
34
|
+
...props
|
35
|
+
}: ControllerProps<TFieldValues, TName>) => {
|
36
|
+
return (
|
37
|
+
<FormFieldContext.Provider value={{ name: props.name }}>
|
38
|
+
<Controller {...props} />
|
39
|
+
</FormFieldContext.Provider>
|
40
|
+
)
|
41
|
+
}
|
42
|
+
|
43
|
+
const useFormField = () => {
|
44
|
+
const fieldContext = React.useContext(FormFieldContext)
|
45
|
+
const itemContext = React.useContext(FormItemContext)
|
46
|
+
const { getFieldState } = useFormContext()
|
47
|
+
const formState = useFormState({ name: fieldContext.name })
|
48
|
+
const fieldState = getFieldState(fieldContext.name, formState)
|
49
|
+
|
50
|
+
if (!fieldContext) {
|
51
|
+
throw new Error("useFormField should be used within <FormField>")
|
52
|
+
}
|
53
|
+
|
54
|
+
const { id } = itemContext
|
55
|
+
|
56
|
+
return {
|
57
|
+
id,
|
58
|
+
name: fieldContext.name,
|
59
|
+
formItemId: `${id}-form-item`,
|
60
|
+
formDescriptionId: `${id}-form-item-description`,
|
61
|
+
formMessageId: `${id}-form-item-message`,
|
62
|
+
...fieldState,
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
type FormItemContextValue = {
|
67
|
+
id: string
|
68
|
+
}
|
69
|
+
|
70
|
+
const FormItemContext = React.createContext<FormItemContextValue>(
|
71
|
+
{} as FormItemContextValue
|
72
|
+
)
|
73
|
+
|
74
|
+
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
75
|
+
const id = React.useId()
|
76
|
+
|
77
|
+
return (
|
78
|
+
<FormItemContext.Provider value={{ id }}>
|
79
|
+
<div
|
80
|
+
data-slot="form-item"
|
81
|
+
className={cn("grid gap-2", className)}
|
82
|
+
{...props}
|
83
|
+
/>
|
84
|
+
</FormItemContext.Provider>
|
85
|
+
)
|
86
|
+
}
|
87
|
+
|
88
|
+
function FormLabel({
|
89
|
+
className,
|
90
|
+
...props
|
91
|
+
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
92
|
+
const { error, formItemId } = useFormField()
|
93
|
+
|
94
|
+
return (
|
95
|
+
<Label
|
96
|
+
data-slot="form-label"
|
97
|
+
data-error={!!error}
|
98
|
+
className={cn("data-[error=true]:text-destructive", className)}
|
99
|
+
htmlFor={formItemId}
|
100
|
+
{...props}
|
101
|
+
/>
|
102
|
+
)
|
103
|
+
}
|
104
|
+
|
105
|
+
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
106
|
+
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
107
|
+
|
108
|
+
return (
|
109
|
+
<Slot
|
110
|
+
data-slot="form-control"
|
111
|
+
id={formItemId}
|
112
|
+
aria-describedby={
|
113
|
+
!error
|
114
|
+
? `${formDescriptionId}`
|
115
|
+
: `${formDescriptionId} ${formMessageId}`
|
116
|
+
}
|
117
|
+
aria-invalid={!!error}
|
118
|
+
{...props}
|
119
|
+
/>
|
120
|
+
)
|
121
|
+
}
|
122
|
+
|
123
|
+
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
124
|
+
const { formDescriptionId } = useFormField()
|
125
|
+
|
126
|
+
return (
|
127
|
+
<p
|
128
|
+
data-slot="form-description"
|
129
|
+
id={formDescriptionId}
|
130
|
+
className={cn("text-muted-foreground text-sm", className)}
|
131
|
+
{...props}
|
132
|
+
/>
|
133
|
+
)
|
134
|
+
}
|
135
|
+
|
136
|
+
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
137
|
+
const { error, formMessageId } = useFormField()
|
138
|
+
const body = error ? String(error?.message ?? "") : props.children
|
139
|
+
|
140
|
+
if (!body) {
|
141
|
+
return null
|
142
|
+
}
|
143
|
+
|
144
|
+
return (
|
145
|
+
<p
|
146
|
+
data-slot="form-message"
|
147
|
+
id={formMessageId}
|
148
|
+
className={cn("text-destructive text-sm", className)}
|
149
|
+
{...props}
|
150
|
+
>
|
151
|
+
{body}
|
152
|
+
</p>
|
153
|
+
)
|
154
|
+
}
|
155
|
+
|
156
|
+
export {
|
157
|
+
useFormField,
|
158
|
+
Form,
|
159
|
+
FormItem,
|
160
|
+
FormLabel,
|
161
|
+
FormControl,
|
162
|
+
FormDescription,
|
163
|
+
FormMessage,
|
164
|
+
FormField,
|
165
|
+
}
|
@@ -0,0 +1,86 @@
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
2
|
+
import { FrostedPanel } from "./frosted-panel";
|
3
|
+
|
4
|
+
const meta: Meta<typeof FrostedPanel> = {
|
5
|
+
title: "UI/FrostedPanel",
|
6
|
+
component: FrostedPanel,
|
7
|
+
parameters: {
|
8
|
+
layout: "fullscreen",
|
9
|
+
},
|
10
|
+
tags: ["autodocs"],
|
11
|
+
};
|
12
|
+
|
13
|
+
export default meta;
|
14
|
+
type Story = StoryObj<typeof FrostedPanel>;
|
15
|
+
|
16
|
+
export const Default: Story = {
|
17
|
+
args: {
|
18
|
+
children: (
|
19
|
+
<>
|
20
|
+
<FrostedPanel.Aside>
|
21
|
+
<div className="p-4 text-white">Aside Content</div>
|
22
|
+
</FrostedPanel.Aside>
|
23
|
+
<FrostedPanel.Main>
|
24
|
+
<div className="p-4">Main Content</div>
|
25
|
+
</FrostedPanel.Main>
|
26
|
+
</>
|
27
|
+
),
|
28
|
+
},
|
29
|
+
};
|
30
|
+
|
31
|
+
export const CustomGradient: Story = {
|
32
|
+
args: {
|
33
|
+
gradientColors: {
|
34
|
+
top: "#1a237e",
|
35
|
+
bottom: "#0d47a1",
|
36
|
+
},
|
37
|
+
children: (
|
38
|
+
<>
|
39
|
+
<FrostedPanel.Aside>
|
40
|
+
<div className="p-4 text-white">Aside Content</div>
|
41
|
+
</FrostedPanel.Aside>
|
42
|
+
<FrostedPanel.Main>
|
43
|
+
<div className="p-4">Main Content</div>
|
44
|
+
</FrostedPanel.Main>
|
45
|
+
</>
|
46
|
+
),
|
47
|
+
},
|
48
|
+
};
|
49
|
+
|
50
|
+
export const CustomBlobConfig: Story = {
|
51
|
+
args: {
|
52
|
+
blobConfig: {
|
53
|
+
count: 6,
|
54
|
+
color: "rgba(255, 255, 255, 0.6)",
|
55
|
+
minSize: 150,
|
56
|
+
maxSize: 300,
|
57
|
+
speed: 5,
|
58
|
+
},
|
59
|
+
children: (
|
60
|
+
<>
|
61
|
+
<FrostedPanel.Aside>
|
62
|
+
<div className="p-4 text-white">Aside Content</div>
|
63
|
+
</FrostedPanel.Aside>
|
64
|
+
<FrostedPanel.Main>
|
65
|
+
<div className="p-4">Main Content</div>
|
66
|
+
</FrostedPanel.Main>
|
67
|
+
</>
|
68
|
+
),
|
69
|
+
},
|
70
|
+
};
|
71
|
+
|
72
|
+
export const CustomAsideWidth: Story = {
|
73
|
+
args: {
|
74
|
+
asideWidth: "40%",
|
75
|
+
children: (
|
76
|
+
<>
|
77
|
+
<FrostedPanel.Aside>
|
78
|
+
<div className="p-4 text-white">Wider Aside Content</div>
|
79
|
+
</FrostedPanel.Aside>
|
80
|
+
<FrostedPanel.Main>
|
81
|
+
<div className="p-4">Main Content</div>
|
82
|
+
</FrostedPanel.Main>
|
83
|
+
</>
|
84
|
+
),
|
85
|
+
},
|
86
|
+
};
|
@@ -0,0 +1,276 @@
|
|
1
|
+
"use client";
|
2
|
+
|
3
|
+
import React, { useEffect, useRef, createContext, useContext, useState } from "react";
|
4
|
+
import { cn } from "@/lib/utils";
|
5
|
+
import logo from "../../assets/brand/saafe-color-white-logo.svg";
|
6
|
+
import { LanguageSwitcher } from "../language/LanguageSwitcher";
|
7
|
+
import { SessionTimer } from "../session/SessionTimer";
|
8
|
+
import { SessionTimeoutScreen } from "../session/SessionTimeoutScreen";
|
9
|
+
import { useRedirectStore } from "@/store/redirect.store";
|
10
|
+
import { useAuthStore } from "@/store/auth.store";
|
11
|
+
import { useTrustedCount } from "@/hooks/use-trusted-count";
|
12
|
+
import { VersionDisplay } from "./version-display";
|
13
|
+
|
14
|
+
// Create context for FrostedPanel configuration
|
15
|
+
type FrostedPanelContextType = {
|
16
|
+
asideWidth?: string | number;
|
17
|
+
asideRatio?: string;
|
18
|
+
gradientColors?: {
|
19
|
+
top: string;
|
20
|
+
bottom: string;
|
21
|
+
};
|
22
|
+
blobConfig?: {
|
23
|
+
count: number;
|
24
|
+
color: string;
|
25
|
+
minSize: number;
|
26
|
+
maxSize: number;
|
27
|
+
speed: number;
|
28
|
+
};
|
29
|
+
};
|
30
|
+
|
31
|
+
const FrostedPanelContext = createContext<FrostedPanelContextType>({
|
32
|
+
asideWidth: "30%",
|
33
|
+
asideRatio: "auto",
|
34
|
+
gradientColors: {
|
35
|
+
top: "#004d4d",
|
36
|
+
bottom: "#00b2c1",
|
37
|
+
},
|
38
|
+
blobConfig: {
|
39
|
+
count: 3,
|
40
|
+
color: "rgba(255, 255, 255, 0.4)",
|
41
|
+
minSize: 100,
|
42
|
+
maxSize: 250,
|
43
|
+
speed: 0.8,
|
44
|
+
},
|
45
|
+
});
|
46
|
+
|
47
|
+
interface FrostedPanelProps extends React.HTMLAttributes<HTMLDivElement> {
|
48
|
+
asideWidth?: string | number;
|
49
|
+
asideRatio?: string;
|
50
|
+
gradientColors?: {
|
51
|
+
top: string;
|
52
|
+
bottom: string;
|
53
|
+
};
|
54
|
+
blobConfig?: {
|
55
|
+
count: number;
|
56
|
+
color: string;
|
57
|
+
minSize: number;
|
58
|
+
maxSize: number;
|
59
|
+
speed: number;
|
60
|
+
};
|
61
|
+
}
|
62
|
+
|
63
|
+
function FrostedPanel({
|
64
|
+
children,
|
65
|
+
className,
|
66
|
+
asideWidth = "30%",
|
67
|
+
asideRatio = "auto",
|
68
|
+
gradientColors = {
|
69
|
+
top: "#004d4d",
|
70
|
+
bottom: "#00b2c1",
|
71
|
+
},
|
72
|
+
blobConfig = {
|
73
|
+
count: 4,
|
74
|
+
color: "rgba(255, 255, 255, 0.4)",
|
75
|
+
minSize: 100,
|
76
|
+
maxSize: 250,
|
77
|
+
speed: 10,
|
78
|
+
},
|
79
|
+
...props
|
80
|
+
}: FrostedPanelProps) {
|
81
|
+
const contextValue = {
|
82
|
+
asideWidth,
|
83
|
+
asideRatio,
|
84
|
+
gradientColors,
|
85
|
+
blobConfig,
|
86
|
+
};
|
87
|
+
|
88
|
+
return (
|
89
|
+
<FrostedPanelContext.Provider value={contextValue}>
|
90
|
+
<div className={cn("flex w-full h-screen overflow-hidden", className)} {...props}>
|
91
|
+
{children}
|
92
|
+
</div>
|
93
|
+
</FrostedPanelContext.Provider>
|
94
|
+
);
|
95
|
+
}
|
96
|
+
|
97
|
+
interface AsideProps extends React.HTMLAttributes<HTMLDivElement> {
|
98
|
+
width?: string | number;
|
99
|
+
ratio?: string;
|
100
|
+
}
|
101
|
+
|
102
|
+
function Aside({ children, className, width, ratio, ...props }: AsideProps) {
|
103
|
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
104
|
+
const animationRef = useRef<number | undefined>(undefined);
|
105
|
+
const currentPath = location.pathname;
|
106
|
+
const isLoginPage = currentPath?.split('/')?.includes('login');
|
107
|
+
const blobsRef = useRef<Array<{
|
108
|
+
x: number;
|
109
|
+
y: number;
|
110
|
+
radius: number;
|
111
|
+
xSpeed: number;
|
112
|
+
ySpeed: number;
|
113
|
+
opacity: number;
|
114
|
+
}>>([]);
|
115
|
+
const [isSessionExpired, setIsSessionExpired] = useState(false);
|
116
|
+
const { getCustomStyle } = useRedirectStore();
|
117
|
+
const customStyles = getCustomStyle();
|
118
|
+
const context = useContext(FrostedPanelContext);
|
119
|
+
const { isAuthenticated } = useAuthStore();
|
120
|
+
const { data: trustedCount, isLoading } = useTrustedCount();
|
121
|
+
console.log(trustedCount);
|
122
|
+
|
123
|
+
|
124
|
+
// Handle session timeout - only called when timer actually reaches zero
|
125
|
+
const handleTimeout = () => {
|
126
|
+
setIsSessionExpired(true);
|
127
|
+
};
|
128
|
+
|
129
|
+
useEffect(() => {
|
130
|
+
const canvas = canvasRef.current;
|
131
|
+
if (!canvas) return;
|
132
|
+
|
133
|
+
const ctx = canvas.getContext("2d");
|
134
|
+
if (!ctx) return;
|
135
|
+
|
136
|
+
// Initialize blobs only once
|
137
|
+
if (blobsRef.current.length === 0) {
|
138
|
+
const blobConfig = context.blobConfig;
|
139
|
+
for (let i = 0; i < (blobConfig?.count || 3); i++) {
|
140
|
+
blobsRef.current.push({
|
141
|
+
x: Math.random() * canvas.width,
|
142
|
+
y: Math.random() * canvas.height,
|
143
|
+
radius: blobConfig?.minSize || 100 + Math.random() * ((blobConfig?.maxSize || 250) - (blobConfig?.minSize || 100)),
|
144
|
+
xSpeed: (Math.random() - 0.5) * (blobConfig?.speed || 0.8),
|
145
|
+
ySpeed: (Math.random() - 0.5) * (blobConfig?.speed || 0.8),
|
146
|
+
opacity: 0.4 + Math.random() * 0.4,
|
147
|
+
});
|
148
|
+
}
|
149
|
+
}
|
150
|
+
|
151
|
+
const resizeCanvas = () => {
|
152
|
+
const parent = canvas.parentElement;
|
153
|
+
if (!parent) return;
|
154
|
+
canvas.width = parent.offsetWidth;
|
155
|
+
canvas.height = parent.offsetHeight;
|
156
|
+
};
|
157
|
+
|
158
|
+
resizeCanvas();
|
159
|
+
window.addEventListener("resize", resizeCanvas);
|
160
|
+
|
161
|
+
const animate = () => {
|
162
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
163
|
+
blobsRef.current.forEach((blob) => {
|
164
|
+
blob.x += blob.xSpeed;
|
165
|
+
blob.y += blob.ySpeed;
|
166
|
+
|
167
|
+
if (blob.x < -blob.radius || blob.x > canvas.width + blob.radius) {
|
168
|
+
blob.xSpeed = -blob.xSpeed;
|
169
|
+
}
|
170
|
+
if (blob.y < -blob.radius || blob.y > canvas.height + blob.radius) {
|
171
|
+
blob.ySpeed = -blob.ySpeed;
|
172
|
+
}
|
173
|
+
|
174
|
+
ctx.beginPath();
|
175
|
+
ctx.arc(blob.x, blob.y, blob.radius, 0, Math.PI * 2);
|
176
|
+
ctx.fillStyle = context.blobConfig?.color
|
177
|
+
.replace(")", `, ${blob.opacity})`)
|
178
|
+
.replace("rgba", "rgba")
|
179
|
+
.replace("rgb", "rgba") || "";
|
180
|
+
ctx.fill();
|
181
|
+
});
|
182
|
+
|
183
|
+
animationRef.current = requestAnimationFrame(animate);
|
184
|
+
};
|
185
|
+
|
186
|
+
animate();
|
187
|
+
|
188
|
+
return () => {
|
189
|
+
window.removeEventListener("resize", resizeCanvas);
|
190
|
+
if (animationRef.current) {
|
191
|
+
cancelAnimationFrame(animationRef.current);
|
192
|
+
}
|
193
|
+
};
|
194
|
+
}, []); // Empty dependency array to run only once
|
195
|
+
|
196
|
+
if (isSessionExpired) {
|
197
|
+
return <SessionTimeoutScreen />;
|
198
|
+
}
|
199
|
+
|
200
|
+
return (
|
201
|
+
<div
|
202
|
+
className={cn(
|
203
|
+
"relative flex flex-col justify-center overflow-y-auto [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden",
|
204
|
+
className
|
205
|
+
)}
|
206
|
+
style={{
|
207
|
+
width: width || context.asideWidth,
|
208
|
+
aspectRatio: ratio || context.asideRatio,
|
209
|
+
background: context.gradientColors
|
210
|
+
? `linear-gradient(to bottom, ${customStyles?.primaryButtonColor || context.gradientColors.top}, ${customStyles?.primaryButtonColor ? `${customStyles?.primaryButtonColor}95` : context.gradientColors.bottom})`
|
211
|
+
: undefined,
|
212
|
+
}}
|
213
|
+
{...props}
|
214
|
+
>
|
215
|
+
<canvas ref={canvasRef} className="absolute inset-0 z-10 w-full h-full" />
|
216
|
+
<div className="absolute inset-0 backdrop-blur-[140px] bg-white/10 z-20" />
|
217
|
+
<div className="z-50 mx-6 mt-4">
|
218
|
+
{isLoginPage ? (
|
219
|
+
<LanguageSwitcher />
|
220
|
+
) : (
|
221
|
+
<SessionTimer onTimeout={handleTimeout} />
|
222
|
+
)}
|
223
|
+
</div>
|
224
|
+
<div className="relative z-30 p-6 flex-1 flex flex-col overflow-y-auto max-h-screen [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden">
|
225
|
+
<div className="pb-8">
|
226
|
+
{children}
|
227
|
+
</div>
|
228
|
+
</div>
|
229
|
+
<div className="z-50 ml-4 mt-4">
|
230
|
+
<div className="flex justify-start mb-4">
|
231
|
+
<VersionDisplay variant="minimal"showIcon={true} />
|
232
|
+
</div>
|
233
|
+
<div className="w-80 h-30 bg-black/15 rounded-tr-full rounded-br-4xl absolute left-0 bottom-0 blur-2xl -z-10" />
|
234
|
+
{isAuthenticated ?
|
235
|
+
<>
|
236
|
+
<div className="text-white text-lg flex items-center gap-2 z-auto">
|
237
|
+
<p>Powered by</p>
|
238
|
+
<div className="flex flex-row gap-1 items-end">
|
239
|
+
<img src={logo} alt="Saafe Logo" className="w-20 h-auto mb-1" />
|
240
|
+
</div>
|
241
|
+
</div>
|
242
|
+
{!isLoading && trustedCount?.displayText && (
|
243
|
+
<p className="text-surface font-light text-md mt-0.5">{trustedCount.displayText}</p>
|
244
|
+
)}
|
245
|
+
</>
|
246
|
+
:
|
247
|
+
<>
|
248
|
+
<div className="text-surface text-sm flex items-center gap-2 z-50">
|
249
|
+
<p>Powered by RBI regulated AA</p>
|
250
|
+
</div>
|
251
|
+
<div className="flex flex-row gap-1 items-end">
|
252
|
+
<img src={logo} alt="Saafe Logo" className="w-22 h-auto mt-0.5" />
|
253
|
+
</div>
|
254
|
+
</>
|
255
|
+
}
|
256
|
+
</div>
|
257
|
+
</div>
|
258
|
+
);
|
259
|
+
}
|
260
|
+
|
261
|
+
function Main({ children, className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
262
|
+
return (
|
263
|
+
<div
|
264
|
+
className={cn("flex-1 flex flex-col overflow-y-auto", className)}
|
265
|
+
{...props}
|
266
|
+
>
|
267
|
+
{children}
|
268
|
+
</div>
|
269
|
+
);
|
270
|
+
}
|
271
|
+
|
272
|
+
// Assign components to FrostedPanel
|
273
|
+
FrostedPanel.Aside = Aside;
|
274
|
+
FrostedPanel.Main = Main;
|
275
|
+
|
276
|
+
export { FrostedPanel };
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import * as React from "react"
|
2
|
+
import { cn } from "@/lib/utils"
|
3
|
+
|
4
|
+
interface InputProps extends React.ComponentProps<"input"> {
|
5
|
+
leftSection?: React.ReactNode;
|
6
|
+
rightSection?: React.ReactNode;
|
7
|
+
}
|
8
|
+
|
9
|
+
function Input({ className, type, leftSection, rightSection, ...props }: InputProps) {
|
10
|
+
return (
|
11
|
+
<div className="relative w-full">
|
12
|
+
{leftSection && (
|
13
|
+
<div className="absolute left-3 top-1/2 -translate-y-1/2 z-10">
|
14
|
+
{leftSection}
|
15
|
+
</div>
|
16
|
+
)}
|
17
|
+
{rightSection && (
|
18
|
+
<div className="absolute right-3 top-1/2 -translate-y-1/2 z-10">
|
19
|
+
{rightSection}
|
20
|
+
</div>
|
21
|
+
)}
|
22
|
+
<input
|
23
|
+
type={type}
|
24
|
+
data-slot="input"
|
25
|
+
className={cn(
|
26
|
+
"dark:text-white file:text-consent-primary placeholder:text-consent-secondary selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-md",
|
27
|
+
"focus-visible:border-primary focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
28
|
+
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
29
|
+
leftSection && "pl-10",
|
30
|
+
rightSection && "pr-10",
|
31
|
+
className
|
32
|
+
)}
|
33
|
+
{...props}
|
34
|
+
/>
|
35
|
+
</div>
|
36
|
+
)
|
37
|
+
}
|
38
|
+
|
39
|
+
export { Input }
|
@@ -0,0 +1,67 @@
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
2
|
+
import { Label } from "./label";
|
3
|
+
|
4
|
+
const meta: Meta<typeof Label> = {
|
5
|
+
title: "UI/Label",
|
6
|
+
component: Label,
|
7
|
+
parameters: {
|
8
|
+
layout: "padded",
|
9
|
+
},
|
10
|
+
tags: ["autodocs"],
|
11
|
+
};
|
12
|
+
|
13
|
+
export default meta;
|
14
|
+
type Story = StoryObj<typeof Label>;
|
15
|
+
|
16
|
+
export const Default: Story = {
|
17
|
+
args: {
|
18
|
+
children: "Default Label",
|
19
|
+
},
|
20
|
+
};
|
21
|
+
|
22
|
+
export const WithHtmlFor: Story = {
|
23
|
+
args: {
|
24
|
+
htmlFor: "input-id",
|
25
|
+
children: "Label with htmlFor",
|
26
|
+
},
|
27
|
+
};
|
28
|
+
|
29
|
+
export const CustomClassName: Story = {
|
30
|
+
args: {
|
31
|
+
className: "text-lg text-primary",
|
32
|
+
children: "Custom Styled Label",
|
33
|
+
},
|
34
|
+
};
|
35
|
+
|
36
|
+
export const Disabled: Story = {
|
37
|
+
args: {
|
38
|
+
className: "group-data-[disabled=true]:opacity-50",
|
39
|
+
children: "Disabled Label",
|
40
|
+
},
|
41
|
+
};
|
42
|
+
|
43
|
+
export const WithIcon: Story = {
|
44
|
+
args: {
|
45
|
+
className: "flex items-center gap-2",
|
46
|
+
children: (
|
47
|
+
<>
|
48
|
+
<span>Label with Icon</span>
|
49
|
+
<svg
|
50
|
+
xmlns="http://www.w3.org/2000/svg"
|
51
|
+
width="16"
|
52
|
+
height="16"
|
53
|
+
viewBox="0 0 24 24"
|
54
|
+
fill="none"
|
55
|
+
stroke="currentColor"
|
56
|
+
strokeWidth="2"
|
57
|
+
strokeLinecap="round"
|
58
|
+
strokeLinejoin="round"
|
59
|
+
>
|
60
|
+
<circle cx="12" cy="12" r="10" />
|
61
|
+
<line x1="12" y1="8" x2="12" y2="12" />
|
62
|
+
<line x1="12" y1="16" x2="12.01" y2="16" />
|
63
|
+
</svg>
|
64
|
+
</>
|
65
|
+
),
|
66
|
+
},
|
67
|
+
};
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import * as React from "react"
|
2
|
+
import * as LabelPrimitive from "@radix-ui/react-label"
|
3
|
+
import { useRTL } from "@/contexts/RTLContext"
|
4
|
+
import { cn } from "@/lib/utils"
|
5
|
+
|
6
|
+
function Label({
|
7
|
+
className,
|
8
|
+
...props
|
9
|
+
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
10
|
+
const { isRTL } = useRTL();
|
11
|
+
return (
|
12
|
+
<LabelPrimitive.Root
|
13
|
+
data-slot="label"
|
14
|
+
className={cn(
|
15
|
+
`flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 ${isRTL ? 'flex-row-reverse' : ''}`,
|
16
|
+
className
|
17
|
+
)}
|
18
|
+
{...props}
|
19
|
+
/>
|
20
|
+
)
|
21
|
+
}
|
22
|
+
|
23
|
+
export { Label }
|