@merchi/cart 1.4.5 → 1.4.6

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.
@@ -33,6 +33,7 @@ import { makeProduct } from './utilities/product';
33
33
  import { getSavedCheckoutCustomer, saveCheckoutAddress } from './utilities/local_storage';
34
34
  import { makeUser } from './utilities/user';
35
35
  import { persistTestCheckoutFlag, readTestCheckoutFlag } from './utilities/test_checkout';
36
+ import { appendJobSourceToFormData, captureAndReadJobSource, cartNeedsSourceUpdate, jobSourceFieldsForApi } from './utilities/job_source';
36
37
  import { jsx as _jsx } from "react/jsx-runtime";
37
38
  var CartContext = /*#__PURE__*/createContext({
38
39
  cart: {},
@@ -498,65 +499,56 @@ var CartProvider = function CartProvider(_ref) {
498
499
  }
499
500
  function _createCartAndCookie() {
500
501
  _createCartAndCookie = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2(callback) {
501
- var isTest, _cart, created, cartJson, _t3;
502
+ var isTest, jobSource, _cart, query, created, cartJson, _t3;
502
503
  return _regenerator().w(function (_context2) {
503
504
  while (1) switch (_context2.p = _context2.n) {
504
505
  case 0:
505
506
  setFetchingCart(true);
506
507
  _context2.p = 1;
507
508
  isTest = readTestCheckoutFlag();
509
+ jobSource = jobSourceFieldsForApi(captureAndReadJobSource());
508
510
  _cart = makeCart({
509
511
  domain: {
510
512
  id: domainId
511
513
  },
512
514
  isTest: isTest
513
515
  }, true);
514
- if (!isTest) {
515
- _context2.n = 3;
516
- break;
517
- }
516
+ query = [['embed', JSON.stringify(cartEmbed)], ['skip_rights', 'y']];
517
+ if (isTest) query.push(['is_test', 'true']);
518
518
  _context2.n = 2;
519
519
  return merchi.authenticatedFetch('/carts/', {
520
520
  method: 'POST',
521
- body: _cart.toFormData(),
522
- query: [['embed', JSON.stringify(cartEmbed)], ['skip_rights', 'y'], ['is_test', 'true']]
521
+ body: appendJobSourceToFormData(_cart.toFormData(), jobSource),
522
+ query: query
523
523
  });
524
524
  case 2:
525
525
  created = _context2.v;
526
526
  _cart.fromJson(created.cart);
527
- _context2.n = 4;
528
- break;
529
- case 3:
530
- _context2.n = 4;
531
- return _cart.create({
532
- embed: cartEmbed
533
- });
534
- case 4:
535
- _context2.n = 5;
527
+ _context2.n = 3;
536
528
  return _cart.toJson();
537
- case 5:
529
+ case 3:
538
530
  cartJson = _context2.v;
539
531
  if (domainId) setCartCookie(Number(domainId), cartJson, undefined, isTest);
540
- _context2.n = 6;
532
+ _context2.n = 4;
541
533
  return attachSavedClientIfNeeded(cartJson);
542
- case 6:
534
+ case 4:
543
535
  cartJson = _context2.v;
544
536
  setCart(_objectSpread({}, cartJson));
545
537
  if (callback) callback();
546
- _context2.n = 8;
538
+ _context2.n = 6;
547
539
  break;
548
- case 7:
549
- _context2.p = 7;
540
+ case 5:
541
+ _context2.p = 5;
550
542
  _t3 = _context2.v;
551
543
  alertError(_t3.errorMessage || _t3.message || 'Unable to fetch cart.');
552
- case 8:
553
- _context2.p = 8;
544
+ case 6:
545
+ _context2.p = 6;
554
546
  setFetchingCart(false);
555
- return _context2.f(8);
556
- case 9:
547
+ return _context2.f(6);
548
+ case 7:
557
549
  return _context2.a(2);
558
550
  }
559
- }, _callee2, null, [[1, 7, 8, 9]]);
551
+ }, _callee2, null, [[1, 5, 6, 7]]);
560
552
  }));
561
553
  return _createCartAndCookie.apply(this, arguments);
