@pagamio/frontend-commons-lib 0.8.279 → 0.8.281
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/LoginPage.d.ts +25 -1
- package/lib/auth/components/LoginPage.js +55 -5
- package/lib/auth/components/OtpVerification.js +1 -3
- package/lib/auth/components/PhoneForgotPinPage.d.ts +54 -0
- package/lib/auth/components/PhoneForgotPinPage.js +131 -0
- package/lib/auth/components/PhonePinSetupPage.d.ts +35 -0
- package/lib/auth/components/PhonePinSetupPage.js +44 -0
- package/lib/auth/components/index.d.ts +3 -1
- package/lib/auth/components/index.js +2 -0
- package/lib/form-engine/components/FieldWrapper.js +10 -3
- package/package.json +1 -1
|
@@ -45,6 +45,25 @@ export interface PhoneOtpLoginConfig {
|
|
|
45
45
|
/** Optional label for the password tab (default: 'Email & Password') */
|
|
46
46
|
passwordTabLabel?: string;
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Configuration for the phone PIN login tab.
|
|
50
|
+
* When provided, a "Phone + PIN" tab is displayed alongside the password tab.
|
|
51
|
+
*/
|
|
52
|
+
export interface PhonePinLoginConfig {
|
|
53
|
+
/** Login with phone number and PIN. Should throw on error. */
|
|
54
|
+
loginWithPin: (phoneNumber: string, pin: string) => Promise<any>;
|
|
55
|
+
/** Optional callback when forgot PIN is clicked */
|
|
56
|
+
onForgotPin?: () => void;
|
|
57
|
+
/**
|
|
58
|
+
* Optional callback called instead of onLoginSuccess when mustChangePIN is true.
|
|
59
|
+
* Use this to redirect to the forgot-pin flow.
|
|
60
|
+
*/
|
|
61
|
+
onMustChangePIN?: (phoneNumber: string, response: any) => void;
|
|
62
|
+
/** Optional label for the phone-PIN tab (default: 'Phone + PIN') */
|
|
63
|
+
tabLabel?: string;
|
|
64
|
+
/** Optional label for the password tab (default: 'Email & Password') */
|
|
65
|
+
passwordTabLabel?: string;
|
|
66
|
+
}
|
|
48
67
|
/**
|
|
49
68
|
* Base login credentials interface that can be extended for specific implementations
|
|
50
69
|
*/
|
|
@@ -121,6 +140,11 @@ interface PagamioLoginPageProps<T extends CustomAuthConfig> extends BaseAuthPage
|
|
|
121
140
|
* The tab handles the full 2-step phone OTP flow internally.
|
|
122
141
|
*/
|
|
123
142
|
phoneOtpConfig?: PhoneOtpLoginConfig;
|
|
143
|
+
/**
|
|
144
|
+
* When provided, adds a "Phone + PIN" tab alongside the standard login form.
|
|
145
|
+
* The tab handles the 2-step phone PIN flow internally.
|
|
146
|
+
*/
|
|
147
|
+
phonePinConfig?: PhonePinLoginConfig;
|
|
124
148
|
}
|
|
125
149
|
export interface LoginErrorProps {
|
|
126
150
|
code: string;
|
|
@@ -142,6 +166,6 @@ export declare const loginPageDefaultText: {
|
|
|
142
166
|
* Generic Login Page component
|
|
143
167
|
* @template T - Authentication configuration type
|
|
144
168
|
*/
|
|
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;
|
|
169
|
+
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, phonePinConfig, }: Readonly<PagamioLoginPageProps<T>>): import("react/jsx-runtime").JSX.Element;
|
|
146
170
|
export default PagamioLoginPage;
|
|
147
171
|
export type { PagamioLoginCredentials, PagamioLoginPageProps };
|
|
@@ -47,7 +47,7 @@ export const loginPageDefaultText = {
|
|
|
47
47
|
* Generic Login Page component
|
|
48
48
|
* @template T - Authentication configuration type
|
|
49
49
|
*/
|
|
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
|
+
export function PagamioLoginPage({ logo, text = loginPageDefaultText, appLabel, onForgotPassword, onLoginSuccess, onLoginError, hasCreateAccount = false, createAccountRoute, onCreateAccount, transformLoginData, authenticatorType, loginFieldType = 'username', customLoginFields, className = '', features, sideContentClass, footer, phoneOtpConfig, phonePinConfig, }) {
|
|
51
51
|
const { login, error: authError } = useAuth();
|
|
52
52
|
const { addToast } = useToast();
|
|
53
53
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -63,6 +63,8 @@ export function PagamioLoginPage({ logo, text = loginPageDefaultText, appLabel,
|
|
|
63
63
|
const [phoneError, setPhoneError] = useState(null);
|
|
64
64
|
const [phoneCountdown, setPhoneCountdown] = useState(0);
|
|
65
65
|
const [canResendPhone, setCanResendPhone] = useState(true);
|
|
66
|
+
const [phonePinValue, setPhonePinValue] = useState('');
|
|
67
|
+
const [isLoggingInWithPin, setIsLoggingInWithPin] = useState(false);
|
|
66
68
|
const startCountdown = (seconds) => {
|
|
67
69
|
setPhoneCountdown(seconds);
|
|
68
70
|
setCanResendPhone(false);
|
|
@@ -91,6 +93,11 @@ export function PagamioLoginPage({ logo, text = loginPageDefaultText, appLabel,
|
|
|
91
93
|
];
|
|
92
94
|
const handlePhoneFormSubmit = async (data) => {
|
|
93
95
|
const phone = data.phoneNumber;
|
|
96
|
+
if (phonePinConfig) {
|
|
97
|
+
setPhoneNumber(phone);
|
|
98
|
+
setPhoneStep('enterPin');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
94
101
|
setIsSendingOtp(true);
|
|
95
102
|
setPhoneError(null);
|
|
96
103
|
try {
|
|
@@ -107,6 +114,28 @@ export function PagamioLoginPage({ logo, text = loginPageDefaultText, appLabel,
|
|
|
107
114
|
setIsSendingOtp(false);
|
|
108
115
|
}
|
|
109
116
|
};
|
|
117
|
+
const handleLoginWithPin = async () => {
|
|
118
|
+
if (phonePinValue.length !== 4)
|
|
119
|
+
return;
|
|
120
|
+
setIsLoggingInWithPin(true);
|
|
121
|
+
setPhoneError(null);
|
|
122
|
+
try {
|
|
123
|
+
const response = await phonePinConfig.loginWithPin(phoneNumber, phonePinValue);
|
|
124
|
+
const responseData = response?.data ?? response;
|
|
125
|
+
if (responseData?.mustChangePIN && phonePinConfig.onMustChangePIN) {
|
|
126
|
+
phonePinConfig.onMustChangePIN(phoneNumber, response);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
onLoginSuccess?.(response);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
setPhoneError(err?.response?.data?.message ?? err?.message ?? 'Login failed. Please try again.');
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
setIsLoggingInWithPin(false);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
110
139
|
const handleResendPhoneOtp = async () => {
|
|
111
140
|
if (!canResendPhone || isResendingOtp)
|
|
112
141
|
return;
|
|
@@ -224,12 +253,33 @@ export function PagamioLoginPage({ logo, text = loginPageDefaultText, appLabel,
|
|
|
224
253
|
window.location.href = createAccountRoute;
|
|
225
254
|
}
|
|
226
255
|
};
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
|
|
256
|
+
const showPhoneTab = phoneOtpConfig ?? phonePinConfig;
|
|
257
|
+
const passwordTabLabel = (phoneOtpConfig?.passwordTabLabel ?? phonePinConfig?.passwordTabLabel) ?? 'Email & Password';
|
|
258
|
+
const phoneTabLabel = phoneOtpConfig?.tabLabel ?? phonePinConfig?.tabLabel ?? (phonePinConfig ? 'Phone + PIN' : 'Phone + OTP');
|
|
259
|
+
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: [showPhoneTab && (_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: () => {
|
|
260
|
+
setActiveTab('phone');
|
|
261
|
+
setPhoneStep('enterPhone');
|
|
262
|
+
setPhoneOtpValue('');
|
|
263
|
+
setPhonePinValue('');
|
|
264
|
+
setPhoneError(null);
|
|
265
|
+
}, 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
266
|
...(loginFieldType === 'email' ? { email: '' } : { username: '' }),
|
|
231
267
|
password: '',
|
|
232
268
|
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' &&
|
|
269
|
+
} }), _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' && showPhoneTab && (_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' && phoneOtpConfig && (_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) => {
|
|
270
|
+
setPhoneOtpValue(v);
|
|
271
|
+
setPhoneError(null);
|
|
272
|
+
}, 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: () => {
|
|
273
|
+
setPhoneStep('enterPhone');
|
|
274
|
+
setPhoneOtpValue('');
|
|
275
|
+
setPhoneError(null);
|
|
276
|
+
}, className: "w-full text-sm text-foreground/70", children: "\u2190 Change number" })] })), phoneStep === 'enterPin' && phonePinConfig && (_jsxs(_Fragment, { children: [_jsxs("p", { className: "text-sm text-muted-foreground text-center", children: ["Enter your 4-digit PIN for", ' ', _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: "PIN" }), _jsx(OtpInput, { length: 4, value: phonePinValue, onChange: (v) => {
|
|
277
|
+
setPhonePinValue(v);
|
|
278
|
+
setPhoneError(null);
|
|
279
|
+
}, disabled: isLoggingInWithPin })] }), _jsx(Button, { type: "button", onClick: handleLoginWithPin, disabled: phonePinValue.length !== 4 || isLoggingInWithPin, className: "w-full", size: "lg", children: isLoggingInWithPin ? 'Signing in...' : 'Sign In' }), phonePinConfig.onForgotPin && (_jsx("div", { className: "text-center", children: _jsx(Button, { type: "button", variant: "link", onClick: phonePinConfig.onForgotPin, className: "text-sm text-primary hover:underline", children: "Forgot PIN?" }) })), _jsx(Button, { type: "button", variant: "ghost", onClick: () => {
|
|
280
|
+
setPhoneStep('enterPhone');
|
|
281
|
+
setPhonePinValue('');
|
|
282
|
+
setPhoneError(null);
|
|
283
|
+
}, className: "w-full text-sm text-foreground/70", children: "\u2190 Change number" })] }))] }))] }) }));
|
|
234
284
|
}
|
|
235
285
|
export default PagamioLoginPage;
|
|
@@ -147,9 +147,7 @@ export const OtpVerificationPage = ({ email, phoneNumber, onVerifySuccess, onBac
|
|
|
147
147
|
setIsResending(false);
|
|
148
148
|
}
|
|
149
149
|
};
|
|
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) => {
|
|
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) => {
|
|
153
151
|
setOtpValue(value);
|
|
154
152
|
clearError();
|
|
155
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] }) })] })] }) }));
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phone Forgot PIN Page component.
|
|
3
|
+
* Guides the user through a 3-step flow to reset their phone PIN:
|
|
4
|
+
* 1. Enter phone number → triggers OTP send
|
|
5
|
+
* 2. Enter 4-digit OTP → returns a verificationToken
|
|
6
|
+
* 3. Set a new 4-digit PIN (with confirmation)
|
|
7
|
+
*/
|
|
8
|
+
import React from 'react';
|
|
9
|
+
export interface PhoneForgotPinPageProps {
|
|
10
|
+
/**
|
|
11
|
+
* Called with the phone number to trigger an OTP send.
|
|
12
|
+
* Should call POST /auth/forgot-phone-pin. Should throw on error.
|
|
13
|
+
*/
|
|
14
|
+
onRequestOtp: (phoneNumber: string) => Promise<any>;
|
|
15
|
+
/**
|
|
16
|
+
* Called with phone + OTP to verify and obtain a verificationToken.
|
|
17
|
+
* Should call POST /auth/verify-phone-otp. Should throw on error.
|
|
18
|
+
* Must resolve to an object containing `verificationToken`.
|
|
19
|
+
*/
|
|
20
|
+
onVerifyOtp: (phoneNumber: string, otpCode: string) => Promise<{
|
|
21
|
+
verificationToken: string;
|
|
22
|
+
}>;
|
|
23
|
+
/**
|
|
24
|
+
* Called with phone, verificationToken, and new PIN to complete the reset.
|
|
25
|
+
* Should call POST /auth/set-phone-pin. Should throw on error.
|
|
26
|
+
*/
|
|
27
|
+
onSetPin: (phoneNumber: string, verificationToken: string, pin: string) => Promise<any>;
|
|
28
|
+
/** Called on successful PIN reset (after onSetPin resolves). */
|
|
29
|
+
onSuccess?: (response: any) => void;
|
|
30
|
+
/** Called when the user clicks "Back to login". */
|
|
31
|
+
onBackToLogin?: () => void;
|
|
32
|
+
/** Optional initial phone number to pre-fill (used when redirected from mustChangePIN flow) */
|
|
33
|
+
initialPhoneNumber?: string;
|
|
34
|
+
/** Optional logo configuration */
|
|
35
|
+
logo?: {
|
|
36
|
+
src: string;
|
|
37
|
+
darkSrc?: string;
|
|
38
|
+
alt: string;
|
|
39
|
+
width: number;
|
|
40
|
+
height: number;
|
|
41
|
+
};
|
|
42
|
+
/** Optional feature list to display in side carousel */
|
|
43
|
+
features?: {
|
|
44
|
+
title: string;
|
|
45
|
+
description: string;
|
|
46
|
+
icon?: React.ReactNode;
|
|
47
|
+
}[];
|
|
48
|
+
/** Optional footer content */
|
|
49
|
+
footer?: React.ReactNode;
|
|
50
|
+
/** Optional app label (e.g. "COMMERCE") */
|
|
51
|
+
appLabel?: string;
|
|
52
|
+
}
|
|
53
|
+
declare const PhoneForgotPinPage: React.FC<PhoneForgotPinPageProps>;
|
|
54
|
+
export default PhoneForgotPinPage;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Phone Forgot PIN Page component.
|
|
4
|
+
* Guides the user through a 3-step flow to reset their phone PIN:
|
|
5
|
+
* 1. Enter phone number → triggers OTP send
|
|
6
|
+
* 2. Enter 4-digit OTP → returns a verificationToken
|
|
7
|
+
* 3. Set a new 4-digit PIN (with confirmation)
|
|
8
|
+
*/
|
|
9
|
+
import React, { useState } from 'react';
|
|
10
|
+
import { Button } from '../../components';
|
|
11
|
+
import { FormEngine } from '../../form-engine';
|
|
12
|
+
import { AuthPageLayout } from './AuthPageLayout';
|
|
13
|
+
import { FeatureCarousel } from './FeatureCarousel';
|
|
14
|
+
import { OtpInput } from './OtpVerification';
|
|
15
|
+
const PhoneForgotPinPage = ({ onRequestOtp, onVerifyOtp, onSetPin, onSuccess, onBackToLogin, initialPhoneNumber = '', logo, features, footer, appLabel, }) => {
|
|
16
|
+
const [step, setStep] = useState('enterPhone');
|
|
17
|
+
const [phoneNumber, setPhoneNumber] = useState('');
|
|
18
|
+
const [otpValue, setOtpValue] = useState('');
|
|
19
|
+
const [verificationToken, setVerificationToken] = useState('');
|
|
20
|
+
const [pin, setPin] = useState('');
|
|
21
|
+
const [confirmPin, setConfirmPin] = useState('');
|
|
22
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
23
|
+
const [error, setError] = useState(null);
|
|
24
|
+
// Step 1 — phone number field config
|
|
25
|
+
const phoneFields = [
|
|
26
|
+
{
|
|
27
|
+
name: 'phoneNumber',
|
|
28
|
+
label: 'Phone Number',
|
|
29
|
+
type: 'tel',
|
|
30
|
+
placeholder: '+27 82 123 4567',
|
|
31
|
+
gridSpan: 12,
|
|
32
|
+
defaultCountry: 'ZA',
|
|
33
|
+
validation: { required: 'Phone number is required' },
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
const formRef = React.useRef(null);
|
|
37
|
+
// Auto-submit if initialPhoneNumber is provided (mustChangePIN redirect)
|
|
38
|
+
React.useEffect(() => {
|
|
39
|
+
if (initialPhoneNumber) {
|
|
40
|
+
handlePhoneSubmit({ phoneNumber: initialPhoneNumber }).catch(() => { });
|
|
41
|
+
}
|
|
42
|
+
}, []);
|
|
43
|
+
const handlePhoneSubmit = async (data) => {
|
|
44
|
+
const phone = data.phoneNumber;
|
|
45
|
+
setIsLoading(true);
|
|
46
|
+
setError(null);
|
|
47
|
+
try {
|
|
48
|
+
await onRequestOtp(phone);
|
|
49
|
+
setPhoneNumber(phone);
|
|
50
|
+
setStep('enterOtp');
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
setError(err?.response?.data?.message ?? err?.message ?? 'Failed to send OTP. Please try again.');
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
setIsLoading(false);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const handleVerifyOtp = async () => {
|
|
61
|
+
if (otpValue.length !== 4)
|
|
62
|
+
return;
|
|
63
|
+
setIsLoading(true);
|
|
64
|
+
setError(null);
|
|
65
|
+
try {
|
|
66
|
+
const result = await onVerifyOtp(phoneNumber, otpValue);
|
|
67
|
+
const token = result?.data?.verificationToken ?? result?.verificationToken;
|
|
68
|
+
if (!token) {
|
|
69
|
+
throw new Error('Verification failed. Please try again.');
|
|
70
|
+
}
|
|
71
|
+
setVerificationToken(token);
|
|
72
|
+
setStep('enterPin');
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
setError(err?.response?.data?.message ?? err?.message ?? 'Invalid code. Please try again.');
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
setIsLoading(false);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const handleSetPin = async () => {
|
|
82
|
+
if (pin.length !== 4 || confirmPin.length !== 4)
|
|
83
|
+
return;
|
|
84
|
+
if (pin !== confirmPin) {
|
|
85
|
+
setError('PINs do not match. Please try again.');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
setIsLoading(true);
|
|
89
|
+
setError(null);
|
|
90
|
+
try {
|
|
91
|
+
const response = await onSetPin(phoneNumber, verificationToken, pin);
|
|
92
|
+
onSuccess?.(response);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
setError(err?.response?.data?.message ?? err?.message ?? 'Failed to set PIN. Please try again.');
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
setIsLoading(false);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const stepTitles = {
|
|
102
|
+
enterPhone: {
|
|
103
|
+
title: 'Reset Your PIN',
|
|
104
|
+
subtitle: "Enter your phone number and we'll send you a verification code.",
|
|
105
|
+
},
|
|
106
|
+
enterOtp: {
|
|
107
|
+
title: 'Enter Verification Code',
|
|
108
|
+
subtitle: `We sent a 4-digit code to ${phoneNumber}`,
|
|
109
|
+
},
|
|
110
|
+
enterPin: {
|
|
111
|
+
title: 'Set New PIN',
|
|
112
|
+
subtitle: `Create a new 4-digit PIN for ${phoneNumber}`,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
const { title, subtitle } = stepTitles[step];
|
|
116
|
+
return (_jsx(AuthPageLayout, { appLabel: appLabel, title: title, subtitle: subtitle, errorMessage: error, logo: logo, horizontal: false, sideContent: features && features.length > 0 ? _jsx(FeatureCarousel, { features: features }) : undefined, footer: footer, children: _jsxs("div", { className: "mt-8 space-y-4", children: [step === 'enterPhone' && (_jsxs(_Fragment, { children: [_jsx(FormEngine, { fields: phoneFields, onSubmit: handlePhoneSubmit, layout: "vertical", className: "mb-0 px-0", submitButtonClass: "w-full", submitButtonText: isLoading ? 'Sending code...' : 'Send verification code', onCancel: () => { }, showCancelButton: false, showSubmittingText: false, formRef: formRef, initialValues: { phoneNumber: initialPhoneNumber } }), onBackToLogin && (_jsx("div", { className: "text-center", children: _jsx(Button, { type: "button", variant: "link", onClick: onBackToLogin, className: "text-sm text-primary hover:underline", children: "Back to login" }) }))] })), step === 'enterOtp' && (_jsxs(_Fragment, { children: [_jsxs("div", { children: [_jsx("p", { className: "mb-2 block text-sm font-medium text-foreground text-center", children: "Verification Code" }), _jsx(OtpInput, { length: 4, value: otpValue, onChange: (v) => {
|
|
117
|
+
setOtpValue(v);
|
|
118
|
+
setError(null);
|
|
119
|
+
}, disabled: isLoading })] }), _jsx(Button, { type: "button", onClick: handleVerifyOtp, disabled: otpValue.length !== 4 || isLoading, className: "w-full", size: "lg", children: isLoading ? 'Verifying...' : 'Verify Code' }), _jsx(Button, { type: "button", variant: "ghost", onClick: () => {
|
|
120
|
+
setStep('enterPhone');
|
|
121
|
+
setOtpValue('');
|
|
122
|
+
setError(null);
|
|
123
|
+
}, className: "w-full text-sm text-foreground/70", children: "\u2190 Change number" })] })), step === 'enterPin' && (_jsxs(_Fragment, { children: [_jsxs("div", { children: [_jsx("p", { className: "mb-2 block text-sm font-medium text-foreground text-center", children: "New PIN" }), _jsx(OtpInput, { length: 4, value: pin, onChange: (v) => {
|
|
124
|
+
setPin(v);
|
|
125
|
+
setError(null);
|
|
126
|
+
}, disabled: isLoading })] }), _jsxs("div", { children: [_jsx("p", { className: "mb-2 block text-sm font-medium text-foreground text-center", children: "Confirm PIN" }), _jsx(OtpInput, { length: 4, value: confirmPin, onChange: (v) => {
|
|
127
|
+
setConfirmPin(v);
|
|
128
|
+
setError(null);
|
|
129
|
+
}, disabled: isLoading })] }), _jsx(Button, { type: "button", onClick: handleSetPin, disabled: pin.length !== 4 || confirmPin.length !== 4 || isLoading, className: "w-full", size: "lg", children: isLoading ? 'Setting PIN...' : 'Set New PIN' })] }))] }) }));
|
|
130
|
+
};
|
|
131
|
+
export default PhoneForgotPinPage;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phone PIN Setup Page component.
|
|
3
|
+
* Shown after phone OTP verification to let the user set their 4-digit PIN.
|
|
4
|
+
* Used for both initial account setup and PIN reset (via the forgot-PIN flow).
|
|
5
|
+
*/
|
|
6
|
+
import React from 'react';
|
|
7
|
+
export interface PhonePinSetupPageProps {
|
|
8
|
+
/** The phone number the PIN is being set for (displayed to the user) */
|
|
9
|
+
phoneNumber: string;
|
|
10
|
+
/**
|
|
11
|
+
* Called when the user submits a valid PIN.
|
|
12
|
+
* Receives the 4-digit PIN as a plain string. Should throw on error.
|
|
13
|
+
*/
|
|
14
|
+
onPinSet: (pin: string) => Promise<any>;
|
|
15
|
+
/** Optional logo configuration */
|
|
16
|
+
logo?: {
|
|
17
|
+
src: string;
|
|
18
|
+
darkSrc?: string;
|
|
19
|
+
alt: string;
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
};
|
|
23
|
+
/** Optional feature list to display in side carousel */
|
|
24
|
+
features?: {
|
|
25
|
+
title: string;
|
|
26
|
+
description: string;
|
|
27
|
+
icon?: React.ReactNode;
|
|
28
|
+
}[];
|
|
29
|
+
/** Optional footer content */
|
|
30
|
+
footer?: React.ReactNode;
|
|
31
|
+
/** Optional app label (e.g. "COMMERCE") */
|
|
32
|
+
appLabel?: string;
|
|
33
|
+
}
|
|
34
|
+
declare const PhonePinSetupPage: React.FC<PhonePinSetupPageProps>;
|
|
35
|
+
export default PhonePinSetupPage;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Phone PIN Setup Page component.
|
|
4
|
+
* Shown after phone OTP verification to let the user set their 4-digit PIN.
|
|
5
|
+
* Used for both initial account setup and PIN reset (via the forgot-PIN flow).
|
|
6
|
+
*/
|
|
7
|
+
import { useState } from 'react';
|
|
8
|
+
import { Button } from '../../components';
|
|
9
|
+
import { AuthPageLayout } from './AuthPageLayout';
|
|
10
|
+
import { FeatureCarousel } from './FeatureCarousel';
|
|
11
|
+
import { OtpInput } from './OtpVerification';
|
|
12
|
+
const PhonePinSetupPage = ({ phoneNumber, onPinSet, logo, features, footer, appLabel, }) => {
|
|
13
|
+
const [pin, setPin] = useState('');
|
|
14
|
+
const [confirmPin, setConfirmPin] = useState('');
|
|
15
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
16
|
+
const [error, setError] = useState(null);
|
|
17
|
+
const handleSubmit = async () => {
|
|
18
|
+
if (pin.length !== 4 || confirmPin.length !== 4)
|
|
19
|
+
return;
|
|
20
|
+
if (pin !== confirmPin) {
|
|
21
|
+
setError('PINs do not match. Please try again.');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
setIsSubmitting(true);
|
|
25
|
+
setError(null);
|
|
26
|
+
try {
|
|
27
|
+
await onPinSet(pin);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
setError(err?.response?.data?.message ?? err?.message ?? 'Failed to set PIN. Please try again.');
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
setIsSubmitting(false);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
return (_jsx(AuthPageLayout, { appLabel: appLabel, title: "Set Your PIN", subtitle: `Create a 4-digit PIN for ${phoneNumber}`, errorMessage: error, logo: logo, horizontal: false, sideContent: features && features.length > 0 ? _jsx(FeatureCarousel, { features: features }) : undefined, footer: footer, children: _jsxs("div", { className: "mt-8 space-y-6", children: [_jsxs("div", { children: [_jsx("p", { className: "mb-2 block text-sm font-medium text-foreground text-center", children: "Create PIN" }), _jsx(OtpInput, { length: 4, value: pin, onChange: (v) => {
|
|
37
|
+
setPin(v);
|
|
38
|
+
setError(null);
|
|
39
|
+
}, disabled: isSubmitting })] }), _jsxs("div", { children: [_jsx("p", { className: "mb-2 block text-sm font-medium text-foreground text-center", children: "Confirm PIN" }), _jsx(OtpInput, { length: 4, value: confirmPin, onChange: (v) => {
|
|
40
|
+
setConfirmPin(v);
|
|
41
|
+
setError(null);
|
|
42
|
+
}, disabled: isSubmitting })] }), _jsx(Button, { type: "button", onClick: handleSubmit, disabled: pin.length !== 4 || confirmPin.length !== 4 || isSubmitting, className: "w-full", size: "lg", children: isSubmitting ? 'Setting PIN...' : 'Set PIN' })] }) }));
|
|
43
|
+
};
|
|
44
|
+
export default PhonePinSetupPage;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { default as LogoutButton } from './LogoutButton';
|
|
2
|
-
export { default as PagamioLoginPage, loginPageDefaultText, type LoginFieldType, type PagamioLoginCredentials, type PagamioLoginPageProps, type PhoneOtpLoginConfig, } from './LoginPage';
|
|
2
|
+
export { default as PagamioLoginPage, loginPageDefaultText, type LoginFieldType, type PagamioLoginCredentials, type PagamioLoginPageProps, type PhoneOtpLoginConfig, type PhonePinLoginConfig, } 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';
|
|
@@ -9,3 +9,5 @@ export { AuthPageLayout as PagamioAuthPageLayout } from './AuthPageLayout';
|
|
|
9
9
|
export { FeatureCarousel, type FeatureCarouselProps, type FeatureItem } from './FeatureCarousel';
|
|
10
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';
|
|
12
|
+
export { default as PhonePinSetupPage, type PhonePinSetupPageProps } from './PhonePinSetupPage';
|
|
13
|
+
export { default as PhoneForgotPinPage, type PhoneForgotPinPageProps } from './PhoneForgotPinPage';
|
|
@@ -8,3 +8,5 @@ export { AuthPageLayout as PagamioAuthPageLayout } from './AuthPageLayout';
|
|
|
8
8
|
export { FeatureCarousel } from './FeatureCarousel';
|
|
9
9
|
export { default as OtpVerificationPage, OtpInput, otpVerificationDefaultText, } from './OtpVerification';
|
|
10
10
|
export { passwordValidation } from './AuthFormUtils';
|
|
11
|
+
export { default as PhonePinSetupPage } from './PhonePinSetupPage';
|
|
12
|
+
export { default as PhoneForgotPinPage } from './PhoneForgotPinPage';
|
|
@@ -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.281",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
7
7
|
"provenance": false
|