keystone-design-bootstrap 1.0.81 → 1.0.82
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/package.json
CHANGED
|
@@ -7,6 +7,7 @@ import { getNationalMask, formatDigitsToMask } from '../../utils/phone-helpers';
|
|
|
7
7
|
import { firePixelEvent, setPixelUserData } from '../../tracking/firePixelEvent';
|
|
8
8
|
import { captureEvent } from '../../tracking/captureEvent';
|
|
9
9
|
type Step = 'identifier' | 'returning' | 'new';
|
|
10
|
+
type FunnelStep = 'identifier' | 'signin' | 'signup';
|
|
10
11
|
|
|
11
12
|
interface LoginFormProps {
|
|
12
13
|
onSuccess?: () => void;
|
|
@@ -26,6 +27,12 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
|
|
|
26
27
|
captureEvent('portal_login_started');
|
|
27
28
|
}, []);
|
|
28
29
|
|
|
30
|
+
const toFunnelStep = (value: Step): FunnelStep => (value === 'returning' ? 'signin' : value === 'new' ? 'signup' : 'identifier');
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
captureEvent('portal_login_step_viewed', { step: toFunnelStep(step) });
|
|
34
|
+
}, [step]);
|
|
35
|
+
|
|
29
36
|
// Identifier step state
|
|
30
37
|
const [phoneValue, setPhoneValue] = useState(''); // formatted national number
|
|
31
38
|
const [selectedCountry, setSelectedCountry] = useState('US');
|
|
@@ -102,7 +109,12 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
|
|
|
102
109
|
|
|
103
110
|
const handleContinue = async (e: React.FormEvent) => {
|
|
104
111
|
e.preventDefault();
|
|
105
|
-
if (!fullPhone) {
|
|
112
|
+
if (!fullPhone) {
|
|
113
|
+
captureEvent('portal_login_failed', { step: 'identifier', reason: 'validation_missing_phone' });
|
|
114
|
+
setError('Enter your phone number to continue.');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
captureEvent('portal_login_step_submitted', { step: 'identifier' });
|
|
106
118
|
setError(null);
|
|
107
119
|
setLoading(true);
|
|
108
120
|
try {
|
|
@@ -119,15 +131,30 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
|
|
|
119
131
|
captureEvent('portal_login_identified', { method, user_exists: true });
|
|
120
132
|
setWelcomeName(result.firstName ?? null);
|
|
121
133
|
if (result.hasPassword === false) {
|
|
134
|
+
captureEvent('portal_login_step_advanced', {
|
|
135
|
+
from_step: 'identifier',
|
|
136
|
+
to_step: 'signup',
|
|
137
|
+
reason: 'existing_user_without_password',
|
|
138
|
+
});
|
|
122
139
|
setIdentifiedWith('phone');
|
|
123
140
|
setStep('new');
|
|
124
141
|
} else {
|
|
142
|
+
captureEvent('portal_login_step_advanced', {
|
|
143
|
+
from_step: 'identifier',
|
|
144
|
+
to_step: 'signin',
|
|
145
|
+
reason: 'existing_user_with_password',
|
|
146
|
+
});
|
|
125
147
|
setStep('returning');
|
|
126
148
|
}
|
|
127
149
|
} else if (result.exists === false) {
|
|
128
150
|
await setPixelUserData({ email: emailVal, phone: fullPhone });
|
|
129
151
|
firePixelEvent('Lead');
|
|
130
152
|
captureEvent('portal_login_identified', { method, user_exists: false });
|
|
153
|
+
captureEvent('portal_login_step_advanced', {
|
|
154
|
+
from_step: 'identifier',
|
|
155
|
+
to_step: 'signup',
|
|
156
|
+
reason: 'new_user',
|
|
157
|
+
});
|
|
131
158
|
setIdentifiedWith('phone');
|
|
132
159
|
setStep('new');
|
|
133
160
|
} else {
|
|
@@ -144,6 +171,7 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
|
|
|
144
171
|
|
|
145
172
|
const handleSignIn = async (e: React.FormEvent) => {
|
|
146
173
|
e.preventDefault();
|
|
174
|
+
captureEvent('portal_login_step_submitted', { step: 'signin' });
|
|
147
175
|
setError(null);
|
|
148
176
|
setLoading(true);
|
|
149
177
|
try {
|
|
@@ -155,6 +183,11 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
|
|
|
155
183
|
const result = await res.json().catch(() => ({}));
|
|
156
184
|
if (!res.ok) {
|
|
157
185
|
if (result.code === 'not_found' || result.code === 'no_password') {
|
|
186
|
+
captureEvent('portal_login_step_advanced', {
|
|
187
|
+
from_step: 'signin',
|
|
188
|
+
to_step: 'signup',
|
|
189
|
+
reason: 'signin_requires_signup',
|
|
190
|
+
});
|
|
158
191
|
setStep('new');
|
|
159
192
|
setError(null);
|
|
160
193
|
return;
|
|
@@ -163,6 +196,11 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
|
|
|
163
196
|
setError(result.error || 'Login failed. Please try again.');
|
|
164
197
|
return;
|
|
165
198
|
}
|
|
199
|
+
captureEvent('portal_login_step_advanced', {
|
|
200
|
+
from_step: 'signin',
|
|
201
|
+
to_step: 'authenticated',
|
|
202
|
+
reason: 'signin_success',
|
|
203
|
+
});
|
|
166
204
|
captureEvent('portal_login_completed', { flow: 'signin' });
|
|
167
205
|
if (onSuccess) onSuccess(); else router.refresh();
|
|
168
206
|
} catch {
|
|
@@ -175,9 +213,22 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
|
|
|
175
213
|
|
|
176
214
|
const handleSignUp = async (e: React.FormEvent) => {
|
|
177
215
|
e.preventDefault();
|
|
178
|
-
if (!welcomeName && !firstName.trim()) {
|
|
179
|
-
|
|
180
|
-
|
|
216
|
+
if (!welcomeName && !firstName.trim()) {
|
|
217
|
+
captureEvent('portal_login_failed', { step: 'signup', reason: 'validation_missing_first_name' });
|
|
218
|
+
setError('Please enter your first name.');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (!welcomeName && !lastName.trim()) {
|
|
222
|
+
captureEvent('portal_login_failed', { step: 'signup', reason: 'validation_missing_last_name' });
|
|
223
|
+
setError('Please enter your last name.');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (password !== passwordConfirm) {
|
|
227
|
+
captureEvent('portal_login_failed', { step: 'signup', reason: 'validation_password_mismatch' });
|
|
228
|
+
setError('Passwords do not match.');
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
captureEvent('portal_login_step_submitted', { step: 'signup' });
|
|
181
232
|
setError(null);
|
|
182
233
|
setLoading(true);
|
|
183
234
|
try {
|
|
@@ -203,6 +254,11 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
|
|
|
203
254
|
setError(result.error || 'Signup failed. Please try again.');
|
|
204
255
|
return;
|
|
205
256
|
}
|
|
257
|
+
captureEvent('portal_login_step_advanced', {
|
|
258
|
+
from_step: 'signup',
|
|
259
|
+
to_step: 'authenticated',
|
|
260
|
+
reason: 'signup_success',
|
|
261
|
+
});
|
|
206
262
|
captureEvent('portal_login_completed', { flow: 'signup' });
|
|
207
263
|
if (onSuccess) onSuccess(); else router.refresh();
|
|
208
264
|
} catch {
|
|
@@ -214,6 +270,10 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
|
|
|
214
270
|
};
|
|
215
271
|
|
|
216
272
|
const goBack = () => {
|
|
273
|
+
if (step === 'returning' || step === 'new') {
|
|
274
|
+
const fromStep = step === 'returning' ? 'signin' : 'signup';
|
|
275
|
+
captureEvent('portal_login_back_clicked', { from_step: fromStep, to_step: 'identifier' });
|
|
276
|
+
}
|
|
217
277
|
setStep('identifier');
|
|
218
278
|
setPassword('');
|
|
219
279
|
setPasswordConfirm('');
|
|
@@ -45,6 +45,10 @@ export type KsEventName =
|
|
|
45
45
|
| 'portal_tab_viewed'
|
|
46
46
|
// Member portal — authentication flow
|
|
47
47
|
| 'portal_login_started'
|
|
48
|
+
| 'portal_login_step_viewed'
|
|
49
|
+
| 'portal_login_step_submitted'
|
|
50
|
+
| 'portal_login_step_advanced'
|
|
51
|
+
| 'portal_login_back_clicked'
|
|
48
52
|
| 'portal_login_identified'
|
|
49
53
|
| 'portal_login_completed'
|
|
50
54
|
| 'portal_login_failed';
|
|
@@ -92,6 +96,29 @@ export type KsEventProperties = {
|
|
|
92
96
|
/** Fired when the portal login modal is opened / login flow starts. */
|
|
93
97
|
portal_login_started: Record<string, never>;
|
|
94
98
|
|
|
99
|
+
/** Fired whenever a login step becomes visible to the user. */
|
|
100
|
+
portal_login_step_viewed: {
|
|
101
|
+
step: 'identifier' | 'signin' | 'signup';
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** Fired when the user submits a specific login step. */
|
|
105
|
+
portal_login_step_submitted: {
|
|
106
|
+
step: 'identifier' | 'signin' | 'signup';
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** Fired when the flow transitions from one step to another state. */
|
|
110
|
+
portal_login_step_advanced: {
|
|
111
|
+
from_step: 'identifier' | 'signin' | 'signup';
|
|
112
|
+
to_step: 'signin' | 'signup' | 'authenticated';
|
|
113
|
+
reason: string;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** Fired when the user explicitly navigates back to the identifier step. */
|
|
117
|
+
portal_login_back_clicked: {
|
|
118
|
+
from_step: 'signin' | 'signup';
|
|
119
|
+
to_step: 'identifier';
|
|
120
|
+
};
|
|
121
|
+
|
|
95
122
|
/**
|
|
96
123
|
* Fired after the identifier step resolves — we know whether the user
|
|
97
124
|
* already has an account.
|