562
554
  }
@@ -565,7 +557,7 @@ var CartProvider = function CartProvider(_ref) {
565
557
  }
566
558
  function _getCart() {
567
559
  _getCart = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3(cartIdAndToken) {
568
- var id, _cart2, cartJson, isTest, _t4;
560
+ var id, _cart2, cartJson, isTest, jobSource, saved, _t4;
569
561
  return _regenerator().w(function (_context3) {
570
562
  while (1) switch (_context3.p = _context3.n) {
571
563
  case 0:
@@ -593,36 +585,52 @@ var CartProvider = function CartProvider(_ref) {
593
585
  case 4:
594
586
  return _context3.a(2);
595
587
  case 5:
596
- if (!(stripeIsValidAndActive(cartJson) || isTest)) {
588
+ jobSource = jobSourceFieldsForApi(captureAndReadJobSource());
589
+ if (!cartNeedsSourceUpdate(cartJson, jobSource)) {
597
590
  _context3.n = 7;
598
591
  break;
599
592
  }
600
593
  _context3.n = 6;
601
- return attachSavedClientIfNeeded(cartJson);
594
+ return merchi.authenticatedFetch("/carts/".concat(_cart2.id, "/"), {
595
+ method: 'PATCH',
596
+ body: appendJobSourceToFormData(_cart2.toFormData(), jobSource),
597
+ query: [['embed', JSON.stringify(cartEmbed)], ['skip_rights', 'y']]
598
+ });
602
599
  case 6:
600
+ saved = _context3.v;
601
+ _cart2.fromJson(saved.cart);
602
+ cartJson = _cart2.toJson();
603
+ case 7:
604
+ if (!(stripeIsValidAndActive(cartJson) || isTest)) {
605
+ _context3.n = 9;
606
+ break;
607
+ }
608
+ _context3.n = 8;
609
+ return attachSavedClientIfNeeded(cartJson);
610
+ case 8:
603
611
  cartJson = _context3.v;
604
612
  setCart(_objectSpread({}, cartJson));
605
- _context3.n = 8;
613
+ _context3.n = 10;
606
614
  break;
607
- case 7:
615
+ case 9:
608
616
  console.error("MErhci cart error: Stripe payment " + "options have not been correctly set up. " + "Check the company profile payment options tab " + "to set and edit stripe payment options.");
609
617
  setCartSettingsInvalid(true);
610
- case 8:
611
- _context3.n = 10;
618
+ case 10:
619
+ _context3.n = 12;
612
620
  break;
613
- case 9:
614
- _context3.p = 9;
621
+ case 11:
622
+ _context3.p = 11;
615
623
  _t4 = _context3.v;
616
- _context3.n = 10;
624
+ _context3.n = 12;
617
625
  return createCartAndCookie();
618
- case 10:
619
- _context3.p = 10;
626
+ case 12:
627
+ _context3.p = 12;
620
628
  setFetchingCart(false);
621
- return _context3.f(10);
622
- case 11:
629
+ return _context3.f(12);
630
+ case 13:
623
631
  return _context3.a(2);
624
632
  }
625
- }, _callee3, null, [[1, 9, 10, 11]]);
633
+ }, _callee3, null, [[1, 11, 12, 13]]);
626
634
  }));
627
635
  return _getCart.apply(this, arguments);
628
636
  }
@@ -18,10 +18,10 @@ import { useForm } from "react-hook-form";
18
18
  import InputsAddress from "./InputsAddress";
19
19
  import { CheckoutContainer, InnerContainer } from "../components/containers";
20
20
  import { Title } from "../components";
21
- import { shipmentFormId } from "../utilities/shipment";
21
+ import { checkoutShipmentHydrationAction, shipmentFormId } from "../utilities/shipment";
22
22
  import { faMapMarkerAlt } from "@fortawesome/free-solid-svg-icons";
23
23
  import { useCartContext } from "../CartProvider";
24
- import { useState } from "react";
24
+ import { useEffect, useRef, useState } from "react";
25
25
  import { getCartCookieToken } from "../utilities/cookie";
26
26
  import { makeAddress, sanitizeAddressFields } from "../utilities/address";
