@pagamio/frontend-commons-lib 0.8.278 → 0.8.280
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/lib/auth/components/CustomerRegistrationPage.d.ts +12 -1
- package/lib/auth/components/CustomerRegistrationPage.js +6 -3
- package/lib/auth/components/LoginPage.d.ts +22 -1
- package/lib/auth/components/LoginPage.js +108 -7
- package/lib/auth/components/OtpVerification.d.ts +20 -1
- package/lib/auth/components/OtpVerification.js +17 -5
- package/lib/auth/components/index.d.ts +2 -2
- package/lib/form-engine/FormEngine.js +14 -3
- package/lib/form-engine/components/FieldWrapper.js +10 -3
- package/package.json +1 -1
|
@@ -46,6 +46,17 @@ interface PagamioCustomerRegistrationPageProps<T extends CustomAuthConfig> exten
|
|
|
46
46
|
customRegistrationFields?: Field[];
|
|
47
47
|
/** Custom data mapper function to transform form data before sending it to authService.register */
|
|
48
48
|
mapFormDataToRegistration?: (formData: Record<string, any>) => Record<string, any>;
|
|
49
|
+
/**
|
|
50
|
+
* When provided, completely overrides the default authService.register() call.
|
|
51
|
+
* Useful for custom auth flows (e.g. phone registration).
|
|
52
|
+
* Should throw on error.
|
|
53
|
+
*/
|
|
54
|
+
customSubmitHandler?: (formData: Record<string, any>) => Promise<any>;
|
|
55
|
+
/**
|
|
56
|
+
* Optional content rendered above the form inside the card.
|
|
57
|
+
* Useful for tab switchers or banners.
|
|
58
|
+
*/
|
|
59
|
+
headerContent?: React.ReactNode;
|
|
49
60
|
/** Callback handlers */
|
|
50
61
|
onRegistrationSuccess?: (authResponse?: AuthResponse<T>) => void;
|
|
51
62
|
onRegistrationError?: (error: Error) => void;
|
|
@@ -80,6 +91,6 @@ export declare const customerRegistrationPageDefaultText: {
|
|
|
80
91
|
* Customer Registration Page component
|
|
81
92
|
* @template T - Authentication configuration type
|
|
82
93
|
*/
|
|
83
|
-
export declare function PagamioCustomerRegistrationPage<T extends CustomAuthConfig>({ logo, text, customRegistrationFields, mapFormDataToRegistration, appLabel, onRegistrationSuccess, onRegistrationError, onBackToLogin, className, features, sideContentClass, footer, }: Readonly<PagamioCustomerRegistrationPageProps<T>>): import("react/jsx-runtime").JSX.Element;
|
|
94
|
+
export declare function PagamioCustomerRegistrationPage<T extends CustomAuthConfig>({ logo, text, customRegistrationFields, mapFormDataToRegistration, customSubmitHandler, headerContent, appLabel, onRegistrationSuccess, onRegistrationError, onBackToLogin, className, features, sideContentClass, footer, }: Readonly<PagamioCustomerRegistrationPageProps<T>>): import("react/jsx-runtime").JSX.Element;
|
|
84
95
|
export default PagamioCustomerRegistrationPage;
|
|
85
96
|
export type { PagamioCustomerRegistrationPageProps };
|
|
@@ -46,7 +46,7 @@ export const customerRegistrationPageDefaultText = {
|
|
|
46
46
|
* Customer Registration Page component
|
|
47
47
|
* @template T - Authentication configuration type
|
|
48
48
|
*/
|
|
49
|
-
export function PagamioCustomerRegistrationPage({ logo, text = customerRegistrationPageDefaultText, customRegistrationFields, mapFormDataToRegistration, appLabel, onRegistrationSuccess, onRegistrationError, onBackToLogin, className = '', features, sideContentClass, footer, }) {
|
|
49
|
+
export function PagamioCustomerRegistrationPage({ logo, text = customerRegistrationPageDefaultText, customRegistrationFields, mapFormDataToRegistration, customSubmitHandler, headerContent, appLabel, onRegistrationSuccess, onRegistrationError, onBackToLogin, className = '', features, sideContentClass, footer, }) {
|
|
50
50
|
const { authService, error } = useAuth();
|
|
51
51
|
const { addToast } = useToast();
|
|
52
52
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -120,6 +120,10 @@ export function PagamioCustomerRegistrationPage({ logo, text = customerRegistrat
|
|
|
120
120
|
onSuccess: (response) => onRegistrationSuccess?.(response),
|
|
121
121
|
onError: onRegistrationError,
|
|
122
122
|
}, async (data) => {
|
|
123
|
+
// Custom submit handler takes full precedence
|
|
124
|
+
if (customSubmitHandler) {
|
|
125
|
+
return customSubmitHandler(data);
|
|
126
|
+
}
|
|
123
127
|
// If custom mapper is provided, use it to transform the form data
|
|
124
128
|
const registrationData = mapFormDataToRegistration ? mapFormDataToRegistration(data) : data;
|
|
125
129
|
// For backward compatibility, extract specific fields if no custom mapper is provided
|
|
@@ -133,10 +137,9 @@ export function PagamioCustomerRegistrationPage({ logo, text = customerRegistrat
|
|
|
133
137
|
phoneNumber: phone ?? '',
|
|
134
138
|
});
|
|
135
139
|
}
|
|
136
|
-
console.log('Registration data:', registrationData);
|
|
137
140
|
// Use the transformed data directly when custom mapper is provided
|
|
138
141
|
return authService.register(registrationData);
|
|
139
142
|
}, 'Registration successful', 'Registration failed. Please try again.');
|
|
140
|
-
return (_jsx(AuthPageLayout, { title: text.registerTitle, subtitle: text.registerSubtitle, errorMessage: error?.message, logo: logo, appLabel: appLabel, className: className, horizontal: false, sideContent: features && features.length > 0 ? _jsx(FeatureCarousel, { features: features }) : undefined, sideContentClass: sideContentClass, footer: footer, children: _jsxs("div", { className: "mt-8", children: [_jsx(FormEngine, { fields: customRegistrationFields ?? registrationFields, onSubmit: handleSubmit, layout: "vertical", className: "mb-0 px-0", submitButtonClass: "w-full", submitButtonText: isLoading ? text.loadingButtonLabel : text.registerButtonLabel, onCancel: () => { }, showCancelButton: false, showSubmittingText: false, formRef: formRef }), onBackToLogin && (_jsxs("div", { className: "mt-6 text-center text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "Already have an account? " }), _jsx(Button, { type: "button", variant: "link", onClick: onBackToLogin, className: "font-medium text-primary hover:underline", children: text.backToLoginLabel })] }))] }) }));
|
|
143
|
+
return (_jsx(AuthPageLayout, { title: text.registerTitle, subtitle: text.registerSubtitle, errorMessage: error?.message, logo: logo, appLabel: appLabel, className: className, horizontal: false, sideContent: features && features.length > 0 ? _jsx(FeatureCarousel, { features: features }) : undefined, sideContentClass: sideContentClass, footer: footer, children: _jsxs("div", { className: "mt-8", children: [headerContent, _jsx(FormEngine, { fields: customRegistrationFields ?? registrationFields, onSubmit: handleSubmit, layout: "vertical", className: "mb-0 px-0", submitButtonClass: "w-full", submitButtonText: isLoading ? text.loadingButtonLabel : text.registerButtonLabel, onCancel: () => { }, showCancelButton: false, showSubmittingText: false, formRef: formRef }), onBackToLogin && (_jsxs("div", { className: "mt-6 text-center text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "Already have an account? " }), _jsx(Button, { type: "button", variant: "link", onClick: onBackToLogin, className: "font-medium text-primary hover:underline", children: text.backToLoginLabel })] }))] }) }));
|
|
141
144
|
}
|
|
142
145
|
export default PagamioCustomerRegistrationPage;
|
|
@@ -29,6 +29,22 @@ import type { BaseAuthPageProps } from './AuthFormUtils';
|
|
|
29
29
|
* Login field type - determines whether the login form uses username or email
|
|
30
30
|
*/
|
|
31
31
|
export type LoginFieldType = 'username' | 'email';
|
|
32
|
+
/**
|
|
33
|
+
* Configuration for the phone OTP login tab.
|
|
34
|
+
* When provided, a "Phone + OTP" tab is displayed alongside the password tab.
|
|
35
|
+
*/
|
|
36
|
+
export interface PhoneOtpLoginConfig {
|
|
37
|
+
/** Send OTP to the given phone number. Should throw on error. */
|
|
38
|
+
sendOtp: (phoneNumber: string) => Promise<any>;
|
|
39
|
+
/** Verify OTP for the given phone number. Should throw on error. */
|
|
40
|
+
verifyOtp: (phoneNumber: string, otpCode: string) => Promise<any>;
|
|
41
|
+
/** Optional countdown seconds before resend is allowed (default: 60) */
|
|
42
|
+
resendCountdown?: number;
|
|
43
|
+
/** Optional label for the phone-OTP tab (default: 'Phone + OTP') */
|
|
44
|
+
tabLabel?: string;
|
|
45
|
+
/** Optional label for the password tab (default: 'Email & Password') */
|
|
46
|
+
passwordTabLabel?: string;
|
|
47
|
+
}
|
|
32
48
|
/**
|
|
33
49
|
* Base login credentials interface that can be extended for specific implementations
|
|
34
50
|
*/
|
|
@@ -100,6 +116,11 @@ interface PagamioLoginPageProps<T extends CustomAuthConfig> extends BaseAuthPage
|
|
|
100
116
|
sideContentClass?: string;
|
|
101
117
|
/** Footer content */
|
|
102
118
|
footer?: React.ReactNode;
|
|
119
|
+
/**
|
|
120
|
+
* When provided, adds a "Phone + OTP" tab alongside the standard login form.
|
|
121
|
+
* The tab handles the full 2-step phone OTP flow internally.
|
|
122
|
+
*/
|
|
123
|
+
phoneOtpConfig?: PhoneOtpLoginConfig;
|
|
103
124
|
}
|
|
104
125
|
export interface LoginErrorProps {
|
|
105
126
|
code: string;
|
|
@@ -121,6 +142,6 @@ export declare const loginPageDefaultText: {
|
|
|
121
142
|
* Generic Login Page component
|
|
122
143
|
* @template T - Authentication configuration type
|
|
123
144
|
*/
|
|
124
|
-
export declare function PagamioLoginPage<T extends CustomAuthConfig>({ logo, text, appLabel, onForgotPassword, onLoginSuccess, onLoginError, hasCreateAccount, createAccountRoute, onCreateAccount, transformLoginData, authenticatorType, loginFieldType, customLoginFields, className, features, sideContentClass, footer, }: Readonly<PagamioLoginPageProps<T>>): import("react/jsx-runtime").JSX.Element;
|
|
145
|
+
export declare function PagamioLoginPage<T extends CustomAuthConfig>({ logo, text, appLabel, onForgotPassword, onLoginSuccess, onLoginError, hasCreateAccount, createAccountRoute, onCreateAccount, transformLoginData, authenticatorType, loginFieldType, customLoginFields, className, features, sideContentClass, footer, phoneOtpConfig, }: Readonly<PagamioLoginPageProps<T>>): import("react/jsx-runtime").JSX.Element;
|
|
125
146
|
export default PagamioLoginPage;
|
|
126
147
|
export type { PagamioLoginCredentials, PagamioLoginPageProps };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* Generic Login Page component that works with different authentication configurations.
|
|
4
4
|
* Provides a customizable login interface with support for different branding and text options.
|
|
@@ -30,6 +30,7 @@ import { useAuth } from '../context';
|
|
|
30
30
|
import { createFormSubmissionHandler } from './AuthFormUtils';
|
|
31
31
|
import { AuthPageLayout } from './AuthPageLayout';
|
|
32
32
|
import { FeatureCarousel } from './FeatureCarousel';
|
|
33
|
+
import { OtpInput } from './OtpVerification';
|
|
33
34
|
export const loginPageDefaultText = {
|
|
34
35
|
welcomeTitle: 'Welcome Back!',
|
|
35
36
|
welcomeSubtitle: 'Sign in to your account to continue',
|
|
@@ -46,12 +47,99 @@ export const loginPageDefaultText = {
|
|
|
46
47
|
* Generic Login Page component
|
|
47
48
|
* @template T - Authentication configuration type
|
|
48
49
|
*/
|
|
49
|
-
export function PagamioLoginPage({ logo, text = loginPageDefaultText, appLabel, onForgotPassword, onLoginSuccess, onLoginError, hasCreateAccount = false, createAccountRoute, onCreateAccount, transformLoginData, authenticatorType, loginFieldType = 'username', customLoginFields, className = '', features, sideContentClass, footer, }) {
|
|
50
|
+
export function PagamioLoginPage({ logo, text = loginPageDefaultText, appLabel, onForgotPassword, onLoginSuccess, onLoginError, hasCreateAccount = false, createAccountRoute, onCreateAccount, transformLoginData, authenticatorType, loginFieldType = 'username', customLoginFields, className = '', features, sideContentClass, footer, phoneOtpConfig, }) {
|
|
50
51
|
const { login, error: authError } = useAuth();
|
|
51
52
|
const { addToast } = useToast();
|
|
52
53
|
const [isLoading, setIsLoading] = useState(false);
|
|
53
54
|
const [error, setError] = useState(null);
|
|
54
55
|
const formRef = useRef(null);
|
|
56
|
+
const [activeTab, setActiveTab] = useState('password');
|
|
57
|
+
const [phoneStep, setPhoneStep] = useState('enterPhone');
|
|
58
|
+
const [phoneNumber, setPhoneNumber] = useState('');
|
|
59
|
+
const [phoneOtpValue, setPhoneOtpValue] = useState('');
|
|
60
|
+
const [isSendingOtp, setIsSendingOtp] = useState(false);
|
|
61
|
+
const [isVerifyingOtp, setIsVerifyingOtp] = useState(false);
|
|
62
|
+
const [isResendingOtp, setIsResendingOtp] = useState(false);
|
|
63
|
+
const [phoneError, setPhoneError] = useState(null);
|
|
64
|
+
const [phoneCountdown, setPhoneCountdown] = useState(0);
|
|
65
|
+
const [canResendPhone, setCanResendPhone] = useState(true);
|
|
66
|
+
const startCountdown = (seconds) => {
|
|
67
|
+
setPhoneCountdown(seconds);
|
|
68
|
+
setCanResendPhone(false);
|
|
69
|
+
const tick = () => {
|
|
70
|
+
setPhoneCountdown((prev) => {
|
|
71
|
+
if (prev <= 1) {
|
|
72
|
+
setCanResendPhone(true);
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
setTimeout(tick, 1000);
|
|
76
|
+
return prev - 1;
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
setTimeout(tick, 1000);
|
|
80
|
+
};
|
|
81
|
+
const phoneNumberField = [
|
|
82
|
+
{
|
|
83
|
+
name: 'phoneNumber',
|
|
84
|
+
label: 'Phone Number',
|
|
85
|
+
type: 'tel',
|
|
86
|
+
placeholder: '+27 82 123 4567',
|
|
87
|
+
gridSpan: 12,
|
|
88
|
+
defaultCountry: 'ZA',
|
|
89
|
+
validation: { required: 'Phone number is required' },
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
const handlePhoneFormSubmit = async (data) => {
|
|
93
|
+
const phone = data.phoneNumber;
|
|
94
|
+
setIsSendingOtp(true);
|
|
95
|
+
setPhoneError(null);
|
|
96
|
+
try {
|
|
97
|
+
await phoneOtpConfig.sendOtp(phone);
|
|
98
|
+
setPhoneNumber(phone);
|
|
99
|
+
setPhoneStep('enterOtp');
|
|
100
|
+
startCountdown(phoneOtpConfig.resendCountdown ?? 60);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
setPhoneError(err?.response?.data?.message ?? err?.message ?? 'Failed to send OTP. Please try again.');
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
setIsSendingOtp(false);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const handleResendPhoneOtp = async () => {
|
|
111
|
+
if (!canResendPhone || isResendingOtp)
|
|
112
|
+
return;
|
|
113
|
+
setIsResendingOtp(true);
|
|
114
|
+
setPhoneError(null);
|
|
115
|
+
try {
|
|
116
|
+
await phoneOtpConfig.sendOtp(phoneNumber);
|
|
117
|
+
startCountdown(phoneOtpConfig.resendCountdown ?? 60);
|
|
118
|
+
setPhoneOtpValue('');
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
setPhoneError(err?.response?.data?.message ?? err?.message ?? 'Failed to resend OTP.');
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
setIsResendingOtp(false);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const handleVerifyPhoneOtp = async () => {
|
|
128
|
+
if (phoneOtpValue.length !== 6)
|
|
129
|
+
return;
|
|
130
|
+
setIsVerifyingOtp(true);
|
|
131
|
+
setPhoneError(null);
|
|
132
|
+
try {
|
|
133
|
+
const response = await phoneOtpConfig.verifyOtp(phoneNumber, phoneOtpValue);
|
|
134
|
+
onLoginSuccess?.(response);
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
setPhoneError(err?.response?.data?.message ?? err?.message ?? 'Verification failed. Please try again.');
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
setIsVerifyingOtp(false);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
55
143
|
// Generate the identifier field based on loginFieldType
|
|
56
144
|
const identifierField = loginFieldType === 'email'
|
|
57
145
|
? {
|
|
@@ -136,10 +224,23 @@ export function PagamioLoginPage({ logo, text = loginPageDefaultText, appLabel,
|
|
|
136
224
|
window.location.href = createAccountRoute;
|
|
137
225
|
}
|
|
138
226
|
};
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
227
|
+
const passwordTabLabel = phoneOtpConfig?.passwordTabLabel ?? 'Email & Password';
|
|
228
|
+
const phoneTabLabel = phoneOtpConfig?.tabLabel ?? 'Phone + OTP';
|
|
229
|
+
return (_jsx(AuthPageLayout, { appLabel: appLabel, title: text.welcomeTitle, subtitle: text.welcomeSubtitle, errorMessage: activeTab === 'password' ? (error ?? authError?.message) : undefined, logo: logo, className: className, horizontal: false, sideContent: features && features.length > 0 ? _jsx(FeatureCarousel, { features: features }) : undefined, sideContentClass: sideContentClass, footer: footer, children: _jsxs("div", { className: "mt-8", children: [phoneOtpConfig && (_jsxs("div", { className: "mb-6 flex rounded-lg border border-border overflow-hidden", children: [_jsx("button", { type: "button", onClick: () => setActiveTab('password'), className: `flex-1 py-2 text-sm font-medium transition-colors ${activeTab === 'password' ? 'bg-primary text-primary-foreground' : 'bg-background text-foreground/70 hover:bg-muted'}`, children: passwordTabLabel }), _jsx("button", { type: "button", onClick: () => {
|
|
230
|
+
setActiveTab('phone');
|
|
231
|
+
setPhoneStep('enterPhone');
|
|
232
|
+
setPhoneError(null);
|
|
233
|
+
}, className: `flex-1 py-2 text-sm font-medium transition-colors ${activeTab === 'phone' ? 'bg-primary text-primary-foreground' : 'bg-background text-foreground/70 hover:bg-muted'}`, children: phoneTabLabel })] })), activeTab === 'password' && (_jsxs(_Fragment, { children: [_jsx(FormEngine, { fields: loginFields, onSubmit: handleSubmit, layout: "vertical", className: "mb-0 px-0", submitButtonClass: "w-full", submitButtonText: isLoading ? text.loadingButtonLabel : text.loginButtonLabel, onCancel: () => { }, showCancelButton: false, showSubmittingText: false, formRef: formRef, initialValues: {
|
|
234
|
+
...(loginFieldType === 'email' ? { email: '' } : { username: '' }),
|
|
235
|
+
password: '',
|
|
236
|
+
rememberMe: false,
|
|
237
|
+
} }), _jsxs("div", { className: "flex items-center justify-center gap-x-4 mt-4", children: [onForgotPassword && (_jsx(Button, { type: "button", variant: "link", onClick: onForgotPassword, className: "text-sm text-primary hover:underline", children: text.forgotPasswordLabel })), hasCreateAccount && (_jsxs(_Fragment, { children: [onForgotPassword && _jsx("span", { className: "text-muted-foreground", children: "|" }), _jsx(Button, { type: "button", variant: "link", onClick: handleCreateAccount, className: "text-sm text-primary hover:underline", children: text.createAccountLabel })] }))] })] })), activeTab === 'phone' && phoneOtpConfig && (_jsxs("div", { className: "space-y-4", children: [phoneError && _jsx("div", { className: "rounded-lg bg-red-50 p-4 text-sm text-red-600", children: phoneError }), phoneStep === 'enterPhone' && (_jsxs(_Fragment, { children: [_jsx(FormEngine, { fields: phoneNumberField, onSubmit: handlePhoneFormSubmit, layout: "vertical", className: "mb-0 px-0", submitButtonClass: "w-full", submitButtonText: isSendingOtp ? 'Sending code...' : 'Send code', onCancel: () => { }, showCancelButton: false, showSubmittingText: false, initialValues: { phoneNumber: '' } }), hasCreateAccount && (_jsx("div", { className: "text-center mt-2", children: _jsx(Button, { type: "button", variant: "link", onClick: handleCreateAccount, className: "text-sm text-primary hover:underline", children: text.createAccountLabel }) }))] })), phoneStep === 'enterOtp' && (_jsxs(_Fragment, { children: [_jsxs("p", { className: "text-sm text-muted-foreground text-center", children: ["Enter the 6-digit code sent to ", _jsx("span", { className: "font-medium text-foreground", children: phoneNumber })] }), _jsxs("div", { children: [_jsx("p", { className: "mb-2 block text-sm font-medium text-foreground text-center", children: "Verification Code" }), _jsx(OtpInput, { length: 6, value: phoneOtpValue, onChange: (v) => {
|
|
238
|
+
setPhoneOtpValue(v);
|
|
239
|
+
setPhoneError(null);
|
|
240
|
+
}, disabled: isVerifyingOtp })] }), _jsx(Button, { type: "button", onClick: handleVerifyPhoneOtp, disabled: phoneOtpValue.length !== 6 || isVerifyingOtp, className: "w-full", size: "lg", children: isVerifyingOtp ? 'Verifying...' : 'Verify & Sign In' }), _jsx("div", { className: "text-center text-sm text-muted-foreground", children: canResendPhone ? (_jsx(Button, { type: "button", variant: "ghost", onClick: handleResendPhoneOtp, disabled: isResendingOtp, className: "text-sm font-medium text-primary hover:text-primary/90", children: isResendingOtp ? 'Sending...' : 'Resend code' })) : (_jsxs("span", { children: ["Resend code in ", _jsxs("span", { className: "font-medium text-foreground", children: [phoneCountdown, "s"] })] })) }), _jsx(Button, { type: "button", variant: "ghost", onClick: () => {
|
|
241
|
+
setPhoneStep('enterPhone');
|
|
242
|
+
setPhoneOtpValue('');
|
|
243
|
+
setPhoneError(null);
|
|
244
|
+
}, className: "w-full text-sm text-foreground/70", children: "\u2190 Change number" })] }))] }))] }) }));
|
|
144
245
|
}
|
|
145
246
|
export default PagamioLoginPage;
|
|
@@ -28,15 +28,34 @@ export interface OtpVerificationHandlers {
|
|
|
28
28
|
email: string;
|
|
29
29
|
}) => Promise<any>;
|
|
30
30
|
}
|
|
31
|
+
export interface PhoneOtpVerificationHandlers {
|
|
32
|
+
/** Function to verify OTP using phone number - should throw on error */
|
|
33
|
+
verifyOtp: (params: {
|
|
34
|
+
phoneNumber: string;
|
|
35
|
+
otpCode: string;
|
|
36
|
+
}) => Promise<any>;
|
|
37
|
+
/** Function to resend OTP using phone number - should throw on error */
|
|
38
|
+
resendOtp: (params: {
|
|
39
|
+
phoneNumber: string;
|
|
40
|
+
}) => Promise<any>;
|
|
41
|
+
}
|
|
31
42
|
export interface OtpVerificationPageProps {
|
|
32
43
|
/** Email address to verify */
|
|
33
44
|
email: string;
|
|
45
|
+
/**
|
|
46
|
+
* Phone number to verify (E.164 format).
|
|
47
|
+
* When provided alongside `phoneHandlers`, the component switches to phone OTP mode:
|
|
48
|
+
* shows the phone number instead of email and calls phoneHandlers for verify/resend.
|
|
49
|
+
*/
|
|
50
|
+
phoneNumber?: string;
|
|
34
51
|
/** Callback when verification is successful */
|
|
35
52
|
onVerifySuccess: (response?: any) => void;
|
|
36
53
|
/** Callback when user clicks back */
|
|
37
54
|
onBack: () => void;
|
|
38
|
-
/** OTP verification handlers */
|
|
55
|
+
/** OTP verification handlers (email-based) */
|
|
39
56
|
handlers: OtpVerificationHandlers;
|
|
57
|
+
/** Phone OTP handlers — required when `phoneNumber` is provided */
|
|
58
|
+
phoneHandlers?: PhoneOtpVerificationHandlers;
|
|
40
59
|
/** Optional logo configuration */
|
|
41
60
|
logo?: {
|
|
42
61
|
src: string;
|
|
@@ -17,7 +17,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
17
17
|
* />
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
|
-
import { HiOutlineArrowLeft, HiOutlineMail } from 'react-icons/hi';
|
|
20
|
+
import { HiOutlineArrowLeft, HiOutlineMail, HiOutlinePhone } from 'react-icons/hi';
|
|
21
21
|
import { useEffect, useRef, useState } from 'react';
|
|
22
22
|
import { Button, Loader } from '../../components';
|
|
23
23
|
// ============================================
|
|
@@ -79,7 +79,8 @@ export const OtpInput = ({ length = 6, value, onChange, disabled = false, inputC
|
|
|
79
79
|
/**
|
|
80
80
|
* Full-page OTP verification component with email display, countdown, and resend functionality
|
|
81
81
|
*/
|
|
82
|
-
export const OtpVerificationPage = ({ email, onVerifySuccess, onBack, handlers, logo, config = {}, text: customText = {}, classNames = {}, }) => {
|
|
82
|
+
export const OtpVerificationPage = ({ email, phoneNumber, onVerifySuccess, onBack, handlers, phoneHandlers, logo, config = {}, text: customText = {}, classNames = {}, }) => {
|
|
83
|
+
const isPhoneMode = Boolean(phoneNumber && phoneHandlers);
|
|
83
84
|
const { otpLength = 6, resendCountdown = 60 } = config;
|
|
84
85
|
const text = { ...otpVerificationDefaultText, ...customText };
|
|
85
86
|
const [otpValue, setOtpValue] = useState('');
|
|
@@ -105,7 +106,13 @@ export const OtpVerificationPage = ({ email, onVerifySuccess, onBack, handlers,
|
|
|
105
106
|
setIsVerifying(true);
|
|
106
107
|
clearError();
|
|
107
108
|
try {
|
|
108
|
-
|
|
109
|
+
let response;
|
|
110
|
+
if (isPhoneMode) {
|
|
111
|
+
response = await phoneHandlers.verifyOtp({ phoneNumber: phoneNumber, otpCode: otpValue });
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
response = await handlers.verifyOtp({ email, otpCode: otpValue });
|
|
115
|
+
}
|
|
109
116
|
onVerifySuccess(response);
|
|
110
117
|
}
|
|
111
118
|
catch (err) {
|
|
@@ -122,7 +129,12 @@ export const OtpVerificationPage = ({ email, onVerifySuccess, onBack, handlers,
|
|
|
122
129
|
setIsResending(true);
|
|
123
130
|
clearError();
|
|
124
131
|
try {
|
|
125
|
-
|
|
132
|
+
if (isPhoneMode) {
|
|
133
|
+
await phoneHandlers.resendOtp({ phoneNumber: phoneNumber });
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
await handlers.resendOtp({ email });
|
|
137
|
+
}
|
|
126
138
|
setCountdown(resendCountdown);
|
|
127
139
|
setCanResend(false);
|
|
128
140
|
setOtpValue('');
|
|
@@ -135,7 +147,7 @@ export const OtpVerificationPage = ({ email, onVerifySuccess, onBack, handlers,
|
|
|
135
147
|
setIsResending(false);
|
|
136
148
|
}
|
|
137
149
|
};
|
|
138
|
-
return (_jsx("div", { className: `flex min-h-screen items-center justify-center bg-muted px-4 py-12 ${classNames.container ?? ''}`, children: _jsxs("div", { className: "w-full max-w-md", children: [logo && (_jsxs("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${logo.darkSrc ? ' dark:hidden' : ''}` }), logo.darkSrc && (_jsx("img", { src: logo.darkSrc, alt: logo.alt, width: logo.width, height: logo.height, className: "h-auto max-h-16 hidden dark:block" }))] })), _jsxs("div", { className: `rounded-2xl border border-border bg-background 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/15 ${classNames.icon ?? ''}`, children: _jsx(HiOutlineMail, { className: "h-8 w-8 text-primary" }) }), _jsx("h1", { className: "text-2xl font-bold text-foreground", children: text.title }), _jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: text.subtitle }), _jsx("p", { className: "mt-1 font-medium text-foreground", 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-foreground", children: text.codeLabel }), _jsx(OtpInput, { length: otpLength, value: otpValue, onChange: (value) => {
|
|
150
|
+
return (_jsx("div", { className: `flex min-h-screen items-center justify-center bg-muted px-4 py-12 ${classNames.container ?? ''}`, children: _jsxs("div", { className: "w-full max-w-md", children: [logo && (_jsxs("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${logo.darkSrc ? ' dark:hidden' : ''}` }), logo.darkSrc && (_jsx("img", { src: logo.darkSrc, alt: logo.alt, width: logo.width, height: logo.height, className: "h-auto max-h-16 hidden dark:block" }))] })), _jsxs("div", { className: `rounded-2xl border border-border bg-background 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/15 ${classNames.icon ?? ''}`, children: isPhoneMode ? (_jsx(HiOutlinePhone, { className: "h-8 w-8 text-primary" })) : (_jsx(HiOutlineMail, { className: "h-8 w-8 text-primary" })) }), _jsx("h1", { className: "text-2xl font-bold text-foreground", children: text.title }), _jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: text.subtitle }), _jsx("p", { className: "mt-1 font-medium text-foreground", children: isPhoneMode ? phoneNumber : 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-foreground", children: text.codeLabel }), _jsx(OtpInput, { length: otpLength, value: otpValue, onChange: (value) => {
|
|
139
151
|
setOtpValue(value);
|
|
140
152
|
clearError();
|
|
141
153
|
}, 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, { type: "button", variant: "ghost", onClick: handleResend, disabled: isResending, className: "text-sm font-medium text-primary hover:text-primary/90 disabled:text-muted-foreground", children: isResending ? text.resendingButton : text.resendButton })) : (_jsxs("p", { className: "text-sm text-muted-foreground", children: [text.resendCountdownText, " ", _jsxs("span", { className: "font-medium text-foreground", children: [countdown, "s"] })] })) }), _jsx("div", { className: "mt-6 border-t border-border pt-6", children: _jsxs(Button, { type: "button", variant: "ghost", onClick: onBack, className: "flex w-full items-center justify-center gap-2 text-sm text-foreground/70 hover:text-foreground", children: [_jsx(HiOutlineArrowLeft, { className: "h-4 w-4" }), text.backButton] }) })] })] }) }));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { default as LogoutButton } from './LogoutButton';
|
|
2
|
-
export { default as PagamioLoginPage, loginPageDefaultText, type LoginFieldType, type PagamioLoginCredentials, type PagamioLoginPageProps, } from './LoginPage';
|
|
2
|
+
export { default as PagamioLoginPage, loginPageDefaultText, type LoginFieldType, type PagamioLoginCredentials, type PagamioLoginPageProps, type PhoneOtpLoginConfig, } from './LoginPage';
|
|
3
3
|
export { default as ChangePasswordPage, type ChangePasswordPageProps } from './ChangePasswordPage';
|
|
4
4
|
export { type PostDataProps } from './hooks/useChangeUserPassword';
|
|
5
5
|
export { default as PagamioCustomerRegistrationPage, customerRegistrationPageDefaultText, type PagamioCustomerRegistrationPageProps, } from './CustomerRegistrationPage';
|
|
@@ -7,5 +7,5 @@ export { default as PagamioForgotPasswordPage, forgotPasswordDefaultText, type P
|
|
|
7
7
|
export { default as PagamioResetPasswordPage, resetPasswordDefaultText, type PagamioResetPasswordPageProps, } from './ResetPasswordPage';
|
|
8
8
|
export { AuthPageLayout as PagamioAuthPageLayout } from './AuthPageLayout';
|
|
9
9
|
export { FeatureCarousel, type FeatureCarouselProps, type FeatureItem } from './FeatureCarousel';
|
|
10
|
-
export { default as OtpVerificationPage, OtpInput, otpVerificationDefaultText, type OtpInputProps, type OtpVerificationConfig, type OtpVerificationHandlers, type OtpVerificationPageProps, type OtpVerificationText, } from './OtpVerification';
|
|
10
|
+
export { default as OtpVerificationPage, OtpInput, otpVerificationDefaultText, type OtpInputProps, type OtpVerificationConfig, type OtpVerificationHandlers, type PhoneOtpVerificationHandlers, type OtpVerificationPageProps, type OtpVerificationText, } from './OtpVerification';
|
|
11
11
|
export { passwordValidation } from './AuthFormUtils';
|
|
@@ -8,6 +8,10 @@ import { cn } from '../helpers';
|
|
|
8
8
|
import FieldWrapper from './components/FieldWrapper';
|
|
9
9
|
import { useFormPersistence } from './hooks/useFormPersistence';
|
|
10
10
|
import { isRegistryReady, setupInputRegistry } from './registry';
|
|
11
|
+
// Eagerly start registry setup when this module is first imported so it is
|
|
12
|
+
// ready (or nearly ready) by the time any FormEngine component mounts.
|
|
13
|
+
// This is safe to call multiple times — the singleton promise is shared.
|
|
14
|
+
setupInputRegistry();
|
|
11
15
|
const CancelButton = ({ isSubmitting, onCancel, isNotTrigger, onClearData }) => {
|
|
12
16
|
const handleCancel = () => {
|
|
13
17
|
if (onClearData) {
|
|
@@ -19,12 +23,19 @@ const CancelButton = ({ isSubmitting, onCancel, isNotTrigger, onClearData }) =>
|
|
|
19
23
|
return isNotTrigger ? (_jsx(Button, { className: sharedClasses, disabled: isSubmitting, onClick: handleCancel, children: "Cancel" })) : (_jsx(SheetRoot, { children: _jsx(SheetTrigger, { asChild: true, children: _jsx(Button, { className: sharedClasses, disabled: isSubmitting, onClick: handleCancel, children: "Cancel" }) }) }));
|
|
20
24
|
};
|
|
21
25
|
const FormEngine = ({ fields, onSubmit, initialValues, layout = 'vertical', isNotTrigger, showCancelButton = true, submitButtonText = 'Save', showSubmittingText = true, submitButtonClass, onCancel, getFieldValues, formRef, className, persistenceKey, }) => {
|
|
22
|
-
|
|
26
|
+
// Always start as false so server and client agree on the initial render
|
|
27
|
+
// (avoids SSR hydration mismatch caused by Node.js module cache keeping
|
|
28
|
+
// registryDone=true across requests while the browser always starts fresh).
|
|
29
|
+
const [registryLoaded, setRegistryLoaded] = useState(false);
|
|
23
30
|
useEffect(() => {
|
|
24
|
-
if (
|
|
31
|
+
if (isRegistryReady()) {
|
|
32
|
+
// Already resolved by the module-level eager call above.
|
|
33
|
+
setRegistryLoaded(true);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
25
36
|
setupInputRegistry().then(() => setRegistryLoaded(true));
|
|
26
37
|
}
|
|
27
|
-
}, [
|
|
38
|
+
}, []);
|
|
28
39
|
const { saveFormData, restoreFormData, clearPersistedData, hasPersistedData } = useFormPersistence(persistenceKey);
|
|
29
40
|
// Determine initial values: persisted data takes precedence over provided initialValues
|
|
30
41
|
const getEffectiveInitialValues = () => {
|
|
@@ -45,11 +45,18 @@ const FieldWrapper = ({ field, control, errors, layout, onFieldUpdate, onFieldCh
|
|
|
45
45
|
}
|
|
46
46
|
};
|
|
47
47
|
if (field.name === 'email') {
|
|
48
|
+
const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
|
49
|
+
const existingValidate = field.validation?.validate;
|
|
48
50
|
field.validation = {
|
|
49
51
|
...field.validation,
|
|
50
|
-
pattern
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
// Use validate instead of pattern so empty values are allowed (field is not required unless
|
|
53
|
+
// the required rule is explicitly set). pattern always runs even on empty strings.
|
|
54
|
+
validate: (value) => {
|
|
55
|
+
if (!value)
|
|
56
|
+
return true;
|
|
57
|
+
if (!emailPattern.test(value))
|
|
58
|
+
return 'Please enter a valid email address';
|
|
59
|
+
return existingValidate ? existingValidate(value) : true;
|
|
53
60
|
},
|
|
54
61
|
};
|
|
55
62
|
}
|
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.280",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
7
7
|
"provenance": false
|