@pagamio/frontend-commons-lib 0.8.235 → 0.8.236

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.
@@ -0,0 +1,80 @@
1
+ import React from 'react';
2
+ export interface OtpVerificationConfig {
3
+ /** Number of OTP digits (default: 6) */
4
+ otpLength?: number;
5
+ /** Countdown duration in seconds before resend is allowed (default: 60) */
6
+ resendCountdown?: number;
7
+ }
8
+ export interface OtpVerificationText {
9
+ title: string;
10
+ subtitle: string;
11
+ subtitleEmail: string;
12
+ codeLabel: string;
13
+ verifyButton: string;
14
+ verifyingButton: string;
15
+ resendButton: string;
16
+ resendingButton: string;
17
+ resendCountdownText: string;
18
+ backButton: string;
19
+ }
20
+ export interface OtpVerificationHandlers {
21
+ /** Function to verify OTP - should throw on error */
22
+ verifyOtp: (params: {
23
+ email: string;
24
+ otpCode: string;
25
+ }) => Promise<any>;
26
+ /** Function to resend OTP - should throw on error */
27
+ resendOtp: (params: {
28
+ email: string;
29
+ }) => Promise<any>;
30
+ }
31
+ export interface OtpVerificationPageProps {
32
+ /** Email address to verify */
33
+ email: string;
34
+ /** Callback when verification is successful */
35
+ onVerifySuccess: (response?: any) => void;
36
+ /** Callback when user clicks back */
37
+ onBack: () => void;
38
+ /** OTP verification handlers */
39
+ handlers: OtpVerificationHandlers;
40
+ /** Optional logo configuration */
41
+ logo?: {
42
+ src: string;
43
+ alt: string;
44
+ width: number;
45
+ height: number;
46
+ };
47
+ /** Optional configuration */
48
+ config?: OtpVerificationConfig;
49
+ /** Optional custom text */
50
+ text?: Partial<OtpVerificationText>;
51
+ /** Optional custom class names */
52
+ classNames?: {
53
+ container?: string;
54
+ card?: string;
55
+ header?: string;
56
+ icon?: string;
57
+ };
58
+ }
59
+ export interface OtpInputProps {
60
+ /** Number of OTP digits */
61
+ length?: number;
62
+ /** Current OTP value */
63
+ value: string;
64
+ /** Callback when value changes */
65
+ onChange: (value: string) => void;
66
+ /** Whether input is disabled */
67
+ disabled?: boolean;
68
+ /** Optional custom class name for inputs */
69
+ inputClassName?: string;
70
+ }
71
+ export declare const otpVerificationDefaultText: OtpVerificationText;
72
+ /**
73
+ * Individual digit inputs for OTP entry with auto-focus and paste support
74
+ */
75
+ export declare const OtpInput: React.FC<OtpInputProps>;
76
+ /**
77
+ * Full-page OTP verification component with email display, countdown, and resend functionality
78
+ */
79
+ export declare const OtpVerificationPage: React.FC<OtpVerificationPageProps>;
80
+ export default OtpVerificationPage;
@@ -0,0 +1,143 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * OTP Verification Component
4
+ *
5
+ * A reusable component for email verification via OTP (One-Time Password).
6
+ * Supports customizable text, countdown timer for resend, and flexible callbacks.
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * <OtpVerificationPage
11
+ * email="user@example.com"
12
+ * onVerifySuccess={() => router.push('/dashboard')}
13
+ * onBack={() => setStep('login')}
14
+ * logo={{ src: '/logo.svg', alt: 'Logo', width: 150, height: 50 }}
15
+ * verifyOtp={async (params) => await api.verifyOtp(params)}
16
+ * resendOtp={async (params) => await api.resendOtp(params)}
17
+ * />
18
+ * ```
19
+ */
20
+ import { HiOutlineArrowLeft, HiOutlineMail } from 'react-icons/hi';
21
+ import { useEffect, useRef, useState } from 'react';
22
+ import { Button, Loader } from '../../components';
23
+ // ============================================
24
+ // DEFAULT TEXT
25
+ // ============================================
26
+ export const otpVerificationDefaultText = {
27
+ title: 'Verify Your Email',
28
+ subtitle: "We've sent a 6-digit verification code to",
29
+ subtitleEmail: 'Please enter the code sent to',
30
+ codeLabel: 'Verification Code',
31
+ verifyButton: 'Verify Email',
32
+ verifyingButton: 'Verifying...',
33
+ resendButton: 'Resend verification code',
34
+ resendingButton: 'Sending...',
35
+ resendCountdownText: 'Resend code in',
36
+ backButton: 'Back',
37
+ };
38
+ // ============================================
39
+ // OTP INPUT COMPONENT
40
+ // ============================================
41
+ /**
42
+ * Individual digit inputs for OTP entry with auto-focus and paste support
43
+ */
44
+ export const OtpInput = ({ length = 6, value, onChange, disabled = false, inputClassName, }) => {
45
+ const inputRefs = useRef([]);
46
+ const handleChange = (index, digit) => {
47
+ if (!/^\d*$/.test(digit))
48
+ return; // Only allow digits
49
+ const newValue = value.split('');
50
+ newValue[index] = digit.slice(-1); // Take only last character
51
+ const updatedValue = newValue.join('');
52
+ onChange(updatedValue.slice(0, length));
53
+ // Auto-focus next input
54
+ if (digit && index < length - 1) {
55
+ inputRefs.current[index + 1]?.focus();
56
+ }
57
+ };
58
+ const handleKeyDown = (index, e) => {
59
+ if (e.key === 'Backspace' && !value[index] && index > 0) {
60
+ inputRefs.current[index - 1]?.focus();
61
+ }
62
+ };
63
+ const handlePaste = (e) => {
64
+ e.preventDefault();
65
+ const pastedData = e.clipboardData.getData('text').replace(/\D/g, '');
66
+ onChange(pastedData.slice(0, length));
67
+ // Focus the appropriate input after paste
68
+ const focusIndex = Math.min(pastedData.length, length - 1);
69
+ inputRefs.current[focusIndex]?.focus();
70
+ };
71
+ const defaultInputClassName = 'h-12 w-12 rounded-lg border border-gray-300 text-center text-lg font-semibold focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20 disabled:bg-gray-100 disabled:text-gray-400';
72
+ return (_jsx("div", { className: "flex justify-center gap-2", children: Array.from({ length }).map((_, index) => (_jsx("input", { ref: (el) => {
73
+ inputRefs.current[index] = el;
74
+ }, type: "text", inputMode: "numeric", maxLength: 1, value: value[index] || '', onChange: (e) => handleChange(index, e.target.value), onKeyDown: (e) => handleKeyDown(index, e), onPaste: handlePaste, disabled: disabled, className: inputClassName ?? defaultInputClassName, autoComplete: "one-time-code", "aria-label": `Digit ${index + 1} of ${length}` }, `otp-input-${index}`))) }));
75
+ };
76
+ // ============================================
77
+ // OTP VERIFICATION PAGE COMPONENT
78
+ // ============================================
79
+ /**
80
+ * Full-page OTP verification component with email display, countdown, and resend functionality
81
+ */
82
+ export const OtpVerificationPage = ({ email, onVerifySuccess, onBack, handlers, logo, config = {}, text: customText = {}, classNames = {}, }) => {
83
+ const { otpLength = 6, resendCountdown = 60 } = config;
84
+ const text = { ...otpVerificationDefaultText, ...customText };
85
+ const [otpValue, setOtpValue] = useState('');
86
+ const [countdown, setCountdown] = useState(resendCountdown);
87
+ const [canResend, setCanResend] = useState(false);
88
+ const [isVerifying, setIsVerifying] = useState(false);
89
+ const [isResending, setIsResending] = useState(false);
90
+ const [error, setError] = useState(null);
91
+ // Countdown timer for resend
92
+ useEffect(() => {
93
+ if (countdown > 0) {
94
+ const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
95
+ return () => clearTimeout(timer);
96
+ }
97
+ else {
98
+ setCanResend(true);
99
+ }
100
+ }, [countdown]);
101
+ const clearError = () => setError(null);
102
+ const handleVerify = async () => {
103
+ if (otpValue.length !== otpLength)
104
+ return;
105
+ setIsVerifying(true);
106
+ clearError();
107
+ try {
108
+ const response = await handlers.verifyOtp({ email, otpCode: otpValue });
109
+ onVerifySuccess(response);
110
+ }
111
+ catch (err) {
112
+ const errorMessage = err?.response?.data?.message || err?.message || 'Verification failed. Please try again.';
113
+ setError(errorMessage);
114
+ }
115
+ finally {
116
+ setIsVerifying(false);
117
+ }
118
+ };
119
+ const handleResend = async () => {
120
+ if (!canResend || isResending)
121
+ return;
122
+ setIsResending(true);
123
+ clearError();
124
+ try {
125
+ await handlers.resendOtp({ email });
126
+ setCountdown(resendCountdown);
127
+ setCanResend(false);
128
+ setOtpValue('');
129
+ }
130
+ catch (err) {
131
+ const errorMessage = err?.response?.data?.message || err?.message || 'Failed to resend code. Please try again.';
132
+ setError(errorMessage);
133
+ }
134
+ finally {
135
+ setIsResending(false);
136
+ }
137
+ };
138
+ return (_jsx("div", { className: `flex min-h-screen items-center justify-center bg-gray-50 px-4 py-12 ${classNames.container ?? ''}`, children: _jsxs("div", { className: "w-full max-w-md", children: [logo && (_jsx("div", { className: "mb-8 flex justify-center", children: _jsx("img", { src: logo.src, alt: logo.alt, width: logo.width, height: logo.height, className: "h-auto max-h-16" }) })), _jsxs("div", { className: `rounded-2xl border border-gray-200 bg-white p-8 shadow-sm ${classNames.card ?? ''}`, children: [_jsxs("div", { className: `mb-6 text-center ${classNames.header ?? ''}`, children: [_jsx("div", { className: `mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-primary-100 ${classNames.icon ?? ''}`, children: _jsx(HiOutlineMail, { className: "h-8 w-8 text-primary-600" }) }), _jsx("h1", { className: "text-2xl font-bold text-gray-900", children: text.title }), _jsx("p", { className: "mt-2 text-sm text-gray-600", children: text.subtitle }), _jsx("p", { className: "mt-1 font-medium text-gray-900", children: email })] }), error && _jsx("div", { className: "mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-600", children: error }), _jsxs("div", { className: "mb-6", children: [_jsx("p", { className: "mb-2 block text-sm font-medium text-gray-700", children: text.codeLabel }), _jsx(OtpInput, { length: otpLength, value: otpValue, onChange: (value) => {
139
+ setOtpValue(value);
140
+ clearError();
141
+ }, disabled: isVerifying })] }), _jsx(Button, { onClick: handleVerify, disabled: otpValue.length !== otpLength || isVerifying, className: "w-full", size: "lg", children: isVerifying ? (_jsxs(_Fragment, { children: [_jsx(Loader, { size: "sm", className: "mr-2" }), text.verifyingButton] })) : (text.verifyButton) }), _jsx("div", { className: "mt-6 text-center", children: canResend ? (_jsx("button", { onClick: handleResend, disabled: isResending, className: "text-sm font-medium text-primary-600 hover:text-primary-500 disabled:text-gray-400", children: isResending ? text.resendingButton : text.resendButton })) : (_jsxs("p", { className: "text-sm text-gray-500", children: [text.resendCountdownText, " ", _jsxs("span", { className: "font-medium text-gray-700", children: [countdown, "s"] })] })) }), _jsx("div", { className: "mt-6 border-t border-gray-200 pt-6", children: _jsxs("button", { onClick: onBack, className: "flex w-full items-center justify-center gap-2 text-sm text-gray-600 hover:text-gray-900", children: [_jsx(HiOutlineArrowLeft, { className: "h-4 w-4" }), text.backButton] }) })] })] }) }));
142
+ };
143
+ export default OtpVerificationPage;
@@ -6,3 +6,4 @@ export { default as PagamioCustomerRegistrationPage, customerRegistrationPageDef
6
6
  export { default as PagamioForgotPasswordPage, forgotPasswordDefaultText, type PagamioForgotPasswordPageProps, } from './ForgotPasswordPage';
7
7
  export { default as PagamioResetPasswordPage, resetPasswordDefaultText, type PagamioResetPasswordPageProps, } from './ResetPasswordPage';
8
8
  export { AuthPageLayout as PagamioAuthPageLayout } from './AuthPageLayout';
9
+ export { default as OtpVerificationPage, OtpInput, otpVerificationDefaultText, type OtpInputProps, type OtpVerificationConfig, type OtpVerificationHandlers, type OtpVerificationPageProps, type OtpVerificationText, } from './OtpVerification';
@@ -5,3 +5,4 @@ export { default as PagamioCustomerRegistrationPage, customerRegistrationPageDef
5
5
  export { default as PagamioForgotPasswordPage, forgotPasswordDefaultText, } from './ForgotPasswordPage';
6
6
  export { default as PagamioResetPasswordPage, resetPasswordDefaultText, } from './ResetPasswordPage';
7
7
  export { AuthPageLayout as PagamioAuthPageLayout } from './AuthPageLayout';
8
+ export { default as OtpVerificationPage, OtpInput, otpVerificationDefaultText, } from './OtpVerification';
package/lib/styles.css CHANGED
@@ -1425,6 +1425,9 @@ input[type="range"]::-ms-fill-lower {
1425
1425
  .mb-6 {
1426
1426
  margin-bottom: 1.5rem;
1427
1427
  }
1428
+ .mb-8 {
1429
+ margin-bottom: 2rem;
1430
+ }
1428
1431
  .mb-\[2\.5em\] {
1429
1432
  margin-bottom: 2.5em;
1430
1433
  }
@@ -1673,6 +1676,9 @@ input[type="range"]::-ms-fill-lower {
1673
1676
  .h-screen {
1674
1677
  height: 100vh;
1675
1678
  }
1679
+ .max-h-16 {
1680
+ max-height: 4rem;
1681
+ }
1676
1682
  .max-h-60 {
1677
1683
  max-height: 15rem;
1678
1684
  }
@@ -2298,6 +2304,9 @@ input[type="range"]::-ms-fill-lower {
2298
2304
  .rounded {
2299
2305
  border-radius: 0.25rem;
2300
2306
  }
2307
+ .rounded-2xl {
2308
+ border-radius: 1rem;
2309
+ }
2301
2310
  .rounded-\[10px\] {
2302
2311
  border-radius: 10px;
2303
2312
  }
@@ -3179,6 +3188,10 @@ input[type="range"]::-ms-fill-lower {
3179
3188
  padding-top: 0.375rem;
3180
3189
  padding-bottom: 0.375rem;
3181
3190
  }
3191
+ .py-12 {
3192
+ padding-top: 3rem;
3193
+ padding-bottom: 3rem;
3194
+ }
3182
3195
  .py-2 {
3183
3196
  padding-top: 0.5rem;
3184
3197
  padding-bottom: 0.5rem;
@@ -3291,6 +3304,9 @@ input[type="range"]::-ms-fill-lower {
3291
3304
  .pt-5 {
3292
3305
  padding-top: 1.25rem;
3293
3306
  }
3307
+ .pt-6 {
3308
+ padding-top: 1.5rem;
3309
+ }
3294
3310
  .text-left {
3295
3311
  text-align: left;
3296
3312
  }
@@ -4761,6 +4777,10 @@ input[type="range"]::-ms-fill-lower {
4761
4777
  .disabled\:cursor-not-allowed:disabled {
4762
4778
  cursor: not-allowed;
4763
4779
  }
4780
+ .disabled\:bg-gray-100:disabled {
4781
+ --tw-bg-opacity: 1;
4782
+ background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
4783
+ }
4764
4784
  .disabled\:bg-gray-50:disabled {
4765
4785
  --tw-bg-opacity: 1;
4766
4786
  background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pagamio/frontend-commons-lib",
3
3
  "description": "Pagamio library for Frontend reusable components like the form engine and table container",
4
- "version": "0.8.235",
4
+ "version": "0.8.236",
5
5
  "publishConfig": {
6
6
  "access": "public",
7
7
  "provenance": false