@eka-care/abha 0.0.1 → 0.0.2

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 (87) hide show
  1. package/.eslintignore +3 -0
  2. package/.eslintrc +29 -0
  3. package/.prettierrc +7 -0
  4. package/dev-abha-ios-android.zip +0 -0
  5. package/{dist/index.html → index.html} +1 -2
  6. package/package.json +5 -32
  7. package/postcss.config.js +6 -0
  8. package/prod-abha-mixpanel.zip +0 -0
  9. package/scripts/build-purged-css.cjs +70 -0
  10. package/src/App.css +0 -0
  11. package/src/App.tsx +32 -0
  12. package/src/api-queries/use-get-profiles-patient.ts +12 -0
  13. package/src/api-queries/use-get-profiles-phr-user.ts +27 -0
  14. package/src/api-queries/use-post-abdm-login-verify-v1.ts +25 -0
  15. package/src/api-queries/use-post-auth-verify-v2.ts +74 -0
  16. package/src/api-queries/use-post-profile-switch.ts +57 -0
  17. package/src/api-queries/use-post-register-mobile-create-phr.ts +39 -0
  18. package/src/api-queries/user-post-abdm-profile-login-phr.ts +25 -0
  19. package/src/assets/react.svg +1 -0
  20. package/src/atoms/button/custom-button.tsx +32 -0
  21. package/src/atoms/button/index.tsx +40 -0
  22. package/src/atoms/button/types.d.ts +31 -0
  23. package/src/atoms/header.tsx +25 -0
  24. package/src/atoms/input-field/index.tsx +62 -0
  25. package/src/atoms/input-field/patient-input-field.tsx +16 -0
  26. package/src/atoms/input-field/types.ts +24 -0
  27. package/src/atoms/pds2-otp-input/index.tsx +35 -0
  28. package/src/atoms/pds2-otp-input/types.d.ts +3 -0
  29. package/src/atoms/single-input-chip/index.tsx +32 -0
  30. package/src/atoms/single-input-chip/types.ts +6 -0
  31. package/src/atoms/spinner.tsx +33 -0
  32. package/src/atoms/text-separator.tsx +11 -0
  33. package/src/index.css +118 -0
  34. package/src/main.tsx +313 -0
  35. package/src/molecules/abha/bottom-sheet/bottom-sheet-wrapper.tsx +40 -0
  36. package/src/molecules/abha/bottom-sheet/index.tsx +66 -0
  37. package/src/molecules/abha/spaced-input-component.tsx +150 -0
  38. package/src/molecules/copyright-year.tsx +16 -0
  39. package/src/molecules/exit-popup/index.tsx +99 -0
  40. package/src/molecules/pds2-otp-component/index.tsx +148 -0
  41. package/src/organisms/abha/abha-header.tsx +25 -0
  42. package/src/organisms/abha/error-bottom-sheet.tsx +27 -0
  43. package/src/organisms/abha/otp-card.tsx +99 -0
  44. package/src/organisms/abha/verification-status.tsx +40 -0
  45. package/src/organisms/choose-language/choose-language.tsx +53 -0
  46. package/src/organisms/choose-language/types.ts +10 -0
  47. package/src/organisms/screen-switcher/screen-switcher.tsx +80 -0
  48. package/src/routes/abha-aadhaar-verification-status-screen.tsx +209 -0
  49. package/src/routes/abha-created-screen.tsx +45 -0
  50. package/src/routes/abha-login-otp-verify-screen.tsx +523 -0
  51. package/src/routes/abha-mobile-linking-status-screen.tsx +267 -0
  52. package/src/routes/abha-otp-and-mobile-screen.tsx +429 -0
  53. package/src/routes/abha-phone-number-verification-screen.tsx +373 -0
  54. package/src/routes/create-abha-address-screen.tsx +928 -0
  55. package/src/routes/create-abha-with-aadhaar-screen.tsx +984 -0
  56. package/src/routes/create-eka-profile-screen.tsx +777 -0
  57. package/src/routes/get-all-profiles-screen.tsx +161 -0
  58. package/src/routes/login-or-create-abha-address-screen.tsx +953 -0
  59. package/src/routes/login-with-abha-screen.tsx +448 -0
  60. package/src/routes/select-abha-from-list-screen.tsx +718 -0
  61. package/src/routes/select-eka-profile-screen.tsx +444 -0
  62. package/src/routes/utils/trackAbhaEvent.ts +41 -0
  63. package/src/stores/auth-abha-store/index.ts +138 -0
  64. package/src/stores/auth-abha-store/types.ts +204 -0
  65. package/src/utils/mock-auth-response.ts +31 -0
  66. package/src/vite-env.d.ts +1 -0
  67. package/tailwind.config.ts +9 -0
  68. package/tsconfig.app.json +26 -0
  69. package/tsconfig.json +25 -0
  70. package/tsconfig.node.json +10 -0
  71. package/tsconfig.node.tsbuildinfo +1 -0
  72. package/tsconfig.tsbuildinfo +1 -0
  73. package/vite.config.d.ts +2 -0
  74. package/vite.config.js +30 -0
  75. package/vite.config.ts +35 -0
  76. package/dist/README.md +0 -114
  77. package/dist/package.json +0 -7
  78. package/dist/sdk/abha/css/abha.css +0 -1
  79. package/dist/sdk/abha/js/abha.js +0 -137
  80. /package/{dist → public}/images/adhaar.webp +0 -0
  81. /package/{dist → public}/images/at-the-rate.webp +0 -0
  82. /package/{dist → public}/images/avatar.webp +0 -0
  83. /package/{dist → public}/images/ayushman-bharat.webp +0 -0
  84. /package/{dist → public}/images/circle-checkmark.webp +0 -0
  85. /package/{dist → public}/images/link-abha.webp +0 -0
  86. /package/{dist → public}/images/national-authority.webp +0 -0
  87. /package/{dist → public}/images/three-dots.webp +0 -0
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ type BottomSheetWrapperProps = {
3
+ isOpen: boolean;
4
+ setIsOpen: (open: boolean) => void;
5
+ children: React.ReactNode;
6
+ shouldCloseOnClickOutside?: boolean;
7
+ };
8
+ const BottomSheetWrapper = ({
9
+ isOpen,
10
+ setIsOpen,
11
+ children,
12
+ shouldCloseOnClickOutside = true,
13
+ }: BottomSheetWrapperProps) => {
14
+ return (
15
+ <div
16
+ className={`pds2-absolute pds2-inset-0 pds2-z-50 pds2-transition-opacity pds2-duration-300 ${
17
+ isOpen
18
+ ? 'pds2-opacity-100 pds2-pointer-events-auto'
19
+ : 'pds2-opacity-0 pds2-pointer-events-none'
20
+ }`}
21
+ >
22
+ {/* Background overlay */}
23
+ <div
24
+ className="pds2-absolute pds2-inset-0 pds2-bg-text-black pds2-bg-opacity-60 pds2-transition-opacity pds2-duration-300"
25
+ onClick={() => shouldCloseOnClickOutside && setIsOpen(false)}
26
+ />
27
+
28
+ {/* Bottom Sheet Content */}
29
+ <div
30
+ className="pds2-absolute pds2-bottom-0 pds2-left-0 pds2-right-0 pds2-w-full pds2-bg-bg-white pds2-rounded-t-16 pds2-shadow-lg pds2-p-4 pds2-transition-all pds2-duration-300 pds2-ease-out"
31
+ style={{
32
+ transform: isOpen ? 'translateY(0)' : 'translateY(100%)',
33
+ }}
34
+ >
35
+ {children}
36
+ </div>
37
+ </div>
38
+ );
39
+ };
40
+ export default BottomSheetWrapper;
@@ -0,0 +1,66 @@
1
+ import { XMarkRegularIcon } from '@elixir/icons';
2
+ import Pds2Button from '../../../atoms/button';
3
+ import BottomSheetWrapper from './bottom-sheet-wrapper';
4
+
5
+ type TAbhaBottomSheetProps = {
6
+ description: string;
7
+ subText?: string;
8
+ className?: string;
9
+ submitText: string;
10
+ onSubmitClick: () => void;
11
+ iconUrl?: string;
12
+ isBottomSheetOpen: boolean;
13
+ onBottomSheetClose: () => void;
14
+ shouldCloseOnClickOutside?: boolean;
15
+ };
16
+
17
+ const AbhaBottomSheet = ({
18
+ description,
19
+ subText,
20
+ submitText,
21
+ onSubmitClick,
22
+ iconUrl,
23
+ isBottomSheetOpen,
24
+ onBottomSheetClose,
25
+ shouldCloseOnClickOutside = true,
26
+ }: TAbhaBottomSheetProps) => {
27
+ return (
28
+ <BottomSheetWrapper
29
+ isOpen={isBottomSheetOpen}
30
+ setIsOpen={onBottomSheetClose}
31
+ shouldCloseOnClickOutside={shouldCloseOnClickOutside}
32
+ >
33
+ <div className="pds2-flex pds2-flex-col pds2-p-16 pds2-text-center">
34
+ {shouldCloseOnClickOutside ? (
35
+ <div className="pds2-flex pds2-justify-end pds2-items-center">
36
+ <button
37
+ onClick={onBottomSheetClose}
38
+ className="pds2-w-24 pds2-h-24 pds2-rounded-full pds2-flex pds2-items-center pds2-justify-center ripple"
39
+ >
40
+ <XMarkRegularIcon className="pds2-w-18 pds2-h-18 pds2-text-text-03" />
41
+ </button>
42
+ </div>
43
+ ) : null}
44
+ <div className="pds2-space-y-8">
45
+ {iconUrl && (
46
+ <span className="pds2-flex pds2-justify-center">
47
+ <img src={iconUrl} className="pds2-w-64 pds2-h-64" alt="Bottom sheet icon" />
48
+ </span>
49
+ )}
50
+ <div className="Heading4Semibold pds2-text-text-01 pds2-py-16 pds2-text-18">
51
+ {description}
52
+ </div>
53
+ {subText && <div className="Body2Regular pds2-text-text-03">{subText}</div>}
54
+ <Pds2Button
55
+ title={submitText}
56
+ onClick={onSubmitClick}
57
+ state={'enabled'}
58
+ className="pds2-w-full"
59
+ />
60
+ </div>
61
+ </div>
62
+ </BottomSheetWrapper>
63
+ );
64
+ };
65
+
66
+ export default AbhaBottomSheet;
@@ -0,0 +1,150 @@
1
+ import { useRef, useState } from 'react';
2
+
3
+ type SpacedInputComponentProps = {
4
+ numberOfInputs: number;
5
+ length?: number;
6
+ placeholder?: string;
7
+ setIsValid?: (val: boolean) => void;
8
+ onSubmit: ({ inputVal }: { inputVal: string }) => void;
9
+ error?: string | null;
10
+ containerClassName?: string;
11
+ setError?: (val: string | null) => void;
12
+ };
13
+
14
+ const SpacedInputComponent = ({
15
+ numberOfInputs,
16
+ length = 1,
17
+ placeholder = '',
18
+ setIsValid,
19
+ onSubmit,
20
+ error,
21
+ containerClassName,
22
+ setError,
23
+ }: SpacedInputComponentProps) => {
24
+ const inputRefs = useRef<HTMLInputElement[]>([]);
25
+ const [inputValues, setInputValues] = useState<string[]>(Array(numberOfInputs).fill(''));
26
+
27
+ const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>, index: number) => {
28
+ if (setError) setError(null);
29
+ const value = e.target.value;
30
+
31
+ const isValueNan = isNaN(Number(value));
32
+ if (isValueNan) return;
33
+
34
+ // To autoread OTP from keyboard clipboard
35
+ if (value.trim().length === numberOfInputs * length) {
36
+ handleClipboardOtp({
37
+ otpValue: value,
38
+ index: 0,
39
+ });
40
+ return;
41
+ }
42
+ const currentValues = [...inputValues];
43
+ currentValues[index] = value;
44
+ const totalLength = currentValues.join('').length;
45
+ if (totalLength > numberOfInputs * length) return;
46
+
47
+ // Update current input value
48
+ setInputValues(currentValues);
49
+
50
+ if (totalLength === numberOfInputs * length) {
51
+ setIsValid?.(true);
52
+ onSubmit({ inputVal: currentValues.join('') });
53
+ } else {
54
+ setIsValid?.(false);
55
+ }
56
+
57
+ // Move to next input if current is full
58
+ if (value.length === length && index < numberOfInputs - 1) {
59
+ inputRefs.current[index + 1]?.focus();
60
+ }
61
+ };
62
+
63
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>, index: number) => {
64
+ if (e.key === 'Backspace' && !inputValues[index] && index > 0) {
65
+ inputRefs.current[index - 1]?.focus();
66
+ setInputValues((prev) => {
67
+ const newValues = [...prev];
68
+ newValues[index - 1] = '';
69
+ return newValues;
70
+ });
71
+ }
72
+ };
73
+
74
+ const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>, index: number) => {
75
+ e.preventDefault();
76
+ const pastedData = e.clipboardData.getData('text').replace(/\D/g, ''); // Only keep digits
77
+
78
+ if (!pastedData) return;
79
+
80
+ const maxLength = numberOfInputs * length;
81
+ const validPastedData = pastedData.slice(0, maxLength);
82
+ handleClipboardOtp({
83
+ otpValue: validPastedData,
84
+ index,
85
+ });
86
+ };
87
+
88
+ const handleClipboardOtp = ({ otpValue, index }: { otpValue: string; index: number }) => {
89
+ const chunks = otpValue.match(new RegExp(`.{1,${length}}`, 'g')) || [];
90
+
91
+ const newValues = [...inputValues];
92
+
93
+ chunks.forEach((chunk, i) => {
94
+ if (index + i < numberOfInputs) {
95
+ newValues[index + i] = chunk;
96
+ }
97
+ });
98
+
99
+ setInputValues(newValues);
100
+
101
+ // Focus the next empty input or the last input
102
+ const nextEmptyIndex = newValues.findIndex((val, i) => i >= index && !val);
103
+ if (nextEmptyIndex !== -1) {
104
+ inputRefs.current[nextEmptyIndex]?.focus();
105
+ } else {
106
+ inputRefs.current[numberOfInputs - 1]?.focus();
107
+ }
108
+
109
+ // Hit endpoint if all inputs are filled
110
+ if (newValues.join('').length === numberOfInputs * length) {
111
+ setIsValid?.(true);
112
+ onSubmit({ inputVal: newValues.join('') });
113
+ } else {
114
+ setIsValid?.(false);
115
+ }
116
+ };
117
+
118
+ return (
119
+ <div
120
+ className={`pds2-flex pds2-justify-center pds2-items-center pds2-space-x-8 ${containerClassName}`}
121
+ >
122
+ {Array.from({ length: numberOfInputs }).map((_, index) => (
123
+ <label
124
+ key={index}
125
+ className={`focus-within:pds2-border-border-brand-01 pds2-border ${error ? 'pds2-border-border-error' : 'pds2-border-border-03'} pds2-rounded-12 pds2-bg-bg-white`}
126
+ >
127
+ <input
128
+ autoFocus={index === 0 ? true : false}
129
+ placeholder={placeholder}
130
+ value={inputValues[index]}
131
+ key={index}
132
+ type="text"
133
+ inputMode="numeric"
134
+ pattern="[0-9]*"
135
+ // maxLength={length}
136
+ onPaste={(e) => handlePaste(e, index)}
137
+ ref={(el) => {
138
+ if (el) inputRefs.current[index] = el;
139
+ }}
140
+ onChange={(e) => handleOnChange(e, index)}
141
+ onKeyDown={(e) => handleKeyDown(e, index)}
142
+ className={`pds2-w-full pds2-text-center pds2-px-16 pds2-h-48 pds2-rounded-12 pds2-outline-none pds2-bg-transparent`}
143
+ />
144
+ </label>
145
+ ))}
146
+ </div>
147
+ );
148
+ };
149
+
150
+ export default SpacedInputComponent;
@@ -0,0 +1,16 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ const CopyrightYearComponent = () => {
4
+ const [year, setYear] = useState<number>(new Date().getFullYear());
5
+
6
+ useEffect(() => {
7
+ setYear(new Date().getFullYear());
8
+ }, []);
9
+ return (
10
+ <div className="pds2-absolute pds2-bottom-5 BodyRegular pds2-text-darwin-neutral-600">
11
+ {`Copyright © ${year} eka.care`}
12
+ </div>
13
+ );
14
+ };
15
+
16
+ export default CopyrightYearComponent;
@@ -0,0 +1,99 @@
1
+ import Pds2CustomButton from '../../atoms/button/custom-button';
2
+ import useAuthAbhaStore from '../../stores/auth-abha-store';
3
+ import { handleSendEvent } from '@elixir/utils';
4
+ import { ABHA_AUTH_FLOW_METHOD } from '@elixir/types';
5
+
6
+ const ExitPopup = ({ open, setOpen }: { open: boolean; setOpen: (val: boolean) => void }) => {
7
+ // State selectors from ABHA auth store
8
+ const goBackLoginScreen = useAuthAbhaStore((state) => state.goBackLoginScreen);
9
+ const clientId = useAuthAbhaStore((state) => state.clientId);
10
+ const isEkaAppLogin = useAuthAbhaStore((state) => state.isEkaAppLogin);
11
+ const isNewLoginOrCreateFlow = useAuthAbhaStore((state) => state.isNewLoginOrCreateFlow);
12
+ const setAbhaAuthFlowMethod = useAuthAbhaStore((state) => state.setAbhaAuthFlowMethod);
13
+ const txnId = useAuthAbhaStore((state) => state.txnId);
14
+
15
+ /**
16
+ * Event tracker for ABHA-related events.
17
+ * Sends data to internal logger and Mixpanel with base context.
18
+ */
19
+ const trackAbhaEvent = ({ name, data = {} }: { name: string; data?: Record<string, any> }) => {
20
+ const baseProps = {
21
+ login_platform: clientId,
22
+ is_eka_app_login: isEkaAppLogin ? 'true' : 'false',
23
+ is_new_login_or_create_flow: isNewLoginOrCreateFlow ? 'true' : 'false',
24
+ txn_id : txnId || 'missing txnId',
25
+ };
26
+
27
+ const eventData = {
28
+ ...baseProps,
29
+ ...data,
30
+ };
31
+
32
+ // Mixpanel tracking
33
+ handleSendEvent({
34
+ eventName: name,
35
+ eventData,
36
+ });
37
+
38
+ window.curio?.pushToMixpanel?.(name, eventData);
39
+ };
40
+
41
+ /**
42
+ * Handles the user's confirmation to exit the ABHA flow.
43
+ * Resets flow to mobile method and navigates back to login.
44
+ */
45
+ const handleExitConfirmed = () => {
46
+ trackAbhaEvent({
47
+ name: 'abha_exit_popup_confirmed',
48
+ data: {
49
+ action: 'go_back_to_login_screen',
50
+ },
51
+ });
52
+
53
+ // Reset flow method to mobile as default
54
+ setAbhaAuthFlowMethod(ABHA_AUTH_FLOW_METHOD.MOBILE);
55
+ // Navigate back to login screen
56
+ goBackLoginScreen();
57
+ };
58
+
59
+ return (
60
+ <>
61
+ {open ? (
62
+ <div className="pds2-z-[60]">
63
+ <div
64
+ className="pds2-flex pds2-items-center pds2-justify-center pds2-fixed pds2-top-0 pds2-inset-0 pds2-bg-text-black pds2-bg-opacity-60 pds2-transition-opacity pds2-duration-300"
65
+ onClick={() => setOpen(false)}
66
+ />
67
+ <div className="pds2-fixed pds2-top-1/2 pds2-transform pds2--translate-y-1/2 pds2-px-24">
68
+ <div className="pds2-flex pds2-flex-col pds2-space-y-16 pds2-bg-bg-white pds2-rounded-16 pds2-w-full pds2-p-24">
69
+ <h2 className="pds2-text-text-black pds2-font-500 pds2-text-18">
70
+ Are you sure you want to exit?
71
+ </h2>
72
+ <p className="pds2-text-text-03 pds2-text-14 pds2-font-400">
73
+ Confirm if you'd like to exit. Any unsaved changes may be lost.
74
+ </p>
75
+ <div className="pds2-flex pds2-justify-end pds2-gap-x-8">
76
+ <Pds2CustomButton
77
+ title="Not yet"
78
+ padding="pds2-px-12 pds2-py-8"
79
+ className="pds2-text-text-error pds2-font-600 pds2-text-16 pds2-border-none"
80
+ width="pds2-w-fit"
81
+ onClick={() => setOpen(false)}
82
+ />
83
+ <Pds2CustomButton
84
+ title="Yes, I'm done"
85
+ padding="pds2-px-12 pds2-py-8"
86
+ className="pds2-text-text-brand pds2-font-600 pds2-text-16 pds2-border-none"
87
+ width="pds2-w-fit"
88
+ onClick={handleExitConfirmed}
89
+ />
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ ) : null}
95
+ </>
96
+ );
97
+ };
98
+
99
+ export default ExitPopup;
@@ -0,0 +1,148 @@
1
+ import { CircleExclamationRegularIcon } from '@elixir/icons';
2
+ import React, { useState } from 'react';
3
+ import ReactOTPInput, { AllowedInputTypes } from 'react-otp-input';
4
+ import { LinkButton, SecondaryButton } from 'ui';
5
+ import Pds2OtpInput from '../../atoms/pds2-otp-input';
6
+
7
+ const OtpErrorComponent = ({ errorMessage }: { errorMessage: string }) => {
8
+ return (
9
+ <div className="pds2-flex pds2-space-x-4 pds2-items-center pds2-text-text-error">
10
+ <div className="pds2-p-8">
11
+ <CircleExclamationRegularIcon className="pds2-w-12 pds2-h-12" />
12
+ </div>
13
+ <div className="Body2Regular">{errorMessage}</div>
14
+ </div>
15
+ );
16
+ };
17
+
18
+ const ResendCodeComponent = ({ handleResendCode }: { handleResendCode: () => void }) => {
19
+ return (
20
+ <div className="pds2-flex pds2-justify-center pds2-py-16 pds2-text-text-doc">
21
+ <div onClick={handleResendCode} role="button">
22
+ Resend code
23
+ </div>
24
+ </div>
25
+ );
26
+ };
27
+
28
+ const ResendTimerComponent = ({ secondsRemaining }: { secondsRemaining: string }) => {
29
+ return (
30
+ <div className="BodyRegular pds2-text-text-03 pds2-text-center pds2-py-16">{`Resend in 00:${secondsRemaining}`}</div>
31
+ );
32
+ };
33
+
34
+ type TOtpComponentProps = {
35
+ /**
36
+ * Number of fields in the OTP input
37
+ */
38
+ numberOfFields: number;
39
+ inputType?: AllowedInputTypes;
40
+ /**
41
+ * Error message to be displayed
42
+ */
43
+ errorMessage?: string | null;
44
+ /**
45
+ * Function to set error message
46
+ */
47
+ setErrorMessage: (message: string) => void;
48
+ /**
49
+ * Function to handle OTP submission
50
+ */
51
+ onSubmit: ({ otp }: { otp: string }) => void;
52
+ /**
53
+ * Function to handle resend code
54
+ */
55
+ onResendCode: () => void;
56
+ /**
57
+ * Function to reset identifier entered by the user
58
+ */
59
+ onResetIdentifier: () => void;
60
+ /**
61
+ * Function to set the enable/disable state of button
62
+ */
63
+ setIsValid?: (val: boolean) => void;
64
+ };
65
+
66
+ const Pds2OtpComponent = ({
67
+ numberOfFields,
68
+ inputType = 'tel',
69
+ errorMessage,
70
+ setErrorMessage,
71
+ onSubmit,
72
+ onResendCode,
73
+ onResetIdentifier,
74
+ setIsValid,
75
+ }: TOtpComponentProps) => {
76
+ const [otp, setOtp] = useState('');
77
+ const [timerSeconds, setTimerSeconds] = useState<number>(30);
78
+ const resendTimerIntervalId = React.useRef<NodeJS.Timeout>();
79
+ const [activeInput, setActiveInput] = useState<number>(-1);
80
+
81
+ const handleOtpChange = (newOtp: string) => {
82
+ setOtp(newOtp);
83
+ setErrorMessage('');
84
+
85
+ if (newOtp.length === numberOfFields) {
86
+ setIsValid?.(true);
87
+ onSubmit({ otp: newOtp });
88
+ // setTimerSeconds(30);
89
+ } else {
90
+ setIsValid?.(false);
91
+ }
92
+ };
93
+
94
+ React.useEffect(() => {
95
+ resendTimerIntervalId.current = setInterval(() => {
96
+ setTimerSeconds((timer) => timer - 1);
97
+ }, 1000);
98
+
99
+ return () => {
100
+ clearInterval(resendTimerIntervalId.current);
101
+ };
102
+ }, []);
103
+
104
+ React.useEffect(() => {
105
+ if (timerSeconds < 1) {
106
+ clearInterval(resendTimerIntervalId.current);
107
+ setTimerSeconds(-1);
108
+ }
109
+ }, [timerSeconds]);
110
+
111
+ const handleResendCodeClick = () => {
112
+ setOtp('');
113
+ setTimerSeconds(30);
114
+ resendTimerIntervalId.current = setInterval(() => {
115
+ setTimerSeconds((timer) => timer - 1);
116
+ }, 1000);
117
+ onResendCode();
118
+ };
119
+
120
+ return (
121
+ <div className="pds2-space-y-8">
122
+ <ReactOTPInput
123
+ inputStyle="pds2-rounded-8 pds2-border-1 pds2-p-10 pds2-border-border-03 BodyRegular pds2-w-36 pds2-bg-bg-white pds2-text-text-04 placeholder:pds2-text-text-03 pds2-outline-none pds2-text-center"
124
+ inputType={inputType}
125
+ value={otp}
126
+ onChange={handleOtpChange}
127
+ numInputs={numberOfFields}
128
+ renderInput={(props) => Pds2OtpInput({ ...props, isError: errorMessage ? true : false })}
129
+ skipDefaultStyles={true}
130
+ renderSeparator={() => <div className="pds2-p-4" />}
131
+ shouldAutoFocus={true}
132
+ />
133
+ {errorMessage || timerSeconds === -1 ? (
134
+ <div className="pds2-flex pds2-flex-col pds2-items-center pds2-space-x-8">
135
+ {errorMessage ? <OtpErrorComponent errorMessage={errorMessage} /> : null}
136
+ {timerSeconds === -1 ? (
137
+ <ResendCodeComponent handleResendCode={handleResendCodeClick} />
138
+ ) : null}
139
+ </div>
140
+ ) : null}
141
+ {timerSeconds > -1 ? (
142
+ <ResendTimerComponent secondsRemaining={timerSeconds.toString().padStart(2, '0')} />
143
+ ) : null}
144
+ </div>
145
+ );
146
+ };
147
+
148
+ export default Pds2OtpComponent;
@@ -0,0 +1,25 @@
1
+ const AbhaHeader = ({
2
+ prefixIcon = <div className="pds2-w-24 pds2-h-24" />,
3
+ title,
4
+ suffixIcon = <div className="pds2-w-24 pds2-h-24" />,
5
+ className = '',
6
+ }: {
7
+ prefixIcon?: React.ReactNode;
8
+ title: string;
9
+ suffixIcon?: React.ReactNode;
10
+ className?: string;
11
+ }) => {
12
+ return (
13
+ <div
14
+ className={`pds2-flex pds2-items-center pds2-justify-between pds2-w-full pds2-p-16 pds2-sticky pds2-top-0 pds2-z-50 pds2-bg-bg-white ${className}`}
15
+ >
16
+ {prefixIcon}
17
+ <div className="pds2-flex-1 Heading4Semibold pds2-text-center pds2-text-text-black">
18
+ {title}
19
+ </div>
20
+ {suffixIcon}
21
+ </div>
22
+ );
23
+ };
24
+
25
+ export default AbhaHeader;
@@ -0,0 +1,27 @@
1
+ import AbhaBottomSheet from '../../molecules/abha/bottom-sheet';
2
+ import useAuthAbhaStore from '../../stores/auth-abha-store';
3
+ type TAbhaErrorBottomSheetProps = {
4
+ onSubmitClick: () => void;
5
+ };
6
+ const AbhaErrorBottomSheet = ({ onSubmitClick }: TAbhaErrorBottomSheetProps) => {
7
+ const bottomsheetErrorInfo = useAuthAbhaStore((state) => state.bottomsheetErrorInfo);
8
+ const setBottomsheetErrorInfo = useAuthAbhaStore((state) => state.setBottomsheetErrorInfo);
9
+ const handleCloseBottomsheet = () => {
10
+ setBottomsheetErrorInfo(null);
11
+ };
12
+
13
+ return (
14
+ <AbhaBottomSheet
15
+ description={bottomsheetErrorInfo?.title || 'Something went wrong'}
16
+ subText={bottomsheetErrorInfo?.description || 'Something went wrong'}
17
+ submitText={bottomsheetErrorInfo?.cta.title || 'Retry'}
18
+ onSubmitClick={onSubmitClick}
19
+ iconUrl={bottomsheetErrorInfo?.img}
20
+ isBottomSheetOpen={bottomsheetErrorInfo ? true : false}
21
+ onBottomSheetClose={handleCloseBottomsheet}
22
+ shouldCloseOnClickOutside={false}
23
+ />
24
+ );
25
+ };
26
+
27
+ export default AbhaErrorBottomSheet;
@@ -0,0 +1,99 @@
1
+ import { useEffect, useState } from 'react';
2
+ import SpacedInputComponent from '../../molecules/abha/spaced-input-component';
3
+
4
+ function formatTime(timeLeft: number): React.ReactNode {
5
+ const minutes = Math.floor(timeLeft / 60);
6
+ const seconds = timeLeft % 60;
7
+ return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
8
+ }
9
+
10
+ type TResendTimerProps = {
11
+ time: number;
12
+ onResendOtpClick: () => void;
13
+ error?: string | null;
14
+ };
15
+
16
+ const ResendTimer = ({ time, onResendOtpClick, error }: TResendTimerProps) => {
17
+ const [timeLeft, setTimeLeft] = useState(time);
18
+
19
+ useEffect(() => {
20
+ const interval = setInterval(() => {
21
+ setTimeLeft(timeLeft - 1);
22
+ }, 1000);
23
+ return () => clearInterval(interval);
24
+ }, [timeLeft]);
25
+
26
+ if (timeLeft > 0) {
27
+ return (
28
+ <div className="pds2-flex pds2-justify-between pds2-items-center pds2-w-full">
29
+ <div className="pds2-text-text-04">
30
+ <span className="Body3CapsSemibold">Resend OTP in</span>{' '}
31
+ <span className="Body3CapsSemibold">{formatTime(timeLeft)}</span>
32
+ </div>
33
+ {/* {error && <div className="Body3Semibold pds2-text-text-error">Invalid OTP</div>} */}
34
+ </div>
35
+ );
36
+ }
37
+
38
+ return (
39
+ <div className="pds2-flex pds2-justify-between pds2-items-center pds2-w-full">
40
+ <button
41
+ className="Body3CapsSemibold pds2-text-text-brand cursor-pointer"
42
+ onClick={() => {
43
+ setTimeLeft(time);
44
+ onResendOtpClick();
45
+ }}
46
+ >
47
+ Resend OTP
48
+ </button>
49
+ {/* {error && <div className="Body3Semibold pds2-text-text-error">Invalid OTP</div>} */}
50
+ </div>
51
+ );
52
+ };
53
+
54
+ type TAbhaOtpCardProps = {
55
+ otpSentToText: string;
56
+ onOtpCompletion: ({ otp }: { otp: string }) => void;
57
+ onResendOtpClick: () => void;
58
+ setIsValid: (isValid: boolean) => void;
59
+ error?: string | null;
60
+ prefixIcon?: React.ReactNode;
61
+ };
62
+
63
+ const AbhaOtpCard = ({
64
+ otpSentToText,
65
+ onOtpCompletion,
66
+ onResendOtpClick,
67
+ setIsValid,
68
+ error,
69
+ prefixIcon = (
70
+ <img
71
+ src="https://cdn.eka.care/vagus/cm5mcdm3h00070tfs9fjkepff.webp"
72
+ alt="adhaar"
73
+ className="pds2-w-48"
74
+ />
75
+ ),
76
+ }: TAbhaOtpCardProps) => {
77
+ return (
78
+ <div className="pds2-flex pds2-flex-col pds2-w-full pds2-p-16 pds2-rounded-16 pds2-bg-bg-01 pds2-space-y-8">
79
+ <div className="pds2-flex pds2-items-center pds2-space-x-16">
80
+ <div className="Body1Regular pds2-text-text-01 pds2-flex-1">
81
+ {otpSentToText}
82
+ <sup className="pds2-text-text-error Body3Semibold"> *</sup>
83
+ </div>
84
+ {prefixIcon}
85
+ </div>
86
+ <SpacedInputComponent
87
+ numberOfInputs={6}
88
+ length={1}
89
+ onSubmit={({ inputVal }) => onOtpCompletion({ otp: inputVal })}
90
+ setIsValid={setIsValid}
91
+ error={error}
92
+ />
93
+ <ResendTimer time={60} onResendOtpClick={onResendOtpClick} error={error} />
94
+ {error && <div className="Body3Semibold pds2-text-text-error">{error}</div>}
95
+ </div>
96
+ );
97
+ };
98
+
99
+ export default AbhaOtpCard;