create-brainerce-store 1.61.0 → 1.62.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.61.0",
34
+ version: "1.62.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"
@@ -187,7 +187,12 @@ var BRAINERCE_RUNTIME_DEPS = Object.freeze({
187
187
  // label live carrier rates, so an older SDK fails type-check at scaffold time
188
188
  // — plus getBusinessHoursForDate(), which the scaffolded DATETIME checkout
189
189
  // field uses to tell "day closed" from "field isn't slot-based".
190
- brainerce: "^1.52.0",
190
+ // 1.53 strips lat/lng/formattedAddress from setShippingAddress/
191
+ // setBillingAddress bodies. The address endpoints reject ANY unknown
192
+ // property with a 400 that blocks checkout outright, and those three come
193
+ // straight off getAddressDetails() — so a scaffolded store on an older SDK
194
+ // is one `...address` spread away from a checkout nobody can complete.
195
+ brainerce: "^1.53.0",
191
196
  "isomorphic-dompurify": "^3.8.0"
192
197
  });
193
198
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-brainerce-store",
3
- "version": "1.61.0",
3
+ "version": "1.62.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"
@@ -13,6 +13,18 @@ import { getClient } from '@/core/lib/brainerce';
13
13
  const ADDRESS_SEARCH_DEBOUNCE_MS = 300;
14
14
  const ADDRESS_SEARCH_MIN_CHARS = 3;
15
15
 
16
+ // Editing any of these invalidates a previously picked suggestion: its
17
+ // coordinates describe the address the shopper selected, not what is in the
18
+ // inputs now. See `pickedPlace` below.
19
+ const PLACE_INVALIDATING_FIELDS = new Set<string>([
20
+ 'line1',
21
+ 'line2',
22
+ 'city',
23
+ 'region',
24
+ 'postalCode',
25
+ 'country',
26
+ ]);
27
+
16
28
  interface CheckoutConsent {
17
29
  acceptsMarketing: boolean;
18
30
  saveDetails: boolean;
@@ -65,6 +77,16 @@ export function CheckoutForm({
65
77
  const [showAddressSuggestions, setShowAddressSuggestions] = useState(false);
66
78
  const [isSearchingAddress, setIsSearchingAddress] = useState(false);
67
79
  const [outsideDeliveryZone, setOutsideDeliveryZone] = useState(false);
80
+ // The suggestion the shopper actually picked, kept so it can ride along to
81
+ // `setShippingAddress`. That is what lets the server match map-drawn
82
+ // ("polygon") delivery zones against this address's exact coordinates
83
+ // instead of geocoding the typed text — skipping it is the most common
84
+ // cause of "the store says it doesn't deliver here, but it does". Null
85
+ // whenever the address was typed by hand or edited after picking.
86
+ const [pickedPlace, setPickedPlace] = useState<{
87
+ placeId: string;
88
+ sessionToken: string;
89
+ } | null>(null);
68
90
  const addressSessionToken = useRef(
69
91
  typeof crypto !== 'undefined' ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`
70
92
  );
@@ -108,10 +130,14 @@ export function CheckoutForm({
108
130
 
109
131
  async function handleSelectAddressSuggestion(suggestion: AddressSuggestion) {
110
132
  setShowAddressSuggestions(false);
133
+ // The token this resolution is billed under — hand the SAME one to
134
+ // `setShippingAddress` so a server-side cache miss re-resolves within this
135
+ // autocomplete session instead of opening a new (billed) one.
136
+ const resolvingSessionToken = addressSessionToken.current;
111
137
  try {
112
138
  const { address, inZone } = await getClient().getAddressDetails(
113
139
  suggestion.placeId,
114
- addressSessionToken.current
140
+ resolvingSessionToken
115
141
  );
116
142
  setFormData((prev) => {
117
143
  const country = address.country || prev.country;
@@ -138,9 +164,15 @@ export function CheckoutForm({
138
164
  };
139
165
  });
140
166
  setOutsideDeliveryZone(!inZone);
167
+ // Note we deliberately do NOT copy address.lat/lng into the form: no
168
+ // address endpoint accepts coordinates (zone matching decides which rate
169
+ // is charged, so the server resolves them from the placeId itself).
170
+ // They're yours to use for a map pin and nothing else.
171
+ setPickedPlace({ placeId: suggestion.placeId, sessionToken: resolvingSessionToken });
141
172
  } catch {
142
173
  // Resolution failed — keep whatever the shopper had typed/selected as
143
174
  // the visible text; they can still fill in the rest manually.
175
+ setPickedPlace(null);
144
176
  } finally {
145
177
  // New session for the next address-entry attempt.
146
178
  addressSessionToken.current =
@@ -212,7 +244,17 @@ export function CheckoutForm({
212
244
  function handleSubmit(e: React.FormEvent) {
213
245
  e.preventDefault();
214
246
  if (validate()) {
215
- onSubmit(formData, { acceptsMarketing, saveDetails: showSaveDetails && saveDetails });
247
+ onSubmit(
248
+ {
249
+ ...formData,
250
+ // Both undefined when the shopper typed the address by hand or
251
+ // edited it after picking a suggestion — the server then geocodes
252
+ // the text, which is the right behaviour once the two disagree.
253
+ placeId: pickedPlace?.placeId,
254
+ placeSessionToken: pickedPlace?.sessionToken,
255
+ },
256
+ { acceptsMarketing, saveDetails: showSaveDetails && saveDetails }
257
+ );
216
258
  }
217
259
  }
218
260
 
@@ -225,6 +267,12 @@ export function CheckoutForm({
225
267
  }
226
268
  return next;
227
269
  });
270
+ // Once any address field is edited, the picked suggestion no longer
271
+ // describes what is in the form — keeping its placeId would match delivery
272
+ // zones against the address the shopper originally selected.
273
+ if (PLACE_INVALIDATING_FIELDS.has(field)) {
274
+ setPickedPlace(null);
275
+ }
228
276
  if (errors[field]) {
229
277
  setErrors((prev) => {
230
278
  const next = { ...prev };