27
27
  import { makeCart } from "../utilities/cart";
@@ -121,7 +121,9 @@ export function ActiveFormShipmentAddressAndNotes() {
121
121
  cart = _useCartContext2.cart,
122
122
  setCart = _useCartContext2.setCart,
123
123
  setLoadingTotals = _useCartContext2.setLoadingTotals,
124
- setActiveTabAndEditDisabled = _useCartContext2.setActiveTabAndEditDisabled;
124
+ setActiveTabAndEditDisabled = _useCartContext2.setActiveTabAndEditDisabled,
125
+ updateCartShipmentAddress = _useCartContext2.updateCartShipmentAddress,
126
+ getCartShipmentOptions = _useCartContext2.getCartShipmentOptions;
125
127
  var saved = getSavedCheckoutAddress(domainId);
126
128
  var cartAddress = cart === null || cart === void 0 ? void 0 : cart.receiverAddress;
127
129
  var receiverAddress = addressHasContent(cartAddress) ? cartAddress : saved.address || {};
@@ -130,6 +132,25 @@ export function ActiveFormShipmentAddressAndNotes() {
130
132
  _useState2 = _slicedToArray(_useState, 2),
131
133
  addressFieldsOpen = _useState2[0],
132
134
  setAddressFieldsOpen = _useState2[1];
135
+ var hydratedShipmentOptionsRef = useRef(false);
136
+ useEffect(function () {
137
+ if (hydratedShipmentOptionsRef.current || !(cart !== null && cart !== void 0 && cart.id)) return;
138
+ var draft = getSavedCheckoutAddress(domainId);
139
+ var action = checkoutShipmentHydrationAction({
140
+ cartAddress: cart === null || cart === void 0 ? void 0 : cart.receiverAddress,
141
+ savedAddress: draft.address,
142
+ shipmentGroups: cart === null || cart === void 0 ? void 0 : cart.shipmentGroups
143
+ });
144
+ if (!action) return;
145
+ hydratedShipmentOptionsRef.current = true;
146
+ if (action === "apply-saved-address" && draft.address) {
147
+ updateCartShipmentAddress(draft.address);
148
+ return;
149
+ }
150
+ if (action === "refresh-quotes") {
151
+ getCartShipmentOptions === null || getCartShipmentOptions === void 0 || getCartShipmentOptions();
152
+ }
153
+ }, [cart === null || cart === void 0 ? void 0 : cart.id, cart === null || cart === void 0 ? void 0 : cart.receiverAddress, cart === null || cart === void 0 ? void 0 : cart.shipmentGroups, domainId, getCartShipmentOptions, updateCartShipmentAddress]);
133
154
  function saveCartShipmentAddressAndGoToNextTab(_x) {
134
155
  return _saveCartShipmentAddressAndGoToNextTab.apply(this, arguments);
135
156
  }
@@ -6,6 +6,7 @@ export * from './currency';
6
6
  export * from './helpers';
7
7
  export * from './invoice';
8
8
  export * from './job';
9
+ export * from './job_source';
9
10
  export * from './local_storage';
10
11
  export * from './product';
11
12
  export * from './shipment';
@@ -0,0 +1,139 @@
1
+ function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
2
+ function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
3
+ function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
4
+ function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
5
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
6
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
7
+ function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
8
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
9
+ function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
10
+ function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; }
11
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
12
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
13
+ function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
14
+ export var JOB_SOURCE_STORAGE_KEY = 'merchi_job_source';
15
+ var CLICK_ID_PARAMS = ['gclid', 'gbraid', 'wbraid', 'fbclid', 'msclkid'];
16
+ var UTM_PARAMS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'];
17
+ function trimTo(value, limit) {
18
+ if (!value) return undefined;
19
+ var text = value.trim().slice(0, limit);
20
+ return text || undefined;
21
+ }
22
+ function readSearchParams() {
23
+ if (typeof window === 'undefined') return new URLSearchParams();
24
+ try {
25
+ return new URLSearchParams(window.location.search);
26
+ } catch (_unused) {
27
+ return new URLSearchParams();
28
+ }
29
+ }
30
+ function readStoredSource() {
31
+ if (typeof window === 'undefined') return null;
32
+ try {
33
+ var raw = window.localStorage.getItem(JOB_SOURCE_STORAGE_KEY);
34
+ if (!raw) return null;
35
+ var parsed = JSON.parse(raw);
36
+ if (!parsed || _typeof(parsed) !== 'object') return null;
37
+ return parsed;
38
+ } catch (_unused2) {
39
+ return null;
40
+ }
41
+ }
42
+ function writeStoredSource(source) {
43
+ if (typeof window === 'undefined') return;
44
+ try {
45
+ window.localStorage.setItem(JOB_SOURCE_STORAGE_KEY, JSON.stringify(source));
46
+ } catch (_unused3) {
47
+ // Ignore quota / private-mode failures.
48
+ }
49
+ }
50
+ function currentLanding() {
51
+ if (typeof window === 'undefined') return undefined;
52
+ try {
53
+ var path = "".concat(window.location.pathname || '/').concat(window.location.search || '');
54
+ return trimTo(path, 512);
55
+ } catch (_unused4) {
56
+ return undefined;
57
+ }
58
+ }
59
+ function currentReferrer() {
60
+ if (typeof document === 'undefined') return undefined;
61
+ try {
62
+ return trimTo(document.referrer, 512);
63
+ } catch (_unused5) {
64
+ return undefined;
65
+ }
66
+ }
67
+ export function sourceFromLocation() {
68
+ var params = readSearchParams();
69
+ var source = {
70
+ utmSource: trimTo(params.get('utm_source'), 256),
71
+ utmMedium: trimTo(params.get('utm_medium'), 256),
72
+ utmCampaign: trimTo(params.get('utm_campaign'), 256),
73
+ utmContent: trimTo(params.get('utm_content'), 256),
74
+ utmTerm: trimTo(params.get('utm_term'), 256),
75
+ sourceLanding: currentLanding(),
76
+ sourceReferrer: currentReferrer()
77
+ };
78
+ var _iterator = _createForOfIteratorHelper(CLICK_ID_PARAMS),
79
+ _step;
80
+ try {
81
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
82
+ var key = _step.value;
83
+ var value = trimTo(params.get(key), 256);
84
+ if (value) {
85
+ source.sourceClickId = value;
86
+ break;
87
+ }
88
+ }
89
+ } catch (err) {
90
+ _iterator.e(err);
91
+ } finally {
92
+ _iterator.f();
93
+ }
94
+ return source;
95
+ }
96
+ export function hasPaidClick(source) {
97
+ return Boolean(source === null || source === void 0 ? void 0 : source.sourceClickId);
98
+ }
99
+ export function hasSourceData(source) {
100
+ if (!source) return false;
101
+ return Boolean(source.utmSource || source.utmMedium || source.utmCampaign || source.utmContent || source.utmTerm || source.sourceClickId || source.sourceLanding || source.sourceReferrer);
102
+ }
103
+ export function captureAndReadJobSource() {
104
+ var incoming = sourceFromLocation();
105
+ var stored = readStoredSource();
106
+ if (hasPaidClick(incoming) || !hasSourceData(stored)) {
107
+ var next = _objectSpread(_objectSpread({}, stored || {}), Object.fromEntries(Object.entries(incoming).filter(function (_ref) {
108
+ var _ref2 = _slicedToArray(_ref, 2),
109
+ value = _ref2[1];
110
+ return Boolean(value);
111
+ })));
112
+ if (hasSourceData(next)) writeStoredSource(next);
113
+ return next;
114
+ }
115
+ return stored || incoming;
116
+ }
117
+ export function jobSourceFieldsForApi(source) {
118
+ var resolved = source || captureAndReadJobSource();
119
+ var out = {};
120
+ Object.keys(resolved).forEach(function (key) {
121
+ var value = resolved[key];
122
+ if (value) out[key] = value;
123
+ });
124
+ return out;
125
+ }
126
+ export function cartNeedsSourceUpdate(cart, source) {
127
+ if (!hasSourceData(source)) return false;
128
+ if (!(cart !== null && cart !== void 0 && cart.utmSource) && !(cart !== null && cart !== void 0 && cart.sourceClickId) && !(cart !== null && cart !== void 0 && cart.sourceChannel)) return true;
129
+ return Boolean(source.sourceClickId && !(cart !== null && cart !== void 0 && cart.sourceClickId));
130
+ }
131
+ export var JOB_SOURCE_URL_KEYS = [].concat(UTM_PARAMS, CLICK_ID_PARAMS);
132
+ export function appendJobSourceToFormData(data, source) {
133
+ var fields = jobSourceFieldsForApi(source);
134
+ Object.keys(fields).forEach(function (key) {
135
+ var value = fields[key];
136
+ if (value) data.append(key, value);
137
+ });
138
+ return data;
139
+ }
@@ -0,0 +1,75 @@
1
+ import { JOB_SOURCE_STORAGE_KEY, captureAndReadJobSource, cartNeedsSourceUpdate, hasPaidClick } from './job_source';
2
+ describe('job source capture', function () {
3
+ var originalLocation = window.location;
4
+ var originalReferrer = document.referrer;
5
+ beforeEach(function () {
6
+ window.localStorage.clear();
7
+ delete window.location;
8
+ window.location = {
9
+ search: '',
10
+ pathname: '/tyvek-wristbands'
11
+ };
12
+ Object.defineProperty(document, 'referrer', {
13
+ configurable: true,
14
+ value: ''
15
+ });
16
+ });
17
+ afterEach(function () {
18
+ window.location = originalLocation;
19
+ Object.defineProperty(document, 'referrer', {
20
+ configurable: true,
21
+ value: originalReferrer
22
+ });
23
+ window.localStorage.clear();
24
+ });
25
+ it('captures google ads click ids and landing path', function () {
26
+ window.location = {
27
+ search: '?utm_source=google&utm_medium=cpc&gclid=EAIa&utm_campaign=bands',
28
+ pathname: '/tyvek-wristbands'
29
+ };
30
+ var source = captureAndReadJobSource();
31
+ expect(source.utmSource).toBe('google');
32
+ expect(source.utmMedium).toBe('cpc');
33
+ expect(source.utmCampaign).toBe('bands');
34
+ expect(source.sourceClickId).toBe('EAIa');
35
+ expect(source.sourceLanding).toBe('/tyvek-wristbands?utm_source=google&utm_medium=cpc&gclid=EAIa&utm_campaign=bands');
36
+ expect(hasPaidClick(source)).toBe(true);
37
+ });
38
+ it('keeps first-touch UTMs unless a paid click arrives later', function () {
39
+ window.location = {
40
+ search: '?utm_source=newsletter&utm_medium=email',
41
+ pathname: '/'
42
+ };
43
+ captureAndReadJobSource();
44
+ window.location = {
45
+ search: '?utm_source=google&utm_medium=cpc',
46
+ pathname: '/product'
47
+ };
48
+ expect(captureAndReadJobSource().utmSource).toBe('newsletter');
49
+ window.location = {
50
+ search: '?gclid=NEWCLICK&utm_source=google',
51
+ pathname: '/product'
52
+ };
53
+ expect(captureAndReadJobSource().sourceClickId).toBe('NEWCLICK');
54
+ expect(window.localStorage.getItem(JOB_SOURCE_STORAGE_KEY)).toContain('NEWCLICK');
55
+ });
56
+ it('knows when an existing cart still needs source fields', function () {
57
+ expect(cartNeedsSourceUpdate({}, {
58
+ utmSource: 'google',
59
+ sourceClickId: 'x'
60
+ })).toBe(true);
61
+ expect(cartNeedsSourceUpdate({
62
+ utmSource: 'google'
63
+ }, {
64
+ utmSource: 'google',
65
+ sourceClickId: 'x'
66
+ })).toBe(true);
67
+ expect(cartNeedsSourceUpdate({
68
+ utmSource: 'google',
69
+ sourceClickId: 'x'
70
+ }, {
71
+ utmSource: 'google',
72
+ sourceClickId: 'x'
73
+ })).toBe(false);
74
+ });
75
+ });
@@ -1,3 +1,4 @@
1
+ import { addressHasContent } from './local_storage';
1
2
  export var shipmentFormId = '_shipment-address-form-id';
