@pagamio/frontend-commons-lib 0.8.280 → 0.8.282

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.
@@ -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,23 +253,33 @@ export function PagamioLoginPage({ logo, text = loginPageDefaultText, appLabel,
224
253
  window.location.href = createAccountRoute;
225
254
  }
226
255
  };
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: () => {
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: () => {
230
260
  setActiveTab('phone');
231
261
  setPhoneStep('enterPhone');
262
+ setPhoneOtpValue('');
263
+ setPhonePinValue('');
232
264
  setPhoneError(null);
233
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: {
234
266
  ...(loginFieldType === 'email' ? { email: '' } : { username: '' }),
235
267
  password: '',
236
268
  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) => {
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: phonePinConfig ? 'Continue' : 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) => {
238
270
  setPhoneOtpValue(v);
239
271
  setPhoneError(null);
240
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: () => {
241
273
  setPhoneStep('enterPhone');
242
274
  setPhoneOtpValue('');
243
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);
244
283
  }, className: "w-full text-sm text-foreground/70", children: "\u2190 Change number" })] }))] }))] }) }));
245
284
  }
246
285
  export default PagamioLoginPage;
@@ -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';
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.280",
4
+ "version": "0.8.282",
5
5
  "publishConfig": {
6
6
  "access": "public",
7
7
  "provenance": false