create-brainerce-store 1.14.5 → 1.15.0

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/dist/index.js CHANGED
@@ -31,7 +31,7 @@ var require_package = __commonJS({
31
31
  "package.json"(exports2, module2) {
32
32
  module2.exports = {
33
33
  name: "create-brainerce-store",
34
- version: "1.14.5",
34
+ version: "1.15.0",
35
35
  description: "Scaffold a production-ready e-commerce storefront connected to Brainerce",
36
36
  bin: {
37
37
  "create-brainerce-store": "dist/index.js"
package/messages/en.json CHANGED
@@ -116,6 +116,8 @@
116
116
  "pickUpInStore": "Pick up in store",
117
117
  "pickUpInStoreDesc": "Collect from a pickup location",
118
118
  "shippingAddress": "Shipping Address",
119
+ "contactInfo": "Contact Information",
120
+ "stepContactInfo": "Contact",
119
121
  "changeMethod": "Change method",
120
122
  "editAddress": "Edit address",
121
123
  "shippingMethod": "Shipping Method",
@@ -168,6 +170,7 @@
168
170
  "phone": "Phone",
169
171
  "phonePlaceholder": "+1234567890 (optional)",
170
172
  "continueToShipping": "Continue to Shipping",
173
+ "continueToPayment": "Continue to Payment",
171
174
  "countryPlaceholder": "e.g. US, IL, GB",
172
175
  "emailRequired": "Email is required",
173
176
  "emailInvalid": "Please enter a valid email",
@@ -180,7 +183,8 @@
180
183
  "privacyAcceptPrefix": "I have read and agree to the",
181
184
  "privacyPolicyLink": "Privacy Policy",
182
185
  "privacyRequired": "You must accept the privacy policy to continue",
183
- "acceptsMarketing": "Send me news, promotions, and updates by email"
186
+ "acceptsMarketing": "Send me news, promotions, and updates by email",
187
+ "saveDetailsForNextTime": "Save my details for faster checkout next time"
184
188
  },
185
189
  "auth": {
186
190
  "loginPageTitle": "Sign In",
package/messages/he.json CHANGED
@@ -116,6 +116,8 @@
116
116
  "pickUpInStore": "איסוף מהחנות",
117
117
  "pickUpInStoreDesc": "איסוף מנקודת איסוף",
118
118
  "shippingAddress": "כתובת למשלוח",
119
+ "contactInfo": "פרטי קשר",
120
+ "stepContactInfo": "פרטי קשר",
119
121
  "changeMethod": "שנה שיטה",
120
122
  "editAddress": "ערוך כתובת",
121
123
  "shippingMethod": "שיטת משלוח",
@@ -168,6 +170,7 @@
168
170
  "phone": "טלפון",
169
171
  "phonePlaceholder": "+972501234567 (אופציונלי)",
170
172
  "continueToShipping": "המשך למשלוח",
173
+ "continueToPayment": "המשך לתשלום",
171
174
  "countryPlaceholder": "לדוגמה: IL, US, GB",
172
175
  "emailRequired": "אימייל הוא שדה חובה",
173
176
  "emailInvalid": "כתובת אימייל לא תקינה",
@@ -180,7 +183,8 @@
180
183
  "privacyAcceptPrefix": "קראתי ואני מסכים/ה ל",
181
184
  "privacyPolicyLink": "מדיניות הפרטיות",
182
185
  "privacyRequired": "יש לאשר את מדיניות הפרטיות כדי להמשיך",
183
- "acceptsMarketing": "שלחו לי חדשות, מבצעים ועדכונים במייל"
186
+ "acceptsMarketing": "שלחו לי חדשות, מבצעים ועדכונים במייל",
187
+ "saveDetailsForNextTime": "שמרו את הפרטים שלי לתשלום מהיר בפעם הבאה"
184
188
  },
185
189
  "auth": {
186
190
  "loginPageTitle": "התחברות",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-brainerce-store",
3
- "version": "1.14.5",
3
+ "version": "1.15.0",
4
4
  "description": "Scaffold a production-ready e-commerce storefront connected to Brainerce",
5
5
  "bin": {
6
6
  "create-brainerce-store": "dist/index.js"
@@ -36,7 +36,6 @@ function CheckoutContent() {
36
36
  const currency = storeInfo?.currency || 'USD';
37
37
  const t = useTranslations('checkout');
38
38
  const tc = useTranslations('common');
39
- const tAddr = useTranslations('checkoutAddress');
40
39
 
41
40
  const [step, setStep] = useState<CheckoutStep>('address');
42
41
  const [checkout, setCheckout] = useState<Checkout | null>(null);
@@ -50,23 +49,29 @@ function CheckoutContent() {
50
49
  const [deliveryType, setDeliveryType] = useState<'shipping' | 'pickup'>('shipping');
51
50
  const [isAllDigital, setIsAllDigital] = useState(false);
52
51
  const [prefillAddress, setPrefillAddress] = useState<SetShippingAddressDto | null>(null);
53
- const [lastSubmittedAddress, setLastSubmittedAddress] = useState<SetShippingAddressDto | null>(
54
- null
55
- );
56
- const [showSavePrompt, setShowSavePrompt] = useState(false);
57
- const [savingAddress, setSavingAddress] = useState(false);
52
+ const [prefillCustomer, setPrefillCustomer] = useState<{
53
+ email: string;
54
+ firstName?: string;
55
+ lastName?: string;
56
+ phone?: string;
57
+ } | null>(null);
58
+ const [hasSavedAddress, setHasSavedAddress] = useState(false);
58
59
 
59
60
  // Check for returning from canceled payment
60
61
  const canceled = searchParams.get('canceled') === 'true';
61
62
  const existingCheckoutId = searchParams.get('checkout_id');
62
63
 
63
- // Pre-fill address from customer profile when logged in
64
+ // Pre-fill address and customer data from profile when logged in
64
65
  useEffect(() => {
65
66
  if (!isLoggedIn) return;
66
67
  getClient()
67
68
  .getCheckoutPrefillData()
68
69
  .then((data) => {
69
- if (data.shippingAddress) setPrefillAddress(data.shippingAddress);
70
+ if (data.customer) setPrefillCustomer(data.customer);
71
+ if (data.shippingAddress) {
72
+ setPrefillAddress(data.shippingAddress);
73
+ setHasSavedAddress(true);
74
+ }
70
75
  })
71
76
  .catch(() => {});
72
77
  }, [isLoggedIn]);
@@ -98,7 +103,8 @@ function CheckoutContent() {
98
103
  );
99
104
  setIsAllDigital(allDigital);
100
105
  if (allDigital) {
101
- setStep('payment');
106
+ // Digital products: show contact info step if email not set, else payment
107
+ setStep(existing.email ? 'payment' : 'address');
102
108
  } else if (existing.deliveryType === 'pickup' && existing.pickupLocation) {
103
109
  setDeliveryType('pickup');
104
110
  setStep('payment');
@@ -120,13 +126,13 @@ function CheckoutContent() {
120
126
  const newCheckout = await client.createCheckout({ cartId: cart.id });
121
127
  setCheckout(newCheckout);
122
128
 
123
- // If all items are downloadable, skip shipping entirely
129
+ // If all items are downloadable, skip shipping — show contact info step
124
130
  const allDigital = newCheckout.lineItems.every(
125
131
  (i) => (i.product as unknown as { isDownloadable?: boolean }).isDownloadable
126
132
  );
127
133
  setIsAllDigital(allDigital);
128
134
  if (allDigital) {
129
- setStep('payment');
135
+ setStep('address');
130
136
  return;
131
137
  }
132
138
  } else {
@@ -155,7 +161,7 @@ function CheckoutContent() {
155
161
  // Handle shipping address submission
156
162
  async function handleAddressSubmit(
157
163
  address: SetShippingAddressDto,
158
- consent: { acceptsMarketing: boolean }
164
+ consent: { acceptsMarketing: boolean; saveDetails: boolean }
159
165
  ) {
160
166
  if (!checkout) return;
161
167
 
@@ -164,9 +170,23 @@ function CheckoutContent() {
164
170
  setError(null);
165
171
  const client = getClient();
166
172
 
167
- const response = await client.setShippingAddress(checkout.id, address);
168
- setCheckout(response.checkout);
169
- setShippingRates(response.rates);
173
+ if (isAllDigital) {
174
+ // Digital products: set customer info only, skip shipping
175
+ const updated = await client.setCheckoutCustomer(checkout.id, {
176
+ email: address.email,
177
+ firstName: address.firstName,
178
+ lastName: address.lastName,
179
+ phone: address.phone,
180
+ acceptsMarketing: consent.acceptsMarketing,
181
+ });
182
+ setCheckout(updated);
183
+ setStep('payment');
184
+ } else {
185
+ const response = await client.setShippingAddress(checkout.id, address);
186
+ setCheckout(response.checkout);
187
+ setShippingRates(response.rates);
188
+ setStep('shipping');
189
+ }
170
190
 
171
191
  // Update marketing preference for logged-in users
172
192
  if (isLoggedIn) {
@@ -177,13 +197,25 @@ function CheckoutContent() {
177
197
  }
178
198
  }
179
199
 
180
- // Offer to save address to profile if logged in and no prefill (new address)
181
- if (isLoggedIn && !prefillAddress) {
182
- setLastSubmittedAddress(address);
183
- setShowSavePrompt(true);
200
+ // Save address to profile if checkbox was checked and no existing saved address
201
+ if (isLoggedIn && consent.saveDetails && !hasSavedAddress && !isAllDigital) {
202
+ try {
203
+ await client.addMyAddress({
204
+ firstName: address.firstName,
205
+ lastName: address.lastName,
206
+ line1: address.line1,
207
+ line2: address.line2,
208
+ city: address.city,
209
+ region: address.region,
210
+ postalCode: address.postalCode,
211
+ country: address.country,
212
+ phone: address.phone,
213
+ isDefault: true,
214
+ });
215
+ } catch {
216
+ // non-critical
217
+ }
184
218
  }
185
-
186
- setStep('shipping');
187
219
  } catch (err) {
188
220
  const message = err instanceof Error ? err.message : t('failedToSaveAddress');
189
221
  setError(message);
@@ -192,30 +224,6 @@ function CheckoutContent() {
192
224
  }
193
225
  }
194
226
 
195
- async function handleSaveAddressToProfile() {
196
- if (!lastSubmittedAddress) return;
197
- setSavingAddress(true);
198
- try {
199
- await getClient().addMyAddress({
200
- firstName: lastSubmittedAddress.firstName,
201
- lastName: lastSubmittedAddress.lastName,
202
- line1: lastSubmittedAddress.line1,
203
- line2: lastSubmittedAddress.line2,
204
- city: lastSubmittedAddress.city,
205
- region: lastSubmittedAddress.region,
206
- postalCode: lastSubmittedAddress.postalCode,
207
- country: lastSubmittedAddress.country,
208
- phone: lastSubmittedAddress.phone,
209
- isDefault: true,
210
- });
211
- } catch {
212
- // ignore
213
- } finally {
214
- setSavingAddress(false);
215
- setShowSavePrompt(false);
216
- }
217
- }
218
-
219
227
  // Handle shipping method selection
220
228
  async function handleShippingSelect(rateId: string) {
221
229
  if (!checkout) return;
@@ -345,7 +353,10 @@ function CheckoutContent() {
345
353
  }
346
354
 
347
355
  const steps: { key: CheckoutStep; label: string }[] = isAllDigital
348
- ? [{ key: 'payment', label: t('stepPayment') }]
356
+ ? [
357
+ { key: 'address', label: t('stepContactInfo') },
358
+ { key: 'payment', label: t('stepPayment') },
359
+ ]
349
360
  : pickupLocations.length > 0
350
361
  ? deliveryType === 'pickup'
351
362
  ? [
@@ -456,8 +467,10 @@ function CheckoutContent() {
456
467
  {step === 'address' && (
457
468
  <div>
458
469
  <div className="mb-4 flex items-center justify-between">
459
- <h2 className="text-foreground text-lg font-semibold">{t('shippingAddress')}</h2>
460
- {pickupLocations.length > 0 && (
470
+ <h2 className="text-foreground text-lg font-semibold">
471
+ {isAllDigital ? t('contactInfo') : t('shippingAddress')}
472
+ </h2>
473
+ {!isAllDigital && pickupLocations.length > 0 && (
461
474
  <button
462
475
  type="button"
463
476
  onClick={() => setStep('method')}
@@ -467,33 +480,12 @@ function CheckoutContent() {
467
480
  </button>
468
481
  )}
469
482
  </div>
470
- {/* Save-to-profile prompt */}
471
- {showSavePrompt && (
472
- <div className="border-border bg-muted/50 mb-4 flex items-center justify-between gap-3 rounded-lg border px-4 py-3 text-sm">
473
- <span className="text-foreground">{tAddr('saveToProfile')}</span>
474
- <div className="flex gap-2">
475
- <button
476
- type="button"
477
- onClick={handleSaveAddressToProfile}
478
- disabled={savingAddress}
479
- className="bg-primary text-primary-foreground rounded px-3 py-1 text-xs font-medium transition-opacity hover:opacity-90 disabled:opacity-50"
480
- >
481
- {savingAddress ? '...' : tAddr('saveYes')}
482
- </button>
483
- <button
484
- type="button"
485
- onClick={() => setShowSavePrompt(false)}
486
- className="text-muted-foreground hover:text-foreground rounded px-3 py-1 text-xs transition-colors"
487
- >
488
- {tAddr('saveNo')}
489
- </button>
490
- </div>
491
- </div>
492
- )}
493
483
  <CheckoutForm
494
484
  onSubmit={handleAddressSubmit}
495
485
  loading={loading}
496
- destinations={destinations}
486
+ destinations={isAllDigital ? null : destinations}
487
+ showSaveDetails={isLoggedIn && !hasSavedAddress && !isAllDigital}
488
+ emailOnly={isAllDigital}
497
489
  initialValues={
498
490
  checkout?.shippingAddress
499
491
  ? {
@@ -521,7 +513,14 @@ function CheckoutContent() {
521
513
  country: prefillAddress.country,
522
514
  phone: prefillAddress.phone || '',
523
515
  }
524
- : undefined
516
+ : prefillCustomer
517
+ ? {
518
+ email: prefillCustomer.email,
519
+ firstName: prefillCustomer.firstName || '',
520
+ lastName: prefillCustomer.lastName || '',
521
+ phone: prefillCustomer.phone || '',
522
+ }
523
+ : undefined
525
524
  }
526
525
  />
527
526
  </div>
@@ -1,12 +1,13 @@
1
1
  'use client';
2
2
 
3
- import { useState } from 'react';
3
+ import { useState, useEffect, useRef } from 'react';
4
4
  import type { SetShippingAddressDto, ShippingDestinations } from 'brainerce';
5
5
  import { useTranslations } from '@/lib/translations';
6
6
  import { cn } from '@/lib/utils';
7
7
 
8
8
  interface CheckoutConsent {
9
9
  acceptsMarketing: boolean;
10
+ saveDetails: boolean;
10
11
  }
11
12
 
12
13
  interface CheckoutFormProps {
@@ -15,6 +16,8 @@ interface CheckoutFormProps {
15
16
  initialValues?: Partial<SetShippingAddressDto>;
16
17
  destinations?: ShippingDestinations | null;
17
18
  className?: string;
19
+ showSaveDetails?: boolean;
20
+ emailOnly?: boolean;
18
21
  }
19
22
 
20
23
  export function CheckoutForm({
@@ -23,6 +26,8 @@ export function CheckoutForm({
23
26
  initialValues,
24
27
  destinations,
25
28
  className,
29
+ showSaveDetails = false,
30
+ emailOnly = false,
26
31
  }: CheckoutFormProps) {
27
32
  const [formData, setFormData] = useState<SetShippingAddressDto>({
28
33
  email: initialValues?.email || '',
@@ -39,8 +44,28 @@ export function CheckoutForm({
39
44
  const [errors, setErrors] = useState<Record<string, string>>({});
40
45
  const [privacyAccepted, setPrivacyAccepted] = useState(false);
41
46
  const [acceptsMarketing, setAcceptsMarketing] = useState(false);
47
+ const [saveDetails, setSaveDetails] = useState(true);
42
48
  const t = useTranslations('checkoutForm');
43
49
  const tc = useTranslations('common');
50
+ const hasAppliedPrefill = useRef(!!initialValues);
51
+
52
+ // Sync prefill data when it arrives async (e.g. from getCheckoutPrefillData)
53
+ useEffect(() => {
54
+ if (!initialValues || hasAppliedPrefill.current) return;
55
+ hasAppliedPrefill.current = true;
56
+ setFormData((prev) => ({
57
+ email: initialValues.email || prev.email,
58
+ firstName: initialValues.firstName || prev.firstName,
59
+ lastName: initialValues.lastName || prev.lastName,
60
+ line1: initialValues.line1 || prev.line1,
61
+ line2: initialValues.line2 || prev.line2 || '',
62
+ city: initialValues.city || prev.city,
63
+ region: initialValues.region || prev.region || '',
64
+ postalCode: initialValues.postalCode || prev.postalCode,
65
+ country: initialValues.country || prev.country,
66
+ phone: initialValues.phone || prev.phone || '',
67
+ }));
68
+ }, [initialValues]);
44
69
 
45
70
  const hasCountryOptions = destinations && destinations.countries.length > 0;
46
71
  const countryRegions = destinations?.regions[formData.country];
@@ -61,17 +86,19 @@ export function CheckoutForm({
61
86
  if (!formData.lastName.trim()) {
62
87
  newErrors.lastName = t('lastNameRequired');
63
88
  }
64
- if (!formData.line1.trim()) {
65
- newErrors.line1 = t('addressRequired');
66
- }
67
- if (!formData.city.trim()) {
68
- newErrors.city = t('cityRequired');
69
- }
70
- if (!formData.postalCode.trim()) {
71
- newErrors.postalCode = t('postalCodeRequired');
72
- }
73
- if (!formData.country.trim()) {
74
- newErrors.country = t('countryRequired');
89
+ if (!emailOnly) {
90
+ if (!formData.line1.trim()) {
91
+ newErrors.line1 = t('addressRequired');
92
+ }
93
+ if (!formData.city.trim()) {
94
+ newErrors.city = t('cityRequired');
95
+ }
96
+ if (!formData.postalCode.trim()) {
97
+ newErrors.postalCode = t('postalCodeRequired');
98
+ }
99
+ if (!formData.country.trim()) {
100
+ newErrors.country = t('countryRequired');
101
+ }
75
102
  }
76
103
  if (!privacyAccepted) {
77
104
  newErrors.privacy = t('privacyRequired');
@@ -84,7 +111,7 @@ export function CheckoutForm({
84
111
  function handleSubmit(e: React.FormEvent) {
85
112
  e.preventDefault();
86
113
  if (validate()) {
87
- onSubmit(formData, { acceptsMarketing });
114
+ onSubmit(formData, { acceptsMarketing, saveDetails: showSaveDetails && saveDetails });
88
115
  }
89
116
  }
90
117
 
@@ -160,6 +187,8 @@ export function CheckoutForm({
160
187
  </div>
161
188
  </div>
162
189
 
190
+ {!emailOnly && (
191
+ <>
163
192
  {/* Country + Region row */}
164
193
  <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
165
194
  <div>
@@ -301,6 +330,8 @@ export function CheckoutForm({
301
330
  placeholder={t('phonePlaceholder')}
302
331
  />
303
332
  </div>
333
+ </>
334
+ )}
304
335
 
305
336
  {/* Privacy Policy (required) */}
306
337
  <div>
@@ -347,12 +378,25 @@ export function CheckoutForm({
347
378
  <span className="text-muted-foreground text-sm">{t('acceptsMarketing')}</span>
348
379
  </label>
349
380
 
381
+ {/* Save details for next time (logged-in users only) */}
382
+ {showSaveDetails && (
383
+ <label className="flex cursor-pointer items-start gap-2">
384
+ <input
385
+ type="checkbox"
386
+ checked={saveDetails}
387
+ onChange={(e) => setSaveDetails(e.target.checked)}
388
+ className="accent-primary mt-0.5"
389
+ />
390
+ <span className="text-muted-foreground text-sm">{t('saveDetailsForNextTime')}</span>
391
+ </label>
392
+ )}
393
+
350
394
  <button
351
395
  type="submit"
352
396
  disabled={loading}
353
397
  className="bg-primary text-primary-foreground w-full rounded px-6 py-3 text-sm font-medium transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60"
354
398
  >
355
- {loading ? tc('saving') : t('continueToShipping')}
399
+ {loading ? tc('saving') : emailOnly ? t('continueToPayment') : t('continueToShipping')}
356
400
  </button>
357
401
  </form>
358
402
  );