keystone-design-bootstrap 1.0.72 → 1.0.74

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.72",
3
+ "version": "1.0.74",
4
4
  "description": "Keystone Design Bootstrap - Sections, Elements, and Theme System for customer websites",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -44,6 +44,13 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
44
44
  const [error, setError] = useState<string | null>(null);
45
45
  const [loading, setLoading] = useState(false);
46
46
 
47
+ // Additional contact method collected on the new-user step.
48
+ // Kept separate so typing here doesn't affect the identifier-step conditions.
49
+ const [additionalPhone, setAdditionalPhone] = useState('');
50
+ const [additionalEmail, setAdditionalEmail] = useState('');
51
+ // Snapshot of which method the user identified with, set when moving to step 'new'.
52
+ const [identifiedWith, setIdentifiedWith] = useState<'email' | 'phone' | null>(null);
53
+
47
54
  // Phone helpers
48
55
  const country = useMemo(() => countries.find((c) => c.code === selectedCountry), [selectedCountry]);
49
56
  const phoneCode = country ? (country.phoneCode.startsWith('+') ? country.phoneCode : `+${country.phoneCode}`) : '+1';
@@ -59,28 +66,23 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
59
66
  const fullPhone = phoneDigits.length > 0 ? `${phoneCode}${phoneDigits}` : null;
60
67
  const emailVal = email.trim() || null;
61
68
 
62
- const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
63
- const digits = e.target.value.replace(/\D/g, '');
64
- const formatted = nationalMask ? formatDigitsToMask(digits, nationalMask) : digits;
65
- setPhoneValue(formatted);
66
- };
67
-
68
- const emailInput = (
69
+ const emailInput = (value: string, onChange: (v: string) => void, required = false) => (
69
70
  <input
70
71
  type="email"
71
- value={email}
72
- onChange={(e) => setEmail(e.target.value)}
72
+ value={value}
73
+ onChange={(e) => onChange(e.target.value)}
73
74
  placeholder="you@example.com"
74
75
  className={inputClass}
75
76
  autoComplete="email"
77
+ required={required}
76
78
  />
77
79
  );
78
80
 
79
- const phoneInput = (
81
+ const phoneInput = (value: string, onChange: (v: string) => void, required = false) => (
80
82
  <div className="flex rounded-input border border-primary overflow-hidden focus-within:border-brand focus-within:ring-1 focus-within:ring-brand transition-colors">
81
83
  <select
82
84
  value={selectedCountry}
83
- onChange={(e) => { setSelectedCountry(e.target.value); setPhoneValue(''); }}
85
+ onChange={(e) => { setSelectedCountry(e.target.value); onChange(''); }}
84
86
  className="border-r border-secondary bg-secondary px-2 py-2.5 text-base text-secondary focus:outline-none"
85
87
  aria-label="Country code"
86
88
  >
@@ -90,11 +92,15 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
90
92
  </select>
91
93
  <input
92
94
  type="tel"
93
- value={phoneValue}
94
- onChange={handlePhoneChange}
95
+ value={value}
96
+ onChange={(e) => {
97
+ const digits = e.target.value.replace(/\D/g, '');
98
+ onChange(nationalMask ? formatDigitsToMask(digits, nationalMask) : digits);
99
+ }}
95
100
  placeholder={nationalPlaceholder}
96
101
  className="flex-1 px-3 py-2.5 text-base text-primary placeholder-quaternary bg-transparent focus:outline-none"
97
102
  autoComplete="tel"
103
+ required={required}
98
104
  />
99
105
  </div>
100
106
  );
@@ -118,11 +124,17 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
118
124
  firePixelEvent('Lead');
119
125
  captureEvent('portal_login_identified', { method, user_exists: true });
120
126
  setWelcomeName(result.firstName ?? null);
121
- setStep(result.hasPassword === false ? 'new' : 'returning');
127
+ if (result.hasPassword === false) {
128
+ setIdentifiedWith(emailVal ? 'email' : 'phone');
129
+ setStep('new');
130
+ } else {
131
+ setStep('returning');
132
+ }
122
133
  } else if (result.exists === false) {
123
134
  await setPixelUserData({ email: emailVal, phone: fullPhone });
124
135
  firePixelEvent('Lead');
125
136
  captureEvent('portal_login_identified', { method, user_exists: false });
137
+ setIdentifiedWith(emailVal ? 'email' : 'phone');
126
138
  setStep('new');
127
139
  } else {
128
140
  captureEvent('portal_login_failed', { step: 'identifier', reason: 'unknown_error' });
@@ -175,12 +187,16 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
175
187
  setError(null);
176
188
  setLoading(true);
177
189
  try {
190
+ const additionalPhoneDigits = additionalPhone.replace(/\D/g, '');
191
+ const additionalFullPhone = additionalPhoneDigits.length > 0 ? `${phoneCode}${additionalPhoneDigits}` : null;
192
+ const additionalEmailVal = additionalEmail.trim() || null;
193
+
178
194
  const res = await fetch('/api/consumer/signup', {
179
195
  method: 'POST',
180
196
  headers: { 'Content-Type': 'application/json' },
181
197
  body: JSON.stringify({
182
- email: emailVal,
183
- phone: fullPhone,
198
+ email: emailVal ?? additionalEmailVal,
199
+ phone: fullPhone ?? additionalFullPhone,
184
200
  password,
185
201
  password_confirmation: passwordConfirm,
186
202
  first_name: firstName.trim() || undefined,
@@ -207,6 +223,9 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
207
223
  setStep('identifier');
208
224
  setPassword('');
209
225
  setPasswordConfirm('');
226
+ setAdditionalPhone('');
227
+ setAdditionalEmail('');
228
+ setIdentifiedWith(null);
210
229
  setError(null);
211
230
  };
212
231
 
@@ -255,7 +274,7 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
255
274
  <form onSubmit={handleContinue} className="space-y-3">
256
275
  <div>
257
276
  <label className={labelClass}>Email address</label>
258
- {emailInput}
277
+ {emailInput(email, setEmail)}
259
278
  </div>
260
279
  <div className="flex items-center gap-3">
261
280
  <hr className="flex-1 border-secondary" />
@@ -264,7 +283,7 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
264
283
  </div>
265
284
  <div>
266
285
  <label className={labelClass}>Phone number</label>
267
- {phoneInput}
286
+ {phoneInput(phoneValue, setPhoneValue)}
268
287
  </div>
269
288
  <div className="pt-1">
270
289
  <button
@@ -345,16 +364,16 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
345
364
  </div>
346
365
  )}
347
366
  {/* Collect the missing contact method so the business can reach out */}
348
- {emailVal && !fullPhone && (
367
+ {identifiedWith === 'email' && (
349
368
  <div>
350
- <label className={labelClass}>Phone number <span className="text-quaternary">(optional)</span></label>
351
- {phoneInput}
369
+ <label className={labelClass}>Phone number</label>
370
+ {phoneInput(additionalPhone, setAdditionalPhone, true)}
352
371
  </div>
353
372
  )}
354
- {fullPhone && !emailVal && (
373
+ {identifiedWith === 'phone' && (
355
374
  <div>
356
- <label className={labelClass}>Email address <span className="text-quaternary">(optional)</span></label>
357
- {emailInput}
375
+ <label className={labelClass}>Email address</label>
376
+ {emailInput(additionalEmail, setAdditionalEmail, true)}
358
377
  </div>
359
378
  )}
360
379
  <div>