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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keystone-design-bootstrap",
3
- "version": "1.0.81",
3
+ "version": "1.0.82",
4
4
  "description": "Keystone Design Bootstrap - Sections, Elements, and Theme System for customer websites",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -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) { setError('Enter your phone number to continue.'); return; }
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()) { setError('Please enter your first name.'); return; }
179
- if (!welcomeName && !lastName.trim()) { setError('Please enter your last name.'); return; }
180
- if (password !== passwordConfirm) { setError('Passwords do not match.'); return; }
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.