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,54 @@
|
|
1
|
+
import React from "react";
|
2
|
+
import { motion, AnimatePresence } from "framer-motion";
|
3
|
+
import { cn } from "@/lib/utils";
|
4
|
+
import Logo from "../../assets/brand/saafe-logo.svg"
|
5
|
+
import { useMediaQuery } from "@/hooks/use-media-query";
|
6
|
+
import { useTrustedCount } from "@/hooks/use-trusted-count";
|
7
|
+
import { VersionDisplay } from "./version-display";
|
8
|
+
|
9
|
+
interface MobileFooterProps {
|
10
|
+
children: React.ReactNode;
|
11
|
+
show: boolean;
|
12
|
+
className?: string;
|
13
|
+
}
|
14
|
+
|
15
|
+
export function MobileFooter({
|
16
|
+
children,
|
17
|
+
show,
|
18
|
+
className
|
19
|
+
}: MobileFooterProps) {
|
20
|
+
const isMobile = useMediaQuery("(max-width: 768px)");
|
21
|
+
const { data: trustedCount, isLoading } = useTrustedCount();
|
22
|
+
|
23
|
+
return (
|
24
|
+
<AnimatePresence>
|
25
|
+
{(show && isMobile) ? (
|
26
|
+
<motion.div
|
27
|
+
initial={{ y: 100, opacity: 0 }}
|
28
|
+
animate={{ y: 0, opacity: 1 }}
|
29
|
+
exit={{ y: 100, opacity: 0 }}
|
30
|
+
transition={{ duration: 0.3 }}
|
31
|
+
className={cn(
|
32
|
+
"fixed bottom-0 left-0 right-0 bg-white drop-shadow-[0_35px_35px_rgba(0,0,0,0.25)] rounded-t-2xl z-50 dark:bg-card",
|
33
|
+
className
|
34
|
+
)}
|
35
|
+
>
|
36
|
+
<div className="w-full pt-8 px-8">{children}</div>
|
37
|
+
<div className="flex-col justify-center items-center text-center py-3">
|
38
|
+
<p className="text-muted-secondary text-xs font-medium text-nowrap flex justify-center items-center gap-2 w-full">
|
39
|
+
Powered by <img src={Logo} alt="Saafe Logo" className="w-12 h-auto" />
|
40
|
+
</p>
|
41
|
+
{!isLoading && trustedCount?.displayText && (
|
42
|
+
<p className="text-muted-secondary text-xs font-light mt-1.5">
|
43
|
+
{trustedCount.displayText}
|
44
|
+
</p>
|
45
|
+
)}
|
46
|
+
<div className="flex justify-center">
|
47
|
+
<VersionDisplay variant="minimal" showIcon={false} className="text-muted-secondary" />
|
48
|
+
</div>
|
49
|
+
</div>
|
50
|
+
</motion.div>
|
51
|
+
) : null}
|
52
|
+
</AnimatePresence>
|
53
|
+
);
|
54
|
+
}
|
@@ -0,0 +1,90 @@
|
|
1
|
+
import React, { ReactNode } from "react";
|
2
|
+
import {
|
3
|
+
Dialog,
|
4
|
+
DialogClose,
|
5
|
+
DialogContent,
|
6
|
+
DialogDescription,
|
7
|
+
DialogFooter,
|
8
|
+
DialogHeader,
|
9
|
+
DialogTitle,
|
10
|
+
DialogTrigger,
|
11
|
+
} from "./dialog";
|
12
|
+
import { cn } from "@/lib/utils";
|
13
|
+
|
14
|
+
interface ModalProps {
|
15
|
+
title?: string | boolean;
|
16
|
+
description?: string;
|
17
|
+
children: ReactNode;
|
18
|
+
withCloseIcon?: boolean;
|
19
|
+
footer?: ReactNode;
|
20
|
+
open?: boolean;
|
21
|
+
onOpenChange?: (open: boolean) => void;
|
22
|
+
className?: string;
|
23
|
+
}
|
24
|
+
|
25
|
+
interface ModalTriggerProps {
|
26
|
+
children: ReactNode;
|
27
|
+
}
|
28
|
+
|
29
|
+
interface ModalFooterProps {
|
30
|
+
children: ReactNode;
|
31
|
+
}
|
32
|
+
|
33
|
+
interface ModalCloseProps {
|
34
|
+
children: ReactNode;
|
35
|
+
className?: string;
|
36
|
+
}
|
37
|
+
|
38
|
+
const Modal = ({ title, description, children, withCloseIcon = true, open, onOpenChange, className }: ModalProps) => {
|
39
|
+
let triggerElement: ReactNode = null;
|
40
|
+
let footerElement: ReactNode = null;
|
41
|
+
const contentElements: ReactNode[] = [];
|
42
|
+
|
43
|
+
// Separate trigger, footer, and content elements
|
44
|
+
React.Children.forEach(children, (child) => {
|
45
|
+
if (React.isValidElement(child)) {
|
46
|
+
if (child.type === Modal.Trigger) {
|
47
|
+
triggerElement = child;
|
48
|
+
} else if (child.type === Modal.Footer) {
|
49
|
+
footerElement = child;
|
50
|
+
} else {
|
51
|
+
contentElements.push(child);
|
52
|
+
}
|
53
|
+
}
|
54
|
+
});
|
55
|
+
|
56
|
+
return (
|
57
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
58
|
+
{triggerElement} {/* Renders only the trigger outside the modal */}
|
59
|
+
<DialogContent className={cn("md:max-w-[800px] sm:max-w-[425px]", className)} withCloseIcon={withCloseIcon}>
|
60
|
+
<DialogHeader>
|
61
|
+
{title && <DialogTitle className="dark:text-white">{title}</DialogTitle>}
|
62
|
+
{description && <DialogDescription>{description}</DialogDescription>}
|
63
|
+
</DialogHeader>
|
64
|
+
<div>{contentElements}</div> {/* Renders modal content */}
|
65
|
+
{footerElement}
|
66
|
+
</DialogContent>
|
67
|
+
</Dialog>
|
68
|
+
);
|
69
|
+
};
|
70
|
+
|
71
|
+
const ModalTrigger = ({ children }: ModalTriggerProps) => {
|
72
|
+
if (!children) return null;
|
73
|
+
return <DialogTrigger asChild>{children}</DialogTrigger>;
|
74
|
+
};
|
75
|
+
|
76
|
+
const ModalFooter = ({ children }: ModalFooterProps) => {
|
77
|
+
if (!children) return null;
|
78
|
+
return <DialogFooter>{children}</DialogFooter>;
|
79
|
+
};
|
80
|
+
|
81
|
+
const ModalClose = ({ children, className }: ModalCloseProps) => {
|
82
|
+
if (!children) return null;
|
83
|
+
return <DialogClose className={className}>{children}</DialogClose>;
|
84
|
+
};
|
85
|
+
|
86
|
+
Modal.Trigger = ModalTrigger;
|
87
|
+
Modal.Footer = ModalFooter;
|
88
|
+
Modal.Close = ModalClose;
|
89
|
+
|
90
|
+
export default Modal;
|
@@ -0,0 +1,62 @@
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
2
|
+
import { OTPInputComponent } from "./otp-input";
|
3
|
+
|
4
|
+
const meta: Meta<typeof OTPInputComponent> = {
|
5
|
+
title: "UI/OTPInput",
|
6
|
+
component: OTPInputComponent,
|
7
|
+
parameters: {
|
8
|
+
layout: "centered",
|
9
|
+
},
|
10
|
+
tags: ["autodocs"],
|
11
|
+
};
|
12
|
+
|
13
|
+
export default meta;
|
14
|
+
type Story = StoryObj<typeof OTPInputComponent>;
|
15
|
+
|
16
|
+
export const Default: Story = {
|
17
|
+
args: {
|
18
|
+
title: "Enter OTP",
|
19
|
+
mobileNumber: "+91 9876543210",
|
20
|
+
onResend: () => alert("Resend clicked"),
|
21
|
+
countdownTime: 30,
|
22
|
+
},
|
23
|
+
};
|
24
|
+
|
25
|
+
export const WithError: Story = {
|
26
|
+
args: {
|
27
|
+
title: "Enter OTP",
|
28
|
+
mobileNumber: "+91 9876543210",
|
29
|
+
onResend: () => alert("Resend clicked"),
|
30
|
+
countdownTime: 30,
|
31
|
+
error: true,
|
32
|
+
errorMessage: "Invalid OTP. Please try again.",
|
33
|
+
},
|
34
|
+
};
|
35
|
+
|
36
|
+
export const Disabled: Story = {
|
37
|
+
args: {
|
38
|
+
title: "Enter OTP",
|
39
|
+
mobileNumber: "+91 9876543210",
|
40
|
+
onResend: () => alert("Resend clicked"),
|
41
|
+
countdownTime: 30,
|
42
|
+
disabled: true,
|
43
|
+
},
|
44
|
+
};
|
45
|
+
|
46
|
+
export const CustomTitle: Story = {
|
47
|
+
args: {
|
48
|
+
title: "Verify Your Phone Number",
|
49
|
+
mobileNumber: "+91 9876543210",
|
50
|
+
onResend: () => alert("Resend clicked"),
|
51
|
+
countdownTime: 30,
|
52
|
+
},
|
53
|
+
};
|
54
|
+
|
55
|
+
export const ShortCountdown: Story = {
|
56
|
+
args: {
|
57
|
+
title: "Enter OTP",
|
58
|
+
mobileNumber: "+91 9876543210",
|
59
|
+
onResend: () => alert("Resend clicked"),
|
60
|
+
countdownTime: 5,
|
61
|
+
},
|
62
|
+
};
|
@@ -0,0 +1,221 @@
|
|
1
|
+
"use client";
|
2
|
+
|
3
|
+
import { cn } from "@/lib/utils";
|
4
|
+
import { Label } from "@/components/ui/label";
|
5
|
+
import { OTPInput, SlotProps } from "input-otp";
|
6
|
+
import { useId, useState, useEffect, forwardRef } from "react";
|
7
|
+
import { useTranslation } from "react-i18next";
|
8
|
+
import { Eye, EyeOff } from "lucide-react";
|
9
|
+
import { Button } from "./button";
|
10
|
+
|
11
|
+
interface OTPInputComponentProps {
|
12
|
+
title?: string;
|
13
|
+
mobileNumber?: string;
|
14
|
+
onResend?: () => void;
|
15
|
+
countdownTime?: number;
|
16
|
+
maxLength?: number;
|
17
|
+
errorMessage?: string;
|
18
|
+
error?: boolean;
|
19
|
+
disabled?: boolean;
|
20
|
+
handleEdit?: () => void;
|
21
|
+
value?: string;
|
22
|
+
onChange?: (value: string) => void;
|
23
|
+
handleLogin?: () => void;
|
24
|
+
name?: string;
|
25
|
+
editable?: boolean;
|
26
|
+
autoFocus?: boolean;
|
27
|
+
resendLoading?: boolean
|
28
|
+
}
|
29
|
+
|
30
|
+
// Extend SlotProps with our custom isError property
|
31
|
+
interface ExtendedSlotProps extends SlotProps {
|
32
|
+
isError?: boolean;
|
33
|
+
isVisible?: boolean;
|
34
|
+
onKeyDown?: (e: React.KeyboardEvent) => void;
|
35
|
+
}
|
36
|
+
|
37
|
+
const OTPInputComponent = forwardRef<HTMLDivElement, OTPInputComponentProps>(({
|
38
|
+
title = "Sign up for Saafe",
|
39
|
+
mobileNumber = "+91 1234567890",
|
40
|
+
onResend = () => { },
|
41
|
+
countdownTime = 10,
|
42
|
+
maxLength = 6,
|
43
|
+
editable = false,
|
44
|
+
errorMessage = "",
|
45
|
+
error = false,
|
46
|
+
disabled = false,
|
47
|
+
handleEdit = () => { },
|
48
|
+
resendLoading = false,
|
49
|
+
handleLogin = () => { },
|
50
|
+
value = "",
|
51
|
+
onChange,
|
52
|
+
name,
|
53
|
+
autoFocus = true
|
54
|
+
}, ref) => {
|
55
|
+
|
56
|
+
const id = useId();
|
57
|
+
const [countdown, setCountdown] = useState(countdownTime);
|
58
|
+
const [otpValue, setOtpValue] = useState(value);
|
59
|
+
const [otpLoadingObj, setOTPLoadingObj] = useState({ loading: false, isResent: false });
|
60
|
+
const [isOtpVisible, setIsOtpVisible] = useState(false);
|
61
|
+
const { t } = useTranslation();
|
62
|
+
|
63
|
+
// Handle OTP value changes
|
64
|
+
const handleOtpChange = (newValue: string) => {
|
65
|
+
// Only allow numeric values
|
66
|
+
const numericValue = newValue.replace(/\D/g, '');
|
67
|
+
setOtpValue(numericValue);
|
68
|
+
if (onChange) {
|
69
|
+
onChange(numericValue);
|
70
|
+
}
|
71
|
+
};
|
72
|
+
|
73
|
+
useEffect(() => {
|
74
|
+
const intervalId = setInterval(() => {
|
75
|
+
if (countdown > 0) {
|
76
|
+
setCountdown(countdown - 1);
|
77
|
+
}
|
78
|
+
}, 1000);
|
79
|
+
return () => clearInterval(intervalId);
|
80
|
+
}, [countdown]);
|
81
|
+
|
82
|
+
// Update internal state when value prop changes
|
83
|
+
useEffect(() => {
|
84
|
+
setOtpValue(value);
|
85
|
+
}, [value]);
|
86
|
+
|
87
|
+
const handleResend = () => {
|
88
|
+
setTimeout(() => {
|
89
|
+
setOTPLoadingObj({ loading: false, isResent: true })
|
90
|
+
setCountdown(countdownTime);
|
91
|
+
}, 1000)
|
92
|
+
if (onResend) onResend();
|
93
|
+
};
|
94
|
+
|
95
|
+
const toggleOtpVisibility = () => {
|
96
|
+
setIsOtpVisible(!isOtpVisible);
|
97
|
+
};
|
98
|
+
|
99
|
+
return (
|
100
|
+
<div ref={ref}>
|
101
|
+
<div className="flex flex-col items-center space-y-2 min-w-[300px]">
|
102
|
+
<h1 className="text-2xl font-semibold dark:text-white">{title}</h1>
|
103
|
+
<Label htmlFor={id} className="font-normal mt-3 text-muted-secondary">
|
104
|
+
<p className="leading-5">
|
105
|
+
{t('login.enterOtp')} {t('login.sentTo')}{" "}
|
106
|
+
<span className="font-semibold">+91 {mobileNumber}</span> {" "}
|
107
|
+
{editable && <span className="text-foreground decoration-black cursor-pointer underline" onClick={handleEdit}>{t('common.edit')}</span>}
|
108
|
+
</p>
|
109
|
+
</Label>
|
110
|
+
<div className="relative w-full flex flex-col items-center">
|
111
|
+
<OTPInput
|
112
|
+
id={id}
|
113
|
+
containerClassName={cn(
|
114
|
+
"flex items-center gap-3 has-[:disabled]:opacity-50 mt-2",
|
115
|
+
{ "border-red-500": error, "opacity-50": disabled }
|
116
|
+
)}
|
117
|
+
onKeyDown={(e) => {
|
118
|
+
if (e.key === 'Enter') {
|
119
|
+
handleLogin(); // trigger login explicitly
|
120
|
+
}
|
121
|
+
}}
|
122
|
+
maxLength={maxLength}
|
123
|
+
value={otpValue}
|
124
|
+
onChange={handleOtpChange}
|
125
|
+
name={name}
|
126
|
+
render={({ slots }) => (
|
127
|
+
<div className="flex gap-2">
|
128
|
+
{slots.map((slot, idx) => (
|
129
|
+
<Slot
|
130
|
+
key={idx}
|
131
|
+
{...slot}
|
132
|
+
isError={error}
|
133
|
+
isVisible={isOtpVisible}
|
134
|
+
onKeyDown={(e) => {
|
135
|
+
if (e.key === 'Enter') {
|
136
|
+
e.preventDefault();
|
137
|
+
handleLogin(); // trigger login only
|
138
|
+
}
|
139
|
+
}}
|
140
|
+
/>
|
141
|
+
))}
|
142
|
+
</div>
|
143
|
+
)}
|
144
|
+
disabled={disabled}
|
145
|
+
autoFocus={autoFocus}
|
146
|
+
/>
|
147
|
+
<Button
|
148
|
+
type="button"
|
149
|
+
variant="link"
|
150
|
+
size="icon"
|
151
|
+
className="absolute top-1/2 -translate-y-1/2 -translate-x-1/2 right-0 sm:right-2 z-10 mt-1"
|
152
|
+
onClick={toggleOtpVisibility}
|
153
|
+
>
|
154
|
+
{isOtpVisible ? (
|
155
|
+
<EyeOff className="h-4 w-4" />
|
156
|
+
) : (
|
157
|
+
<Eye className="h-4 w-4" />
|
158
|
+
)}
|
159
|
+
<span className="sr-only">
|
160
|
+
{isOtpVisible ? "Hide OTP" : "Show OTP"}
|
161
|
+
</span>
|
162
|
+
</Button>
|
163
|
+
</div>
|
164
|
+
</div>
|
165
|
+
<div className="flex flex-col mt-1 justify-center items-center">
|
166
|
+
{errorMessage && (
|
167
|
+
<p className="text-danger text-sm mt-1 w-[216px]">
|
168
|
+
{errorMessage}
|
169
|
+
</p>
|
170
|
+
)}
|
171
|
+
<p className="text-sm mt-1 text-muted-secondary w-[216px]">
|
172
|
+
{countdown > 0 && !otpLoadingObj?.isResent ? t('login.resendCodeIn') : null}
|
173
|
+
{countdown > 0 && otpLoadingObj?.isResent ? t('login.resendCodeInNew') : null}<span>{countdown > 0 ? `${String(Math.floor(countdown / 60)).padStart(2, '0')}:${String(countdown % 60).padStart(2, '0')}s` : null}</span>
|
174
|
+
{(countdown === 0 && !otpLoadingObj?.loading) && (
|
175
|
+
<button
|
176
|
+
className={cn("font-medium text-primary cursor-pointer", {
|
177
|
+
"opacity-50": disabled,
|
178
|
+
})}
|
179
|
+
disabled={disabled}
|
180
|
+
type='button'
|
181
|
+
onClick={() => {
|
182
|
+
setOTPLoadingObj({ loading: true, isResent: true });
|
183
|
+
handleResend();
|
184
|
+
}}
|
185
|
+
>
|
186
|
+
{t('login.resend')}
|
187
|
+
</button>
|
188
|
+
)}
|
189
|
+
{(resendLoading || otpLoadingObj?.loading) ? t("login.resending") : null}
|
190
|
+
</p>
|
191
|
+
</div>
|
192
|
+
</div>
|
193
|
+
);
|
194
|
+
});
|
195
|
+
|
196
|
+
OTPInputComponent.displayName = "OTPInputComponent";
|
197
|
+
|
198
|
+
// Custom Slot component with isError prop
|
199
|
+
function Slot(props: ExtendedSlotProps) {
|
200
|
+
return (
|
201
|
+
<div
|
202
|
+
className={cn(
|
203
|
+
"flex size-9 items-center justify-center rounded-lg border border-input bg-background font-medium text-foreground shadow-sm shadow-primary/5 transition-shadow w-12 h-12",
|
204
|
+
{
|
205
|
+
"z-10 border ring-[3px] ring-ring border-primary/40":
|
206
|
+
props.isActive && !props.isError,
|
207
|
+
"z-10 border border-danger ring-[3px] ring-red-200/50":
|
208
|
+
props.isActive && props.isError,
|
209
|
+
},
|
210
|
+
{ "border-danger": props.isError },
|
211
|
+
)}
|
212
|
+
onKeyDown={props.onKeyDown}
|
213
|
+
>
|
214
|
+
{props.char !== null && (
|
215
|
+
<div>{props.isVisible ? props.char : "•"}</div>
|
216
|
+
)}
|
217
|
+
</div>
|
218
|
+
);
|
219
|
+
}
|
220
|
+
|
221
|
+
export { OTPInputComponent };
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import { usePlatform } from "@/hooks/use-platform";
|
2
|
+
import { useTheme } from "@/contexts/ThemeContext";
|
3
|
+
import { cn } from "@/utils/cn";
|
4
|
+
|
5
|
+
interface PlatformSpecificBehaviorProps {
|
6
|
+
className?: string;
|
7
|
+
}
|
8
|
+
|
9
|
+
export function PlatformSpecificBehavior({ className }: PlatformSpecificBehaviorProps) {
|
10
|
+
const { platform, isMobile, isNativeSDK } = usePlatform();
|
11
|
+
const { theme } = useTheme();
|
12
|
+
|
13
|
+
return (
|
14
|
+
<div className={cn("p-4 rounded-lg border", className)}>
|
15
|
+
<div className="text-sm font-medium">
|
16
|
+
<p>Current Platform: <span className="font-bold">{platform}</span></p>
|
17
|
+
<p>Theme: <span className="font-bold">{theme}</span></p>
|
18
|
+
|
19
|
+
{isNativeSDK && (
|
20
|
+
<div className="mt-2 p-2 bg-primary/10 rounded">
|
21
|
+
<p>Native SDK behaviors enabled!</p>
|
22
|
+
{isMobile && <p>Using mobile-optimized UI elements</p>}
|
23
|
+
</div>
|
24
|
+
)}
|
25
|
+
</div>
|
26
|
+
</div>
|
27
|
+
);
|
28
|
+
}
|
@@ -0,0 +1,46 @@
|
|
1
|
+
import * as React from "react"
|
2
|
+
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
3
|
+
|
4
|
+
import { cn } from "@/lib/utils"
|
5
|
+
|
6
|
+
function Popover({
|
7
|
+
...props
|
8
|
+
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
9
|
+
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
10
|
+
}
|
11
|
+
|
12
|
+
function PopoverTrigger({
|
13
|
+
...props
|
14
|
+
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
15
|
+
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
16
|
+
}
|
17
|
+
|
18
|
+
function PopoverContent({
|
19
|
+
className,
|
20
|
+
align = "center",
|
21
|
+
sideOffset = 4,
|
22
|
+
...props
|
23
|
+
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
24
|
+
return (
|
25
|
+
<PopoverPrimitive.Portal>
|
26
|
+
<PopoverPrimitive.Content
|
27
|
+
data-slot="popover-content"
|
28
|
+
align={align}
|
29
|
+
sideOffset={sideOffset}
|
30
|
+
className={cn(
|
31
|
+
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
32
|
+
className
|
33
|
+
)}
|
34
|
+
{...props}
|
35
|
+
/>
|
36
|
+
</PopoverPrimitive.Portal>
|
37
|
+
)
|
38
|
+
}
|
39
|
+
|
40
|
+
function PopoverAnchor({
|
41
|
+
...props
|
42
|
+
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
43
|
+
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
44
|
+
}
|
45
|
+
|
46
|
+
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
@@ -0,0 +1,103 @@
|
|
1
|
+
"use client";
|
2
|
+
|
3
|
+
import * as React from "react";
|
4
|
+
import { cn } from "@/lib/utils";
|
5
|
+
import { motion } from "framer-motion";
|
6
|
+
|
7
|
+
interface ProgressProps extends React.HTMLAttributes<HTMLDivElement> {
|
8
|
+
value: number;
|
9
|
+
max: number;
|
10
|
+
className?: string;
|
11
|
+
barClassName?: string;
|
12
|
+
barColor?: string;
|
13
|
+
}
|
14
|
+
|
15
|
+
export function LinearProgress({
|
16
|
+
value,
|
17
|
+
max,
|
18
|
+
className,
|
19
|
+
barClassName,
|
20
|
+
barColor = "#00b2c1",
|
21
|
+
...props
|
22
|
+
}: ProgressProps) {
|
23
|
+
const percentage = (value / max) * 100;
|
24
|
+
|
25
|
+
return (
|
26
|
+
<div
|
27
|
+
className={cn("w-full h-2 bg-gray-200 rounded-full overflow-hidden", className)}
|
28
|
+
{...props}
|
29
|
+
>
|
30
|
+
<motion.div
|
31
|
+
className={cn("h-full", barClassName)}
|
32
|
+
style={{
|
33
|
+
background: barColor,
|
34
|
+
width: `${percentage}%`
|
35
|
+
}}
|
36
|
+
initial={{ width: 0 }}
|
37
|
+
animate={{ width: `${percentage}%` }}
|
38
|
+
transition={{ duration: 0.5, ease: "easeInOut" }}
|
39
|
+
/>
|
40
|
+
</div>
|
41
|
+
);
|
42
|
+
}
|
43
|
+
|
44
|
+
interface StepProgressProps extends React.HTMLAttributes<HTMLDivElement> {
|
45
|
+
steps: number;
|
46
|
+
currentStep: number;
|
47
|
+
className?: string;
|
48
|
+
stepClassName?: string;
|
49
|
+
activeStepClassName?: string;
|
50
|
+
completedStepClassName?: string;
|
51
|
+
connectorClassName?: string;
|
52
|
+
activeConnectorClassName?: string;
|
53
|
+
completedConnectorClassName?: string;
|
54
|
+
}
|
55
|
+
|
56
|
+
export function StepProgress({
|
57
|
+
steps,
|
58
|
+
currentStep,
|
59
|
+
className,
|
60
|
+
stepClassName,
|
61
|
+
activeStepClassName,
|
62
|
+
completedStepClassName,
|
63
|
+
connectorClassName,
|
64
|
+
activeConnectorClassName,
|
65
|
+
completedConnectorClassName,
|
66
|
+
...props
|
67
|
+
}: StepProgressProps) {
|
68
|
+
return (
|
69
|
+
<div
|
70
|
+
className={cn("flex items-center w-full", className)}
|
71
|
+
{...props}
|
72
|
+
>
|
73
|
+
{Array.from({ length: steps }).map((_, index) => {
|
74
|
+
const isCompleted = index < currentStep - 1;
|
75
|
+
const isActive = index === currentStep - 1;
|
76
|
+
|
77
|
+
return (
|
78
|
+
<React.Fragment key={index}>
|
79
|
+
<div
|
80
|
+
className={cn(
|
81
|
+
"w-4 h-4 rounded-full bg-gray-300 flex-shrink-0",
|
82
|
+
isActive && cn("bg-primary", activeStepClassName),
|
83
|
+
isCompleted && cn("bg-primary", completedStepClassName),
|
84
|
+
stepClassName
|
85
|
+
)}
|
86
|
+
/>
|
87
|
+
|
88
|
+
{index < steps - 1 && (
|
89
|
+
<div
|
90
|
+
className={cn(
|
91
|
+
"h-1 flex-grow bg-gray-300",
|
92
|
+
isActive && cn("bg-primary", activeConnectorClassName),
|
93
|
+
isCompleted && cn("bg-primary", completedConnectorClassName),
|
94
|
+
connectorClassName
|
95
|
+
)}
|
96
|
+
/>
|
97
|
+
)}
|
98
|
+
</React.Fragment>
|
99
|
+
);
|
100
|
+
})}
|
101
|
+
</div>
|
102
|
+
);
|
103
|
+
}
|
@@ -0,0 +1,45 @@
|
|
1
|
+
"use client";
|
2
|
+
|
3
|
+
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
4
|
+
import * as React from "react";
|
5
|
+
|
6
|
+
import { cn } from "@/lib/utils";
|
7
|
+
|
8
|
+
const RadioGroup = React.forwardRef<
|
9
|
+
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
10
|
+
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
11
|
+
>(({ className, ...props }, ref) => {
|
12
|
+
return <RadioGroupPrimitive.Root className={cn("grid gap-3", className)} {...props} ref={ref} />;
|
13
|
+
});
|
14
|
+
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
15
|
+
|
16
|
+
const RadioGroupItem = React.forwardRef<
|
17
|
+
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
18
|
+
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
19
|
+
>(({ className, ...props }, ref) => {
|
20
|
+
return (
|
21
|
+
<RadioGroupPrimitive.Item
|
22
|
+
ref={ref}
|
23
|
+
className={cn(
|
24
|
+
"aspect-square size-4 rounded-full border border-input shadow-sm shadow-black/5 outline-offset-2 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring/70 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
25
|
+
className,
|
26
|
+
)}
|
27
|
+
{...props}
|
28
|
+
>
|
29
|
+
<RadioGroupPrimitive.Indicator className="flex items-center justify-center text-current">
|
30
|
+
<svg
|
31
|
+
width="6"
|
32
|
+
height="6"
|
33
|
+
viewBox="0 0 6 6"
|
34
|
+
fill="currentcolor"
|
35
|
+
xmlns="http://www.w3.org/2000/svg"
|
36
|
+
>
|
37
|
+
<circle cx="3" cy="3" r="3" />
|
38
|
+
</svg>
|
39
|
+
</RadioGroupPrimitive.Indicator>
|
40
|
+
</RadioGroupPrimitive.Item>
|
41
|
+
);
|
42
|
+
});
|
43
|
+
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
44
|
+
|
45
|
+
export { RadioGroup, RadioGroupItem };
|