@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.
|
|
4
|
+
"version": "0.8.236",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
7
7
|
"provenance": false
|