2
3
  export function cartItemsNeedShipment(cart) {
3
4
  var _cart$cartItems = cart.cartItems,
@@ -8,4 +9,25 @@ export function cartItemsNeedShipment(cart) {
8
9
  }
9
10
  }
10
11
  return false;
12
+ }
13
+ export function shipmentGroupsHaveQuotes(groups) {
14
+ if (!(groups !== null && groups !== void 0 && groups.length)) return false;
15
+ return groups.some(function (group) {
16
+ return Array.isArray(group === null || group === void 0 ? void 0 : group.quotes) && group.quotes.length > 0;
17
+ });
18
+ }
19
+ /**
20
+ * When checkout hydrates a saved address (localStorage) the form shows it,
21
+ * but quotes are only fetched after the user edits the field. Returning
22
+ * `apply-saved-address` means the cart still needs that address applied so
23
+ * quotes can be generated; `refresh-quotes` means the cart already has an
24
+ * address but no usable shipment options.
25
+ */
26
+ export function checkoutShipmentHydrationAction(options) {
27
+ var hasCartAddress = addressHasContent(options.cartAddress);
28
+ var hasSavedAddress = addressHasContent(options.savedAddress);
29
+ var hasQuotes = shipmentGroupsHaveQuotes(options.shipmentGroups);
30
+ if (!hasCartAddress && hasSavedAddress) return 'apply-saved-address';
31
+ if (hasCartAddress && !hasQuotes) return 'refresh-quotes';
32
+ return null;
11
33
  }
