saafe-redirection-flow 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/.github/workflows/build-and-deploy.yml +41 -0
  2. package/.gitlab-ci.yml +108 -0
  3. package/.releaserc.json +18 -0
  4. package/.storybook/main.ts +28 -0
  5. package/.storybook/preview.ts +16 -0
  6. package/.storybook/vitest.setup.ts +9 -0
  7. package/.vite/deps/@radix-ui_react-avatar.js +230 -0
  8. package/.vite/deps/@radix-ui_react-avatar.js.map +7 -0
  9. package/.vite/deps/@radix-ui_react-slot.js +12 -0
  10. package/.vite/deps/@radix-ui_react-slot.js.map +7 -0
  11. package/.vite/deps/_metadata.json +79 -0
  12. package/.vite/deps/chunk-5VGQBUCU.js +597 -0
  13. package/.vite/deps/chunk-5VGQBUCU.js.map +7 -0
  14. package/.vite/deps/chunk-DC5AMYBS.js +38 -0
  15. package/.vite/deps/chunk-DC5AMYBS.js.map +7 -0
  16. package/.vite/deps/chunk-HUIEPYH7.js +11265 -0
  17. package/.vite/deps/chunk-HUIEPYH7.js.map +7 -0
  18. package/.vite/deps/chunk-TKHB4QMX.js +281 -0
  19. package/.vite/deps/chunk-TKHB4QMX.js.map +7 -0
  20. package/.vite/deps/chunk-YLDSBLSF.js +1139 -0
  21. package/.vite/deps/chunk-YLDSBLSF.js.map +7 -0
  22. package/.vite/deps/class-variance-authority.js +63 -0
  23. package/.vite/deps/class-variance-authority.js.map +7 -0
  24. package/.vite/deps/lucide-react.js +36984 -0
  25. package/.vite/deps/lucide-react.js.map +7 -0
  26. package/.vite/deps/package.json +3 -0
  27. package/.vite/deps/react-dom_client.js +17917 -0
  28. package/.vite/deps/react-dom_client.js.map +7 -0
  29. package/.vite/deps/react-router-dom.js +452 -0
  30. package/.vite/deps/react-router-dom.js.map +7 -0
  31. package/.vite/deps/react-router.js +234 -0
  32. package/.vite/deps/react-router.js.map +7 -0
  33. package/.vite/deps/react.js +5 -0
  34. package/.vite/deps/react.js.map +7 -0
  35. package/.vite/deps/react_jsx-dev-runtime.js +470 -0
  36. package/.vite/deps/react_jsx-dev-runtime.js.map +7 -0
  37. package/CHANGELOG.md +420 -0
  38. package/LICENSE +21 -0
  39. package/README.md +129 -0
  40. package/RELEASE_CHEATSHEET.md +93 -0
  41. package/RELEASE_NOTES.md +120 -0
  42. package/components.json +21 -0
  43. package/docs/DEPLOYMENT_WORKFLOW.md +262 -0
  44. package/docs/RELEASE_GUIDE.md +591 -0
  45. package/docs/architecture.md +432 -0
  46. package/docs/components.md +199 -0
  47. package/docs/index.md +69 -0
  48. package/docs/local-release-workflow.md +234 -0
  49. package/docs/routes.md +118 -0
  50. package/docs/sdk-integration.md +325 -0
  51. package/docs/semantic-release.md +124 -0
  52. package/docs/user-flow.md +206 -0
  53. package/eslint.config.js +28 -0
  54. package/index.html +19 -0
  55. package/install.sh +198 -0
  56. package/package.json +115 -0
  57. package/public/images/bank-logo.png +0 -0
  58. package/public/saafe-icon.svg +9 -0
  59. package/src/App.tsx +171 -0
  60. package/src/__tests__/url-parameters.test.ts +82 -0
  61. package/src/assets/brand/applestore.svg +13 -0
  62. package/src/assets/brand/playstore.svg +23 -0
  63. package/src/assets/brand/saafe-color-white-logo.svg +14 -0
  64. package/src/assets/brand/saafe-icon.svg +9 -0
  65. package/src/assets/brand/saafe-logo.svg +18 -0
  66. package/src/assets/icons/check-icon-dark.svg +27 -0
  67. package/src/assets/icons/check-icon.svg +23 -0
  68. package/src/components/ErrorBoundary.tsx +132 -0
  69. package/src/components/alert/alert.tsx +27 -0
  70. package/src/components/auth/AuthGuard.tsx +76 -0
  71. package/src/components/cards/BankCard.stories.tsx +69 -0
  72. package/src/components/cards/BankCard.tsx +227 -0
  73. package/src/components/cards/OuterCard.tsx +109 -0
  74. package/src/components/cards/WrapperCard.tsx +64 -0
  75. package/src/components/documents/PrivacyContent.tsx +1 -0
  76. package/src/components/dummyFooter.tsx +29 -0
  77. package/src/components/icons/github.tsx +12 -0
  78. package/src/components/language/LanguageSwitcher.tsx +44 -0
  79. package/src/components/layouts/FrostedLayout.stories.tsx +42 -0
  80. package/src/components/layouts/FrostedLayout.tsx +333 -0
  81. package/src/components/layouts/MobileLayout.tsx +403 -0
  82. package/src/components/mobile-background.tsx +136 -0
  83. package/src/components/mobileAppDownload.tsx +30 -0
  84. package/src/components/modal/ModalComp.tsx +27 -0
  85. package/src/components/mode-toggle.tsx +36 -0
  86. package/src/components/page-header.tsx +50 -0
  87. package/src/components/session/SessionTimeoutScreen.tsx +134 -0
  88. package/src/components/session/SessionTimer.tsx +173 -0
  89. package/src/components/step-navigation.tsx +87 -0
  90. package/src/components/title/AppBar.stories.tsx +50 -0
  91. package/src/components/title/AppBar.tsx +150 -0
  92. package/src/components/title/SectionTitle.tsx +31 -0
  93. package/src/components/ui/AnimatedButton.module.css +13 -0
  94. package/src/components/ui/alert.tsx +66 -0
  95. package/src/components/ui/animatedButton.tsx +111 -0
  96. package/src/components/ui/avatar.tsx +51 -0
  97. package/src/components/ui/badge.tsx +36 -0
  98. package/src/components/ui/bottom-sheet.tsx +122 -0
  99. package/src/components/ui/button.tsx +59 -0
  100. package/src/components/ui/calendar.tsx +86 -0
  101. package/src/components/ui/card.tsx +92 -0
  102. package/src/components/ui/checkbox.stories.tsx +49 -0
  103. package/src/components/ui/checkbox.tsx +67 -0
  104. package/src/components/ui/collapsible.tsx +45 -0
  105. package/src/components/ui/dialog.tsx +134 -0
  106. package/src/components/ui/document-link.tsx +26 -0
  107. package/src/components/ui/dot-stepper.tsx +57 -0
  108. package/src/components/ui/dropdown-menu.tsx +255 -0
  109. package/src/components/ui/form.tsx +165 -0
  110. package/src/components/ui/frosted-panel.stories.tsx +86 -0
  111. package/src/components/ui/frosted-panel.tsx +276 -0
  112. package/src/components/ui/input.tsx +39 -0
  113. package/src/components/ui/label.stories.tsx +67 -0
  114. package/src/components/ui/label.tsx +23 -0
  115. package/src/components/ui/mobile-footer.tsx +54 -0
  116. package/src/components/ui/modal.tsx +90 -0
  117. package/src/components/ui/otp-input.stories.tsx +62 -0
  118. package/src/components/ui/otp-input.tsx +221 -0
  119. package/src/components/ui/platform-specific-behavior.tsx +28 -0
  120. package/src/components/ui/popover.tsx +46 -0
  121. package/src/components/ui/progress.tsx +103 -0
  122. package/src/components/ui/radio-group.tsx +45 -0
  123. package/src/components/ui/scroll-area.tsx +56 -0
  124. package/src/components/ui/sdk-params-docs.tsx +53 -0
  125. package/src/components/ui/select.tsx +159 -0
  126. package/src/components/ui/separator.tsx +28 -0
  127. package/src/components/ui/sheet.tsx +137 -0
  128. package/src/components/ui/sidebar.tsx +724 -0
  129. package/src/components/ui/skeleton.stories.tsx +50 -0
  130. package/src/components/ui/skeleton.tsx +15 -0
  131. package/src/components/ui/sonner.tsx +23 -0
  132. package/src/components/ui/step.stories.tsx +132 -0
  133. package/src/components/ui/step.tsx +234 -0
  134. package/src/components/ui/stepper-progress.tsx +136 -0
  135. package/src/components/ui/stepper.tsx +259 -0
  136. package/src/components/ui/tabs.tsx +55 -0
  137. package/src/components/ui/tooltip.tsx +61 -0
  138. package/src/components/ui/url-decode-loader.tsx +36 -0
  139. package/src/components/ui/version-display.tsx +104 -0
  140. package/src/components/ui/web-footer.tsx +36 -0
  141. package/src/config/environments.ts +99 -0
  142. package/src/config/urls.ts +53 -0
  143. package/src/const/fiTypeCategoryMap.ts +19 -0
  144. package/src/contexts/LanguageContext.tsx +41 -0
  145. package/src/contexts/RTLContext.tsx +42 -0
  146. package/src/contexts/ThemeContext.tsx +93 -0
  147. package/src/hooks/use-account-discovery.ts +205 -0
  148. package/src/hooks/use-auth-query.ts +141 -0
  149. package/src/hooks/use-fip-query.ts +72 -0
  150. package/src/hooks/use-media-query.ts +32 -0
  151. package/src/hooks/use-mobile.ts +24 -0
  152. package/src/hooks/use-page-title.tsx +48 -0
  153. package/src/hooks/use-platform.ts +52 -0
  154. package/src/hooks/use-trusted-count.ts +21 -0
  155. package/src/hooks/use-url-decode.ts +90 -0
  156. package/src/hooks/useStep.ts +170 -0
  157. package/src/index.css +154 -0
  158. package/src/interfaces/app.interfaces.ts +39 -0
  159. package/src/interfaces/services.interfaces.ts +65 -0
  160. package/src/lib/i18n.ts +68 -0
  161. package/src/lib/utils.ts +6 -0
  162. package/src/locales/en/common.json +167 -0
  163. package/src/locales/hi/common.json +137 -0
  164. package/src/locales/kn/common.json +137 -0
  165. package/src/locales/ml/common.json +137 -0
  166. package/src/locales/ta/common.json +137 -0
  167. package/src/locales/te/common.json +137 -0
  168. package/src/locales/ur/common.json +138 -0
  169. package/src/main.tsx +46 -0
  170. package/src/pages/Login.tsx +363 -0
  171. package/src/pages/accounts/AccountsToProceed.tsx +396 -0
  172. package/src/pages/accounts/Discover.tsx +76 -0
  173. package/src/pages/accounts/DiscoverAccount.tsx +751 -0
  174. package/src/pages/accounts/LinkSelectedAccounts.tsx +638 -0
  175. package/src/pages/accounts/OldUser.tsx +329 -0
  176. package/src/pages/accounts/link-accounts.tsx +913 -0
  177. package/src/pages/consent/ReviewConsent.tsx +836 -0
  178. package/src/pages/consent/rejected.tsx +253 -0
  179. package/src/pages/consent/success.tsx +220 -0
  180. package/src/providers/query-provider.tsx +24 -0
  181. package/src/providers/toast-provider.tsx +26 -0
  182. package/src/services/api/account.service.ts +296 -0
  183. package/src/services/api/auth.service.ts +206 -0
  184. package/src/services/api/axios.ts +138 -0
  185. package/src/services/api/consent.service.ts +142 -0
  186. package/src/services/api/decode.service.ts +53 -0
  187. package/src/services/api/feedback.service.ts +34 -0
  188. package/src/services/api/fip.service.ts +187 -0
  189. package/src/services/api/index.ts +9 -0
  190. package/src/services/api/public.service.ts +18 -0
  191. package/src/services/api.ts +2 -0
  192. package/src/services/postMessage.service.ts +179 -0
  193. package/src/store/NavigationBlockContext.tsx +34 -0
  194. package/src/store/auth.store.ts +79 -0
  195. package/src/store/fip.store.ts +396 -0
  196. package/src/store/mandatoryConsent.store.ts +24 -0
  197. package/src/store/redirect.store.ts +73 -0
  198. package/src/store/step.store.ts +124 -0
  199. package/src/stories/Button.stories.ts +53 -0
  200. package/src/stories/Button.tsx +37 -0
  201. package/src/stories/Configure.mdx +364 -0
  202. package/src/stories/Header.stories.ts +33 -0
  203. package/src/stories/Header.tsx +56 -0
  204. package/src/stories/Page.stories.ts +32 -0
  205. package/src/stories/Page.tsx +73 -0
  206. package/src/stories/button.css +30 -0
  207. package/src/stories/header.css +32 -0
  208. package/src/stories/page.css +68 -0
  209. package/src/styles/rtl-utils.css +90 -0
  210. package/src/styles/rtl.css +105 -0
  211. package/src/utils/api-error.ts +26 -0
  212. package/src/utils/cn.ts +10 -0
  213. package/src/utils/error-callback.ts +116 -0
  214. package/src/utils/formatAccountNumber.ts +9 -0
  215. package/src/utils/handleIdentifiers.ts +90 -0
  216. package/src/utils/posthog.ts +67 -0
  217. package/src/utils/toast-helpers.ts +61 -0
  218. package/src/vite-env.d.ts +1 -0
  219. package/stage-aa-2506251021.zip +0 -0
  220. package/tsconfig.app.json +33 -0
  221. package/tsconfig.json +13 -0
  222. package/tsconfig.node.json +24 -0
  223. package/vite.config.ts +45 -0
  224. package/vitest.shims.d.ts +1 -0
  225. package/vitest.workspace.ts +46 -0
@@ -0,0 +1,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 };