@pradip1995/segment-jewelry-checkout-form 0.1.3 → 0.1.4

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": "@pradip1995/segment-jewelry-checkout-form",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -16,10 +16,6 @@
16
16
  "./manifest": "./src/manifest.ts",
17
17
  "./payment-block": "./src/payment-block.tsx"
18
18
  },
19
- "scripts": {
20
- "typecheck": "tsc --noEmit",
21
- "lint": "tsc --noEmit"
22
- },
23
19
  "peerDependencies": {
24
20
  "@pradip1995/commerce-core": "^4.0.0",
25
21
  "@pradip1995/plugin-sdk": "^0.2.0",
@@ -38,5 +34,9 @@
38
34
  "@types/react": "^19",
39
35
  "react": "19.0.3",
40
36
  "typescript": "^5.7.2"
37
+ },
38
+ "scripts": {
39
+ "typecheck": "tsc --noEmit",
40
+ "lint": "tsc --noEmit"
41
41
  }
42
- }
42
+ }
@@ -100,22 +100,37 @@ function addressToFormValues(
100
100
  }
101
101
  }
102
102
 
103
- function pickPreferredAddress(
104
- customer?: HttpTypes.StoreCustomer | null
103
+ function pickPreferredFromList(
104
+ addresses: HttpTypes.StoreCustomerAddress[]
105
105
  ): HttpTypes.StoreCustomerAddress | undefined {
106
- const addresses = customer?.addresses ?? []
107
106
  return (
108
107
  addresses.find((a) => a.is_default_shipping) ||
108
+ addresses.find((a) => a.is_default_billing) ||
109
109
  addresses.find(
110
- (a) =>
111
- a.is_default_billing ||
112
- a.metadata?.is_default === true ||
113
- a.metadata?.is_default === "true"
110
+ (a) => a.metadata?.is_default === true || a.metadata?.is_default === "true"
114
111
  ) ||
115
112
  addresses[0]
116
113
  )
117
114
  }
118
115
 
116
+ function pickPreferredAddress(
117
+ customer?: HttpTypes.StoreCustomer | null
118
+ ): HttpTypes.StoreCustomerAddress | undefined {
119
+ return pickPreferredFromList(customer?.addresses ?? [])
120
+ }
121
+
122
+ function sortAddressesDefaultFirst(
123
+ addresses: HttpTypes.StoreCustomerAddress[]
124
+ ): HttpTypes.StoreCustomerAddress[] {
125
+ const preferred = pickPreferredFromList(addresses)
126
+ if (!preferred?.id) return addresses
127
+ return [...addresses].sort((left, right) => {
128
+ if (left.id === preferred.id) return -1
129
+ if (right.id === preferred.id) return 1
130
+ return 0
131
+ })
132
+ }
133
+
119
134
  function findMatchingSavedAddress(
120
135
  cart: HttpTypes.StoreCart | undefined,
121
136
  addresses: HttpTypes.StoreCustomerAddress[]
@@ -143,7 +158,7 @@ function buildInitialValues(
143
158
  const shipping = cart?.shipping_address
144
159
 
145
160
  if (addresses.length) {
146
- const source = matched || preferred
161
+ const source = preferred || matched
147
162
  if (source) {
148
163
  return addressToFormValues(source, {
149
164
  email,
@@ -179,29 +194,137 @@ function resolveInitialSelectedId(
179
194
  const addresses = customer?.addresses ?? []
180
195
  if (!addresses.length) return NEW_ADDRESS_ID
181
196
 
182
- const matched = findMatchingSavedAddress(cart, addresses)
183
- if (matched?.id) return matched.id
184
-
185
197
  const preferred = pickPreferredAddress(customer)
186
198
  if (preferred?.id) return preferred.id
187
199
 
200
+ const matched = findMatchingSavedAddress(cart, addresses)
201
+ if (matched?.id) return matched.id
202
+
188
203
  return NEW_ADDRESS_ID
189
204
  }
190
205
 
191
- function formatAddressLine(address: {
206
+ function formatAddressParts(address: {
192
207
  address1?: string | null
193
208
  address2?: string | null
194
209
  city?: string | null
195
210
  province?: string | null
196
211
  postalCode?: string | null
197
212
  countryCode?: string | null
198
- }): string {
213
+ }): { street: string; cityLine: string } {
214
+ const street = [address.address1, address.address2].filter(Boolean).join(", ")
199
215
  const locality = [address.city, address.province].filter(Boolean).join(", ")
200
216
  const pin = address.postalCode ? `${locality ? " - " : ""}${address.postalCode}` : ""
201
217
  const country = (address.countryCode || "").toUpperCase()
202
- return [address.address1, address.address2, `${locality}${pin}`, country]
203
- .filter(Boolean)
204
- .join(", ")
218
+ const cityLine = [`${locality}${pin}`, country].filter(Boolean).join(", ")
219
+ return { street, cityLine }
220
+ }
221
+
222
+ function formatMobileDisplay(phone?: string | null): string {
223
+ const digits = (phone || "").replace(/\D/g, "")
224
+ if (!digits) return ""
225
+ if (digits.length === 10) return `91${digits}`
226
+ return digits
227
+ }
228
+
229
+ function resolveAddressType(value: unknown): "HOME" | "OFFICE" {
230
+ return String(value ?? "").trim().toUpperCase() === "OFFICE" ? "OFFICE" : "HOME"
231
+ }
232
+
233
+ function addressTypeFromMetadata(metadata: unknown): "HOME" | "OFFICE" {
234
+ if (!metadata || typeof metadata !== "object") return "HOME"
235
+ return resolveAddressType((metadata as Record<string, unknown>).address_type)
236
+ }
237
+
238
+ function CheckoutAddressCard({
239
+ name,
240
+ street,
241
+ cityLine,
242
+ phone,
243
+ addressType = "HOME",
244
+ selected = false,
245
+ onEdit,
246
+ onSelect,
247
+ }: {
248
+ name: string
249
+ street: string
250
+ cityLine: string
251
+ phone?: string | null
252
+ addressType?: "HOME" | "OFFICE"
253
+ selected?: boolean
254
+ onEdit?: () => void
255
+ onSelect?: () => void
256
+ }) {
257
+ const mobile = formatMobileDisplay(phone)
258
+ const interactive = Boolean(onSelect)
259
+
260
+ return (
261
+ <div
262
+ className={[
263
+ "jewelry-checkout-address-card__body",
264
+ selected ? "jewelry-checkout-address-card__body--selected" : "",
265
+ interactive ? "jewelry-checkout-address-card__body--pickable" : "",
266
+ ].join(" ")}
267
+ role={interactive ? "button" : undefined}
268
+ tabIndex={interactive ? 0 : undefined}
269
+ aria-pressed={interactive ? selected : undefined}
270
+ onClick={onSelect}
271
+ onKeyDown={
272
+ onSelect
273
+ ? (event) => {
274
+ if (event.key === "Enter" || event.key === " ") {
275
+ event.preventDefault()
276
+ onSelect()
277
+ }
278
+ }
279
+ : undefined
280
+ }
281
+ >
282
+ <span
283
+ className={[
284
+ "jewelry-checkout-address-card__radio",
285
+ selected ? "" : "jewelry-checkout-address-card__radio--empty",
286
+ ].join(" ")}
287
+ aria-hidden
288
+ >
289
+ <span />
290
+ </span>
291
+ <div className="jewelry-checkout-address-card__content">
292
+ <div className="jewelry-checkout-address-card__name-row">
293
+ <span className="jewelry-checkout-address-card__name">{name}</span>
294
+ <span className="jewelry-checkout-address-card__tag">{addressType === "OFFICE" ? "Office" : "Home"}</span>
295
+ </div>
296
+ {street ? <span className="jewelry-checkout-address-card__line">{street}</span> : null}
297
+ {cityLine ? (
298
+ <span className="jewelry-checkout-address-card__line jewelry-checkout-address-card__line--city">
299
+ {cityLine}
300
+ </span>
301
+ ) : null}
302
+ {mobile ? (
303
+ <span className="jewelry-checkout-address-card__mobile">
304
+ <span>Mobile</span> {mobile}
305
+ </span>
306
+ ) : null}
307
+ <span className="jewelry-checkout-address-card__pod">Pay on Delivery available</span>
308
+ </div>
309
+ {onEdit ? (
310
+ <button
311
+ type="button"
312
+ className="jewelry-checkout-address-card__edit"
313
+ onClick={onEdit}
314
+ aria-label="Edit delivery address"
315
+ >
316
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
317
+ <path strokeLinecap="round" strokeLinejoin="round" d="M12 20h9" />
318
+ <path
319
+ strokeLinecap="round"
320
+ strokeLinejoin="round"
321
+ d="M16.5 3.5a2.12 2.12 0 013 3L7 19l-4 1 1-4 12.5-12.5z"
322
+ />
323
+ </svg>
324
+ </button>
325
+ ) : null}
326
+ </div>
327
+ )
205
328
  }
206
329
 
207
330
  function composeFullName(firstName: string, lastName: string): string {
@@ -249,7 +372,7 @@ export default function AddressFields({
249
372
  onValuesChange,
250
373
  }: AddressFieldsProps) {
251
374
  const router = useRouter()
252
- const savedAddresses = customer?.addresses ?? []
375
+ const savedAddresses = sortAddressesDefaultFirst(customer?.addresses ?? [])
253
376
  const [values, setValues] = useState(() => buildInitialValues(cart, customer))
254
377
  const [selectedAddressId, setSelectedAddressId] = useState(() =>
255
378
  resolveInitialSelectedId(cart, customer)
@@ -259,6 +382,7 @@ export default function AddressFields({
259
382
  const [draft, setDraft] = useState<CheckoutFormValues | null>(null)
260
383
  const [draftError, setDraftError] = useState("")
261
384
  const [saveAsDefault, setSaveAsDefault] = useState(true)
385
+ const [addressType, setAddressType] = useState<"HOME" | "OFFICE">("HOME")
262
386
  const [acceptedManualAddress, setAcceptedManualAddress] = useState(false)
263
387
  const [phoneError, setPhoneError] = useState(false)
264
388
  const [sameAsBilling, setSameAsBilling] = useState(true)
@@ -291,12 +415,18 @@ export default function AddressFields({
291
415
 
292
416
  useEffect(() => {
293
417
  if (didPrefillDefaultRef.current) return
294
- if (cart.shipping_address?.address_1) return
295
418
  if (selectedAddressId === NEW_ADDRESS_ID) return
296
419
 
297
420
  const selected = savedAddresses.find((address) => address.id === selectedAddressId)
298
421
  if (!selected) return
299
422
 
423
+ const cartAlreadyMatches =
424
+ findMatchingSavedAddress(cart, [selected])?.id === selected.id
425
+ if (cartAlreadyMatches) {
426
+ didPrefillDefaultRef.current = true
427
+ return
428
+ }
429
+
300
430
  didPrefillDefaultRef.current = true
301
431
  void applySavedAddress(selected, { refresh: true })
302
432
  // eslint-disable-next-line react-hooks/exhaustive-deps -- run once on mount for default address prefill
@@ -436,6 +566,7 @@ export default function AddressFields({
436
566
  const openAddAddress = () => {
437
567
  setDraftError("")
438
568
  setSaveAsDefault(savedAddresses.length === 0)
569
+ setAddressType("HOME")
439
570
  setDraft(emptyAddressDraft(values))
440
571
  setAddressModal("add")
441
572
  }
@@ -447,6 +578,7 @@ export default function AddressFields({
447
578
  !!(editingSaved?.is_default_shipping || editingSaved?.is_default_billing) ||
448
579
  savedAddresses.length === 0
449
580
  )
581
+ setAddressType(addressTypeFromMetadata(editingSaved?.metadata))
450
582
  setDraft({ ...values })
451
583
  setAddressModal("edit")
452
584
  }
@@ -523,6 +655,7 @@ export default function AddressFields({
523
655
  formData.set("postal_code", nextValues.postalCode)
524
656
  formData.set("country_code", nextValues.countryCode)
525
657
  formData.set("phone", nextValues.phone)
658
+ formData.set("address_type", addressType)
526
659
  if (saveAsDefault) formData.set("is_default", "on")
527
660
 
528
661
  try {
@@ -566,7 +699,9 @@ export default function AddressFields({
566
699
  const hasSavedAddresses = savedAddresses.length > 0
567
700
  const hasSelectedAddress = Boolean(selectedSaved) || acceptedManualAddress
568
701
  const isDefaultSelected = !!(
569
- selectedSaved?.is_default_shipping || selectedSaved?.is_default_billing
702
+ selectedSaved?.is_default_shipping ||
703
+ (!savedAddresses.some((address) => address.is_default_shipping) &&
704
+ selectedSaved?.is_default_billing)
570
705
  )
571
706
 
572
707
  if (!hasSavedAddresses) {
@@ -775,6 +910,7 @@ export default function AddressFields({
775
910
  onChange={(value) => updateDraft("email", value)}
776
911
  full
777
912
  />
913
+ <AddressTypePicker value={addressType} onChange={setAddressType} />
778
914
  <label className="jewelry-checkout-modal__check">
779
915
  <input
780
916
  type="checkbox"
@@ -839,45 +975,14 @@ export default function AddressFields({
839
975
  <div className="jewelry-checkout-address-card__badge">
840
976
  {isDefaultSelected ? "Default address" : "Delivery address"}
841
977
  </div>
842
- <div className="jewelry-checkout-address-card__body">
843
- <span className="jewelry-checkout-address-card__radio" aria-hidden>
844
- <span />
845
- </span>
846
- <div className="jewelry-checkout-address-card__content">
847
- <div className="jewelry-checkout-address-card__name-row">
848
- <p className="jewelry-checkout-address-card__name">
849
- {values.fullName || "Delivery address"}
850
- </p>
851
- <span className="jewelry-checkout-address-card__tag">Home</span>
852
- </div>
853
- <p className="jewelry-checkout-address-card__line">
854
- {formatAddressLine({
855
- address1: values.address1,
856
- address2: values.address2,
857
- city: values.city,
858
- province: values.province,
859
- postalCode: values.postalCode,
860
- countryCode: values.countryCode,
861
- })}
862
- </p>
863
- <span className="jewelry-checkout-address-card__pod">Pay on Delivery available</span>
864
- </div>
865
- <button
866
- type="button"
867
- className="jewelry-checkout-address-card__edit"
868
- onClick={openEditAddress}
869
- aria-label="Edit delivery address"
870
- >
871
- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
872
- <path strokeLinecap="round" strokeLinejoin="round" d="M12 20h9" />
873
- <path
874
- strokeLinecap="round"
875
- strokeLinejoin="round"
876
- d="M16.5 3.5a2.12 2.12 0 013 3L7 19l-4 1 1-4 12.5-12.5z"
877
- />
878
- </svg>
879
- </button>
880
- </div>
978
+ <CheckoutAddressCard
979
+ name={values.fullName || "Delivery address"}
980
+ {...formatAddressParts(values)}
981
+ phone={values.phone}
982
+ addressType={addressTypeFromMetadata(selectedSaved?.metadata)}
983
+ selected
984
+ onEdit={openEditAddress}
985
+ />
881
986
  </article>
882
987
  ) : (
883
988
  <div className="jewelry-checkout-address-empty">
@@ -922,51 +1027,29 @@ export default function AddressFields({
922
1027
  <ul className="jewelry-checkout-modal__list">
923
1028
  {savedAddresses.map((address) => {
924
1029
  const isSelected = selectedAddressId === address.id
925
- const isDefault = !!(address.is_default_shipping || address.is_default_billing)
926
1030
  const label = formatAddressName(address) || "Saved address"
927
- const line = formatAddressLine({
928
- address1: address.address_1,
929
- address2: address.address_2,
930
- city: address.city,
931
- province: address.province,
932
- postalCode: address.postal_code,
933
- countryCode: address.country_code,
934
- })
1031
+ const parts = formatAddressParts({
1032
+ address1: address.address_1,
1033
+ address2: address.address_2,
1034
+ city: address.city,
1035
+ province: address.province,
1036
+ postalCode: address.postal_code,
1037
+ countryCode: address.country_code,
1038
+ })
935
1039
 
936
1040
  return (
937
1041
  <li key={address.id}>
938
- <button
939
- type="button"
940
- onClick={() => handlePickSavedAddress(address)}
941
- disabled={isSelectingAddress}
942
- aria-pressed={isSelected}
943
- className={[
944
- "jewelry-checkout-modal__option",
945
- isSelected ? "jewelry-checkout-modal__option--selected" : "",
946
- ].join(" ")}
947
- >
948
- <span
949
- className={[
950
- "jewelry-checkout-address-card__radio",
951
- isSelected ? "" : "jewelry-checkout-address-card__radio--empty",
952
- ].join(" ")}
953
- aria-hidden
954
- >
955
- <span />
956
- </span>
957
- <span className="jewelry-checkout-modal__option-body">
958
- <span className="jewelry-checkout-modal__option-name">
959
- {label}
960
- <span className="jewelry-checkout-address-card__tag">Home</span>
961
- {isDefault ? (
962
- <span className="jewelry-checkout-address-card__tag jewelry-checkout-address-card__tag--default">
963
- Default
964
- </span>
965
- ) : null}
966
- </span>
967
- <span className="jewelry-checkout-modal__option-line">{line}</span>
968
- </span>
969
- </button>
1042
+ <CheckoutAddressCard
1043
+ name={label}
1044
+ street={parts.street}
1045
+ cityLine={parts.cityLine}
1046
+ phone={address.phone}
1047
+ addressType={addressTypeFromMetadata(address.metadata)}
1048
+ selected={isSelected}
1049
+ onSelect={
1050
+ isSelectingAddress ? undefined : () => handlePickSavedAddress(address)
1051
+ }
1052
+ />
970
1053
  </li>
971
1054
  )
972
1055
  })}
@@ -1050,6 +1133,7 @@ export default function AddressFields({
1050
1133
  full
1051
1134
  />
1052
1135
 
1136
+ <AddressTypePicker value={addressType} onChange={setAddressType} />
1053
1137
  <label className="jewelry-checkout-modal__check">
1054
1138
  <input
1055
1139
  type="checkbox"
@@ -1124,6 +1208,34 @@ function CheckoutAddressModal({
1124
1208
  )
1125
1209
  }
1126
1210
 
1211
+ function AddressTypePicker({
1212
+ value,
1213
+ onChange,
1214
+ }: {
1215
+ value: "HOME" | "OFFICE"
1216
+ onChange: (value: "HOME" | "OFFICE") => void
1217
+ }) {
1218
+ return (
1219
+ <fieldset className="jewelry-checkout-modal__type">
1220
+ <legend className="jewelry-checkout-modal__label">Address type</legend>
1221
+ <div className="jewelry-checkout-modal__type-options">
1222
+ {(["HOME", "OFFICE"] as const).map((type) => (
1223
+ <label key={type} className="jewelry-checkout-modal__type-option">
1224
+ <input
1225
+ type="radio"
1226
+ name="checkout_address_type"
1227
+ value={type}
1228
+ checked={value === type}
1229
+ onChange={() => onChange(type)}
1230
+ />
1231
+ <span>{type === "HOME" ? "Home" : "Office"}</span>
1232
+ </label>
1233
+ ))}
1234
+ </div>
1235
+ </fieldset>
1236
+ )
1237
+ }
1238
+
1127
1239
  function ModalField({
1128
1240
  label,
1129
1241
  id,
@@ -168,12 +168,14 @@
168
168
 
169
169
  .jewelry-checkout-address__btn--outline {
170
170
  background: #ffffff;
171
- border: 1px solid #3b1f4e;
172
- color: #3b1f4e;
171
+ border: 1px solid #1a1224;
172
+ color: #1a1224;
173
173
  }
174
174
 
175
175
  .jewelry-checkout-address__btn--outline:hover:not(:disabled) {
176
176
  background: #f7f3fa;
177
+ border-color: #3b1f4e;
178
+ color: #3b1f4e;
177
179
  }
178
180
 
179
181
  .jewelry-checkout-address__btn--block {
@@ -208,10 +210,26 @@
208
210
  display: flex;
209
211
  align-items: flex-start;
210
212
  gap: 0.85rem;
211
- padding: 1.1rem 2.75rem 1.1rem 1.1rem;
212
- border: 1px solid #3b1f4e;
213
- border-radius: 0.5rem;
213
+ padding: 1.15rem 2.85rem 1.15rem 1.1rem;
214
+ border: 1px solid #e4dcd3;
215
+ border-radius: 0.55rem;
214
216
  background: #ffffff;
217
+ overflow: hidden;
218
+ }
219
+
220
+ .jewelry-checkout-address-card__body--selected {
221
+ background: #faf6f1;
222
+ border-color: #e4dcd3;
223
+ box-shadow: inset 4px 0 0 #3b1f4e;
224
+ }
225
+
226
+ .jewelry-checkout-address-card__body--pickable {
227
+ width: 100%;
228
+ cursor: pointer;
229
+ }
230
+
231
+ .jewelry-checkout-address-card__body--pickable:hover {
232
+ border-color: #3b1f4e;
215
233
  }
216
234
 
217
235
  .jewelry-checkout-address-card__radio {
@@ -220,23 +238,24 @@
220
238
  justify-content: center;
221
239
  width: 1.125rem;
222
240
  height: 1.125rem;
223
- margin-top: 0.15rem;
241
+ margin-top: 0.2rem;
224
242
  flex-shrink: 0;
225
243
  border: 1.5px solid #3b1f4e;
226
244
  border-radius: 999px;
227
- background: #ffffff;
245
+ background: #3b1f4e;
228
246
  box-sizing: border-box;
229
247
  }
230
248
 
231
249
  .jewelry-checkout-address-card__radio span {
232
- width: 0.55rem;
233
- height: 0.55rem;
250
+ width: 0.35rem;
251
+ height: 0.35rem;
234
252
  border-radius: 999px;
235
- background: #3b1f4e;
253
+ background: #faf6f1;
236
254
  }
237
255
 
238
256
  .jewelry-checkout-address-card__radio--empty {
239
- border-color: #c9c1d1;
257
+ background: #ffffff;
258
+ border-color: #c4b9c9;
240
259
  }
241
260
 
242
261
  .jewelry-checkout-address-card__radio--empty span {
@@ -246,6 +265,9 @@
246
265
  .jewelry-checkout-address-card__content {
247
266
  min-width: 0;
248
267
  flex: 1;
268
+ display: flex;
269
+ flex-direction: column;
270
+ align-items: flex-start;
249
271
  }
250
272
 
251
273
  .jewelry-checkout-address-card__name-row {
@@ -257,18 +279,20 @@
257
279
 
258
280
  .jewelry-checkout-address-card__name {
259
281
  margin: 0;
282
+ display: inline;
260
283
  font-family: var(--font-sans, inherit);
261
284
  font-size: 0.9375rem;
262
285
  font-weight: 700;
263
- color: #1a1224;
286
+ color: #3b1f4e;
264
287
  }
265
288
 
266
289
  .jewelry-checkout-address-card__tag {
267
290
  display: inline-flex;
268
- padding: 0.12rem 0.4rem;
269
- border-radius: 0.2rem;
270
- background: #f4eff8;
271
- color: #3b1f4e;
291
+ padding: 0.1rem 0.45rem;
292
+ border-radius: 0.22rem;
293
+ border: 1px solid #cfc9d4;
294
+ background: transparent;
295
+ color: #6b6573;
272
296
  font-size: 0.5625rem;
273
297
  font-weight: 700;
274
298
  letter-spacing: 0.08em;
@@ -277,26 +301,49 @@
277
301
 
278
302
  .jewelry-checkout-address-card__tag--default {
279
303
  background: #3b1f4e;
304
+ border-color: #3b1f4e;
280
305
  color: #ffffff;
281
306
  }
282
307
 
283
308
  .jewelry-checkout-address-card__line {
284
- margin: 0.4rem 0 0;
309
+ display: block;
310
+ margin: 0.35rem 0 0;
285
311
  font-size: 0.8125rem;
286
- line-height: 1.55;
287
- color: #5a5463;
312
+ line-height: 1.5;
313
+ color: #4a4453;
314
+ }
315
+
316
+ .jewelry-checkout-address-card__line--city {
317
+ margin-top: 0.08rem;
318
+ }
319
+
320
+ .jewelry-checkout-address-card__mobile {
321
+ display: block;
322
+ margin: 0.45rem 0 0;
323
+ font-size: 0.8125rem;
324
+ font-weight: 600;
325
+ color: #1a1224;
326
+ }
327
+
328
+ .jewelry-checkout-address-card__mobile span {
329
+ margin-right: 0.4rem;
330
+ font-size: 0.6875rem;
331
+ font-weight: 600;
332
+ letter-spacing: 0.08em;
333
+ text-transform: uppercase;
334
+ color: #8a8490;
288
335
  }
289
336
 
290
337
  .jewelry-checkout-address-card__pod {
291
338
  display: inline-flex;
292
339
  margin-top: 0.7rem;
293
- padding: 0.2rem 0.5rem;
294
- border-radius: 0.2rem;
295
- background: #eef8f0;
296
- color: #1f7a3a;
297
- font-size: 0.625rem;
298
- font-weight: 700;
299
- letter-spacing: 0.04em;
340
+ padding: 0.22rem 0.65rem;
341
+ border-radius: 999px;
342
+ background: #e8f6ec;
343
+ color: #1b7a3a;
344
+ font-size: 0.6875rem;
345
+ font-weight: 600;
346
+ letter-spacing: 0.01em;
300
347
  }
301
348
 
302
349
  .jewelry-checkout-address-card__edit {
@@ -306,17 +353,20 @@
306
353
  display: inline-flex;
307
354
  align-items: center;
308
355
  justify-content: center;
309
- width: 1.75rem;
310
- height: 1.75rem;
311
- border: 1px solid #eadff3;
312
- border-radius: 0.3rem;
313
- background: #f7f3fa;
314
- color: #3b1f4e;
356
+ width: 2rem;
357
+ height: 2rem;
358
+ border: 1px solid #e4dcd3;
359
+ border-radius: 0.4rem;
360
+ background: #ffffff;
361
+ color: #5a5463;
315
362
  cursor: pointer;
363
+ transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
316
364
  }
317
365
 
318
366
  .jewelry-checkout-address-card__edit:hover {
319
- background: #efe7f6;
367
+ background: #f7f3fa;
368
+ border-color: #3b1f4e;
369
+ color: #3b1f4e;
320
370
  }
321
371
 
322
372
  .jewelry-checkout-address-empty {
@@ -429,23 +479,17 @@
429
479
  }
430
480
 
431
481
  .jewelry-checkout-modal__option {
432
- display: flex;
433
- align-items: flex-start;
434
- gap: 0.85rem;
482
+ display: block;
435
483
  width: 100%;
436
- padding: 1rem 1.1rem;
484
+ padding: 0;
437
485
  text-align: left;
438
- border: 1px solid #e6e0ea;
439
- border-radius: 0.75rem;
440
- background: #ffffff;
486
+ border: 0;
487
+ background: transparent;
441
488
  cursor: pointer;
442
- transition: border-color 0.2s ease, background 0.2s ease;
443
489
  }
444
490
 
445
- .jewelry-checkout-modal__option:hover:not(:disabled),
446
- .jewelry-checkout-modal__option--selected {
491
+ .jewelry-checkout-modal__option:hover:not(:disabled) .jewelry-checkout-address-card__body {
447
492
  border-color: #3b1f4e;
448
- background: #faf8fc;
449
493
  }
450
494
 
451
495
  .jewelry-checkout-modal__option:disabled {
@@ -512,7 +556,8 @@
512
556
  .jewelry-checkout-modal__field--full,
513
557
  .jewelry-checkout-modal__check,
514
558
  .jewelry-checkout-modal__error,
515
- .jewelry-checkout-modal__actions {
559
+ .jewelry-checkout-modal__actions,
560
+ .jewelry-checkout-modal__type {
516
561
  grid-column: 1 / -1;
517
562
  }
518
563
 
@@ -556,6 +601,55 @@
556
601
  accent-color: #3b1f4e;
557
602
  }
558
603
 
604
+ .jewelry-checkout-modal__type {
605
+ margin: 0;
606
+ padding: 0;
607
+ border: 0;
608
+ }
609
+
610
+ .jewelry-checkout-modal__type-options {
611
+ display: flex;
612
+ flex-wrap: wrap;
613
+ gap: 0.5rem;
614
+ }
615
+
616
+ .jewelry-checkout-modal__type-option {
617
+ position: relative;
618
+ cursor: pointer;
619
+ }
620
+
621
+ .jewelry-checkout-modal__type-option input {
622
+ position: absolute;
623
+ opacity: 0;
624
+ pointer-events: none;
625
+ }
626
+
627
+ .jewelry-checkout-modal__type-option span {
628
+ display: inline-flex;
629
+ align-items: center;
630
+ justify-content: center;
631
+ min-height: 2.4rem;
632
+ padding: 0 1.15rem;
633
+ border-radius: 999px;
634
+ border: 1px solid #e6e0ea;
635
+ background: #ffffff;
636
+ color: #1a1224;
637
+ font-size: 0.75rem;
638
+ font-weight: 700;
639
+ letter-spacing: 0.08em;
640
+ text-transform: uppercase;
641
+ }
642
+
643
+ .jewelry-checkout-modal__type-option input:focus-visible + span {
644
+ box-shadow: 0 0 0 3px rgba(59, 31, 78, 0.16);
645
+ }
646
+
647
+ .jewelry-checkout-modal__type-option input:checked + span {
648
+ background: #3b1f4e;
649
+ border-color: #3b1f4e;
650
+ color: #ffffff;
651
+ }
652
+
559
653
  .jewelry-checkout-modal__grid {
560
654
  display: contents;
561
655
  }
@@ -630,3 +724,140 @@
630
724
  flex: 1;
631
725
  }
632
726
  }
727
+
728
+ .jewelry-leave-checkout {
729
+ position: fixed;
730
+ inset: 0;
731
+ z-index: 100000;
732
+ display: flex;
733
+ align-items: center;
734
+ justify-content: center;
735
+ padding: 1.25rem;
736
+ }
737
+
738
+ .jewelry-leave-checkout__backdrop {
739
+ position: absolute;
740
+ inset: 0;
741
+ border: 0;
742
+ background: rgba(18, 12, 28, 0.46);
743
+ backdrop-filter: blur(8px);
744
+ -webkit-backdrop-filter: blur(8px);
745
+ cursor: pointer;
746
+ }
747
+
748
+ .jewelry-leave-checkout__dialog {
749
+ position: relative;
750
+ width: min(26.5rem, 100%);
751
+ overflow: hidden;
752
+ border-radius: 1.35rem;
753
+ background: #ffffff;
754
+ box-shadow: 0 28px 64px rgba(26, 18, 36, 0.24);
755
+ animation: jewelry-leave-checkout-in 0.22s ease;
756
+ }
757
+
758
+ @keyframes jewelry-leave-checkout-in {
759
+ from {
760
+ opacity: 0;
761
+ transform: translateY(10px) scale(0.98);
762
+ }
763
+ to {
764
+ opacity: 1;
765
+ transform: translateY(0) scale(1);
766
+ }
767
+ }
768
+
769
+ .jewelry-leave-checkout__hero {
770
+ padding: 2.1rem 1.75rem 1.25rem;
771
+ text-align: center;
772
+ background: #f6f1ea;
773
+ }
774
+
775
+ .jewelry-leave-checkout__icon {
776
+ display: grid;
777
+ place-items: center;
778
+ width: 3.35rem;
779
+ height: 3.35rem;
780
+ margin: 0 auto 1rem;
781
+ border-radius: 999px;
782
+ background: #ffffff;
783
+ border: 1px solid rgba(59, 31, 78, 0.14);
784
+ color: #3b1f4e;
785
+ box-shadow: 0 6px 16px rgba(59, 31, 78, 0.08);
786
+ }
787
+
788
+ .jewelry-leave-checkout__title {
789
+ margin: 0;
790
+ font-family: var(--font-heading, Georgia, serif);
791
+ font-size: 1.4rem;
792
+ font-weight: 500;
793
+ letter-spacing: 0.1em;
794
+ text-transform: uppercase;
795
+ color: #3b1f4e;
796
+ }
797
+
798
+ .jewelry-leave-checkout__body {
799
+ padding: 1.35rem 1.75rem 1.75rem;
800
+ text-align: center;
801
+ background: #ffffff;
802
+ }
803
+
804
+ .jewelry-leave-checkout__copy {
805
+ margin: 0 0 1.5rem;
806
+ font-size: 0.9375rem;
807
+ line-height: 1.65;
808
+ color: #6b6573;
809
+ }
810
+
811
+ .jewelry-leave-checkout__actions {
812
+ display: grid;
813
+ grid-template-columns: 1fr 1fr;
814
+ gap: 0.75rem;
815
+ }
816
+
817
+ .jewelry-leave-checkout__btn {
818
+ display: inline-flex;
819
+ align-items: center;
820
+ justify-content: center;
821
+ min-height: 2.85rem;
822
+ padding: 0.7rem 0.85rem;
823
+ border-radius: 0.7rem;
824
+ font-family: var(--font-sans, inherit);
825
+ font-size: 0.6875rem;
826
+ font-weight: 700;
827
+ letter-spacing: 0.08em;
828
+ text-transform: uppercase;
829
+ cursor: pointer;
830
+ transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
831
+ }
832
+
833
+ .jewelry-leave-checkout__btn--stay {
834
+ background: #ffffff;
835
+ border: 1px solid #3b1f4e;
836
+ color: #3b1f4e;
837
+ }
838
+
839
+ .jewelry-leave-checkout__btn--stay:hover {
840
+ background: #f7f3fa;
841
+ }
842
+
843
+ .jewelry-leave-checkout__btn--leave {
844
+ background: #3b1f4e;
845
+ border: 1px solid #3b1f4e;
846
+ color: #ffffff;
847
+ }
848
+
849
+ .jewelry-leave-checkout__btn--leave:hover {
850
+ background: #55306c;
851
+ border-color: #55306c;
852
+ }
853
+
854
+ .jewelry-leave-checkout__btn:focus-visible {
855
+ outline: 2px solid #3b1f4e;
856
+ outline-offset: 2px;
857
+ }
858
+
859
+ @media (max-width: 480px) {
860
+ .jewelry-leave-checkout__actions {
861
+ grid-template-columns: 1fr;
862
+ }
863
+ }
@@ -0,0 +1,198 @@
1
+ "use client"
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react"
4
+ import { createPortal } from "react-dom"
5
+ import { useParams, useRouter } from "next/navigation"
6
+
7
+ const DEFAULT_REGION = "in"
8
+
9
+ function isCheckoutPath(pathname: string): boolean {
10
+ return /\/checkout(\/|$)/.test(pathname)
11
+ }
12
+
13
+ function resolveHref(href: string): URL | null {
14
+ try {
15
+ return new URL(href, window.location.origin)
16
+ } catch {
17
+ return null
18
+ }
19
+ }
20
+
21
+ export default function LeaveCheckoutGuard() {
22
+ const router = useRouter()
23
+ const params = useParams()
24
+ const countryCode = String(params?.countryCode || DEFAULT_REGION).toLowerCase()
25
+ const [open, setOpen] = useState(false)
26
+ const [mounted, setMounted] = useState(false)
27
+ const pendingHrefRef = useRef<string | null>(null)
28
+ const allowLeaveRef = useRef(false)
29
+ const openRef = useRef(false)
30
+ const stayButtonRef = useRef<HTMLButtonElement>(null)
31
+ openRef.current = open
32
+
33
+ const askToLeave = useCallback((href?: string | null) => {
34
+ pendingHrefRef.current = href ?? null
35
+ setOpen(true)
36
+ }, [])
37
+
38
+ const stay = useCallback(() => {
39
+ pendingHrefRef.current = null
40
+ setOpen(false)
41
+ }, [])
42
+
43
+ const leave = useCallback(() => {
44
+ allowLeaveRef.current = true
45
+ const href = pendingHrefRef.current
46
+ pendingHrefRef.current = null
47
+ setOpen(false)
48
+ router.push(href || `/${countryCode}/cart`)
49
+ }, [countryCode, router])
50
+
51
+ useEffect(() => {
52
+ setMounted(true)
53
+ }, [])
54
+
55
+ useEffect(() => {
56
+ if (!window.history.state?.jewelryCheckoutGuard) {
57
+ window.history.pushState({ jewelryCheckoutGuard: true }, "", window.location.href)
58
+ }
59
+
60
+ const onPopState = () => {
61
+ if (allowLeaveRef.current) return
62
+ window.history.pushState({ jewelryCheckoutGuard: true }, "", window.location.href)
63
+ askToLeave(null)
64
+ }
65
+
66
+ const onClick = (event: MouseEvent) => {
67
+ if (allowLeaveRef.current || openRef.current) return
68
+ if (event.defaultPrevented || event.button !== 0) return
69
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
70
+
71
+ const target = event.target as HTMLElement | null
72
+ if (!target) return
73
+
74
+ const anchor = target.closest("a[href]") as HTMLAnchorElement | null
75
+ if (!anchor) return
76
+ if (anchor.target === "_blank" || anchor.hasAttribute("download")) return
77
+
78
+ const href = anchor.getAttribute("href")
79
+ if (!href || href.startsWith("#") || href.startsWith("javascript:")) return
80
+
81
+ const url = resolveHref(href)
82
+ if (!url || url.origin !== window.location.origin) return
83
+ if (isCheckoutPath(url.pathname)) return
84
+
85
+ event.preventDefault()
86
+ event.stopPropagation()
87
+ askToLeave(`${url.pathname}${url.search}${url.hash}`)
88
+ }
89
+
90
+ const onSubmit = (event: SubmitEvent) => {
91
+ if (allowLeaveRef.current || openRef.current) return
92
+ const form = event.target as HTMLFormElement | null
93
+ if (!form) return
94
+ if (form.id === "checkout-address-form") return
95
+ if (form.getAttribute("role") !== "search" && !form.classList.contains("jewelry-nav__search")) {
96
+ return
97
+ }
98
+
99
+ event.preventDefault()
100
+ event.stopPropagation()
101
+ event.stopImmediatePropagation()
102
+
103
+ const input = form.querySelector("input[type='search'], input[name='q']") as HTMLInputElement | null
104
+ const query = input?.value.trim() || ""
105
+ const base = `/${countryCode}/store`
106
+ askToLeave(query ? `${base}?q=${encodeURIComponent(query)}` : base)
107
+ }
108
+
109
+ window.addEventListener("popstate", onPopState)
110
+ document.addEventListener("click", onClick, true)
111
+ document.addEventListener("submit", onSubmit, true)
112
+
113
+ return () => {
114
+ window.removeEventListener("popstate", onPopState)
115
+ document.removeEventListener("click", onClick, true)
116
+ document.removeEventListener("submit", onSubmit, true)
117
+ }
118
+ }, [askToLeave, countryCode])
119
+
120
+ useEffect(() => {
121
+ if (!open) return
122
+ const previous = document.body.style.overflow
123
+ document.body.style.overflow = "hidden"
124
+ stayButtonRef.current?.focus()
125
+
126
+ const onKey = (event: KeyboardEvent) => {
127
+ if (event.key === "Escape") stay()
128
+ }
129
+
130
+ window.addEventListener("keydown", onKey)
131
+ return () => {
132
+ document.body.style.overflow = previous
133
+ window.removeEventListener("keydown", onKey)
134
+ }
135
+ }, [open, stay])
136
+
137
+ if (!mounted || !open) return null
138
+
139
+ return createPortal(
140
+ <div className="jewelry-leave-checkout" role="presentation">
141
+ <button
142
+ type="button"
143
+ className="jewelry-leave-checkout__backdrop"
144
+ aria-label="Stay in checkout"
145
+ onClick={stay}
146
+ />
147
+ <div
148
+ className="jewelry-leave-checkout__dialog"
149
+ role="alertdialog"
150
+ aria-modal="true"
151
+ aria-labelledby="jewelry-leave-checkout-title"
152
+ aria-describedby="jewelry-leave-checkout-copy"
153
+ >
154
+ <div className="jewelry-leave-checkout__hero">
155
+ <span className="jewelry-leave-checkout__icon" aria-hidden>
156
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none">
157
+ <path
158
+ d="M12 3.6L21.4 20.2H2.6L12 3.6Z"
159
+ stroke="currentColor"
160
+ strokeWidth="1.7"
161
+ strokeLinejoin="round"
162
+ />
163
+ <path d="M12 9.4v5.1" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" />
164
+ <circle cx="12" cy="16.9" r="0.85" fill="currentColor" />
165
+ </svg>
166
+ </span>
167
+ <h2 id="jewelry-leave-checkout-title" className="jewelry-leave-checkout__title">
168
+ Leave checkout?
169
+ </h2>
170
+ </div>
171
+ <div className="jewelry-leave-checkout__body">
172
+ <p id="jewelry-leave-checkout-copy" className="jewelry-leave-checkout__copy">
173
+ Your order details and progress will be lost if you leave now. Are you sure you want to
174
+ go back?
175
+ </p>
176
+ <div className="jewelry-leave-checkout__actions">
177
+ <button
178
+ ref={stayButtonRef}
179
+ type="button"
180
+ className="jewelry-leave-checkout__btn jewelry-leave-checkout__btn--stay"
181
+ onClick={stay}
182
+ >
183
+ Stay in checkout
184
+ </button>
185
+ <button
186
+ type="button"
187
+ className="jewelry-leave-checkout__btn jewelry-leave-checkout__btn--leave"
188
+ onClick={leave}
189
+ >
190
+ Yes, leave
191
+ </button>
192
+ </div>
193
+ </div>
194
+ </div>
195
+ </div>,
196
+ document.body
197
+ )
198
+ }
package/src/segment.tsx CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type { HttpTypes } from "@medusajs/types"
4
4
  import AddressFields from "./address-fields"
5
+ import LeaveCheckoutGuard from "./leave-checkout-guard"
5
6
  import "./jewelry-checkout-form.css"
6
7
 
7
8
  export default function JewelryCheckoutForm({
@@ -18,17 +19,23 @@ export default function JewelryCheckoutForm({
18
19
  }) {
19
20
  if (!cart?.items?.length) {
20
21
  return (
21
- <div className="card-surface rounded-lg p-6">
22
- <p className="text-muted text-sm">Your cart is empty.</p>
23
- </div>
22
+ <>
23
+ <LeaveCheckoutGuard />
24
+ <div className="card-surface rounded-lg p-6">
25
+ <p className="text-muted text-sm">Your cart is empty.</p>
26
+ </div>
27
+ </>
24
28
  )
25
29
  }
26
30
 
27
31
  return (
28
- <div className="jewelry-checkout-form checkout-page__address-panel">
29
- <form id="checkout-address-form" autoComplete="off">
30
- <AddressFields cart={cart} customer={customer} formTitle={formTitle} />
31
- </form>
32
- </div>
32
+ <>
33
+ <LeaveCheckoutGuard />
34
+ <div className="jewelry-checkout-form checkout-page__address-panel">
35
+ <form id="checkout-address-form" autoComplete="off">
36
+ <AddressFields cart={cart} customer={customer} formTitle={formTitle} />
37
+ </form>
38
+ </div>
39
+ </>
33
40
  )
34
41
  }