@@ -0,0 +1,60 @@
1
+ import { checkoutShipmentHydrationAction, shipmentGroupsHaveQuotes } from './shipment';
2
+ var savedAddress = {
3
+ lineOne: '32 Bridge St',
4
+ city: 'Brighton',
5
+ state: 'VIC',
6
+ postcode: '3186',
7
+ country: 'AU'
8
+ };
9
+ describe('shipmentGroupsHaveQuotes', function () {
10
+ it('is false when groups are missing or empty', function () {
11
+ expect(shipmentGroupsHaveQuotes(undefined)).toBe(false);
12
+ expect(shipmentGroupsHaveQuotes([])).toBe(false);
13
+ expect(shipmentGroupsHaveQuotes([{
14
+ quotes: []
15
+ }])).toBe(false);
16
+ });
17
+ it('is true when any group has quotes', function () {
18
+ expect(shipmentGroupsHaveQuotes([{
19
+ quotes: []
20
+ }, {
21
+ quotes: [{
22
+ id: 1
23
+ }]
24
+ }])).toBe(true);
25
+ });
26
+ });
27
+ describe('checkoutShipmentHydrationAction', function () {
28
+ it('applies a localStorage address when the cart has none', function () {
29
+ expect(checkoutShipmentHydrationAction({
30
+ cartAddress: {},
31
+ savedAddress: savedAddress,
32
+ shipmentGroups: []
33
+ })).toBe('apply-saved-address');
34
+ });
35
+ it('refreshes quotes when the cart already has an address but no options', function () {
36
+ expect(checkoutShipmentHydrationAction({
37
+ cartAddress: savedAddress,
38
+ savedAddress: savedAddress,
39
+ shipmentGroups: []
40
+ })).toBe('refresh-quotes');
41
+ });
42
+ it('does nothing when quotes are already available', function () {
43
+ expect(checkoutShipmentHydrationAction({
44
+ cartAddress: savedAddress,
45
+ savedAddress: savedAddress,
46
+ shipmentGroups: [{
47
+ quotes: [{
48
+ id: 9
49
+ }]
50
+ }]
51
+ })).toBe(null);
52
+ });
53
+ it('does nothing when neither cart nor localStorage has an address', function () {
54
+ expect(checkoutShipmentHydrationAction({
55
+ cartAddress: {},
56
+ savedAddress: {},
57
+ shipmentGroups: []
58
+ })).toBe(null);
59
+ });
60
+ });