@pagamio/frontend-commons-lib 0.8.278 → 0.8.279

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.
@@ -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, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
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,12 @@ export function PagamioLoginPage({ logo, text = loginPageDefaultText, appLabel,
136
224
  window.location.href = createAccountRoute;
137
225
  }
138
226
  };
139
- return (_jsx(AuthPageLayout, { appLabel: appLabel, title: text.welcomeTitle, subtitle: text.welcomeSubtitle, errorMessage: error ?? authError?.message, 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: [_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: {
140
- ...(loginFieldType === 'email' ? { email: '' } : { username: '' }),
141
- password: '',
142
- rememberMe: false,
143
- } }), _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 })] }))] })] }) }));
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: () => { setActiveTab('phone'); setPhoneStep('enterPhone'); setPhoneError(null); }, 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: {
230
+ ...(loginFieldType === 'email' ? { email: '' } : { username: '' }),
231
+ password: '',
232
+ rememberMe: false,
233
+ } }), _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) => { setPhoneOtpValue(v); setPhoneError(null); }, 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: () => { setPhoneStep('enterPhone'); setPhoneOtpValue(''); setPhoneError(null); }, className: "w-full text-sm text-foreground/70", children: "\u2190 Change number" })] }))] }))] }) }));
144
234
  }
145
235
  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
- const response = await handlers.verifyOtp({ email, otpCode: otpValue });
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
- await handlers.resendOtp({ email });
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,9 @@ 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
151
+ ? _jsx(HiOutlinePhone, { className: "h-8 w-8 text-primary" })
152
+ : _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
153
  setOtpValue(value);
140
154
  clearError();
141
155
  }, 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
- const [registryLoaded, setRegistryLoaded] = useState(isRegistryReady);
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 (!registryLoaded) {
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
- }, [registryLoaded]);
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 = () => {
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.278",
4
+ "version": "0.8.279",
5
5
  "publishConfig": {
6
6
  "access": "public",
7
7
  "provenance": false