merchi_cart 1.4.5 → 1.4.7

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.
@@ -32,6 +32,7 @@ import { makeProduct } from './utilities/product';
32
32
  import { getSavedCheckoutCustomer, saveCheckoutAddress } from './utilities/local_storage';
33
33
  import { makeUser } from './utilities/user';
34
34
  import { persistTestCheckoutFlag, readTestCheckoutFlag } from './utilities/test_checkout';
35
+ import { appendJobSourceToFormData, captureAndReadJobSource, cartNeedsSourceUpdate, jobSourceFieldsForApi } from './utilities/job_source';
35
36
  import { jsx as _jsx } from "react/jsx-runtime";
36
37
  var CartContext = /*#__PURE__*/createContext({
37
38
  cart: {},
@@ -500,66 +501,57 @@ var CartProvider = function CartProvider(_ref) {
500
501
  }
501
502
  function _createCartAndCookie() {
502
503
  _createCartAndCookie = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee2(callback) {
503
- var isTest, _cart, created, _cartJson;
504
+ var isTest, jobSource, _cart, query, created, _cartJson;
504
505
  return _regeneratorRuntime().wrap(function _callee2$(_context2) {
505
506
  while (1) switch (_context2.prev = _context2.next) {
506
507
  case 0:
507
508
  setFetchingCart(true);
508
509
  _context2.prev = 1;
509
510
  isTest = readTestCheckoutFlag();
511
+ jobSource = jobSourceFieldsForApi(captureAndReadJobSource());
510
512
  _cart = makeCart({
511
513
  domain: {
512
514
  id: domainId
513
515
  },
514
516
  isTest: isTest
515
517
  }, true);
516
- if (!isTest) {
517
- _context2.next = 11;
518
- break;
519
- }
520
- _context2.next = 7;
518
+ query = [['embed', JSON.stringify(cartEmbed)], ['skip_rights', 'y']];
519
+ if (isTest) query.push(['is_test', 'true']);
520
+ _context2.next = 9;
521
521
  return merchi.authenticatedFetch('/carts/', {
522
522
  method: 'POST',
523
- body: _cart.toFormData(),
524
- query: [['embed', JSON.stringify(cartEmbed)], ['skip_rights', 'y'], ['is_test', 'true']]
523
+ body: appendJobSourceToFormData(_cart.toFormData(), jobSource),
524
+ query: query
525
525
  });
526
- case 7:
526
+ case 9:
527
527
  created = _context2.sent;
528
528
  _cart.fromJson(created.cart);
529
529
  _context2.next = 13;
530
- break;
531
- case 11:
532
- _context2.next = 13;
533
- return _cart.create({
534
- embed: cartEmbed
535
- });
536
- case 13:
537
- _context2.next = 15;
538
530
  return _cart.toJson();
539
- case 15:
531
+ case 13:
540
532
  _cartJson = _context2.sent;
541
533
  if (domainId) setCartCookie(Number(domainId), _cartJson, undefined, isTest);
542
- _context2.next = 19;
534
+ _context2.next = 17;
543
535
  return attachSavedClientIfNeeded(_cartJson);
544
- case 19:
536
+ case 17:
545
537
  _cartJson = _context2.sent;
546
538
  setCart(_objectSpread({}, _cartJson));
547
539
  if (callback) callback();
548
- _context2.next = 27;
540
+ _context2.next = 25;
549
541
  break;
550
- case 24:
551
- _context2.prev = 24;
542
+ case 22:
543
+ _context2.prev = 22;
552
544
  _context2.t0 = _context2["catch"](1);
553
545
  alertError(_context2.t0.errorMessage || _context2.t0.message || 'Unable to fetch cart.');
554
- case 27:
555
- _context2.prev = 27;
546
+ case 25:
547
+ _context2.prev = 25;
556
548
  setFetchingCart(false);
557
- return _context2.finish(27);
558
- case 30:
549
+ return _context2.finish(25);
550
+ case 28:
559
551
  case "end":
560
552
  return _context2.stop();
561
553
  }
562
- }, _callee2, null, [[1, 24, 27, 30]]);
554
+ }, _callee2, null, [[1, 22, 25, 28]]);
563
555
  }));
564
556
  return _createCartAndCookie.apply(this, arguments);
565
557
  }
@@ -568,7 +560,7 @@ var CartProvider = function CartProvider(_ref) {
568
560
  }
569
561
  function _getCart() {
570
562
  _getCart = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee3(cartIdAndToken) {
571
- var id, _cart2, _cartJson2, isTest;
563
+ var id, _cart2, _cartJson2, isTest, jobSource, saved;
572
564
  return _regeneratorRuntime().wrap(function _callee3$(_context3) {
573
565
  while (1) switch (_context3.prev = _context3.next) {
574
566
  case 0:
@@ -596,37 +588,53 @@ var CartProvider = function CartProvider(_ref) {
596
588
  case 14:
597
589
  return _context3.abrupt("return");
598
590
  case 15:
599
- if (!(stripeIsValidAndActive(_cartJson2) || isTest)) {
591
+ jobSource = jobSourceFieldsForApi(captureAndReadJobSource());
592
+ if (!cartNeedsSourceUpdate(_cartJson2, jobSource)) {
600
593
  _context3.next = 22;
601
594
  break;
602
595
  }
603
- _context3.next = 18;
596
+ _context3.next = 19;
597
+ return merchi.authenticatedFetch("/carts/".concat(_cart2.id, "/"), {
598
+ method: 'PATCH',
599
+ body: appendJobSourceToFormData(_cart2.toFormData(), jobSource),
600
+ query: [['embed', JSON.stringify(cartEmbed)], ['skip_rights', 'y']]
601
+ });
602
+ case 19:
603
+ saved = _context3.sent;
604
+ _cart2.fromJson(saved.cart);
605
+ _cartJson2 = _cart2.toJson();
606
+ case 22:
607
+ if (!(stripeIsValidAndActive(_cartJson2) || isTest)) {
608
+ _context3.next = 29;
609
+ break;
610
+ }
611
+ _context3.next = 25;
604
612
  return attachSavedClientIfNeeded(_cartJson2);
605
- case 18:
613
+ case 25:
606
614
  _cartJson2 = _context3.sent;
607
615
  setCart(_objectSpread({}, _cartJson2));
608
- _context3.next = 24;
616
+ _context3.next = 31;
609
617
  break;
610
- case 22:
618
+ case 29:
611
619
  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.");
612
620
  setCartSettingsInvalid(true);
613
- case 24:
614
- _context3.next = 30;
621
+ case 31:
622
+ _context3.next = 37;
615
623
  break;
616
- case 26:
617
- _context3.prev = 26;
624
+ case 33:
625
+ _context3.prev = 33;
618
626
  _context3.t0 = _context3["catch"](3);
619
- _context3.next = 30;
627
+ _context3.next = 37;
620
628
  return createCartAndCookie();
621
- case 30:
622
- _context3.prev = 30;
629
+ case 37:
630
+ _context3.prev = 37;
623
631
  setFetchingCart(false);
624
- return _context3.finish(30);
625
- case 33:
632
+ return _context3.finish(37);
633
+ case 40:
626
634
  case "end":
627
635
  return _context3.stop();
628
636
  }
629
- }, _callee3, null, [[3, 26, 30, 33]]);
637
+ }, _callee3, null, [[3, 33, 37, 40]]);
630
638
  }));
631
639
  return _getCart.apply(this, arguments);
632
640
  }
@@ -17,10 +17,10 @@ import { useForm } from "react-hook-form";
17
17
  import InputsAddress from "./InputsAddress";
18
18
  import { CheckoutContainer, InnerContainer } from "../components/containers";
19
19
  import { Title } from "../components";
20
- import { shipmentFormId } from "../utilities/shipment";
20
+ import { checkoutShipmentHydrationAction, shipmentFormId } from "../utilities/shipment";
21
21
  import { faMapMarkerAlt } from "@fortawesome/free-solid-svg-icons";
22
22
  import { useCartContext } from "../CartProvider";
23
- import { useState } from "react";
23
+ import { useEffect, useRef, useState } from "react";
24
24
  import { getCartCookieToken } from "../utilities/cookie";
25
25
  import { makeAddress, sanitizeAddressFields } from "../utilities/address";
26
26
  import { makeCart } from "../utilities/cart";
@@ -122,7 +122,9 @@ export function ActiveFormShipmentAddressAndNotes() {
122
122
  cart = _useCartContext2.cart,
123
123
  setCart = _useCartContext2.setCart,
124
124
  setLoadingTotals = _useCartContext2.setLoadingTotals,
125
- setActiveTabAndEditDisabled = _useCartContext2.setActiveTabAndEditDisabled;
125
+ setActiveTabAndEditDisabled = _useCartContext2.setActiveTabAndEditDisabled,
126
+ updateCartShipmentAddress = _useCartContext2.updateCartShipmentAddress,
127
+ getCartShipmentOptions = _useCartContext2.getCartShipmentOptions;
126
128
  var saved = getSavedCheckoutAddress(domainId);
127
129
  var cartAddress = cart === null || cart === void 0 ? void 0 : cart.receiverAddress;
128
130
  var receiverAddress = addressHasContent(cartAddress) ? cartAddress : saved.address || {};
@@ -131,6 +133,25 @@ export function ActiveFormShipmentAddressAndNotes() {
131
133
  _useState2 = _slicedToArray(_useState, 2),
132
134
  addressFieldsOpen = _useState2[0],
133
135
  setAddressFieldsOpen = _useState2[1];
136
+ var hydratedShipmentOptionsRef = useRef(false);
137
+ useEffect(function () {
138
+ if (hydratedShipmentOptionsRef.current || !(cart !== null && cart !== void 0 && cart.id)) return;
139
+ var draft = getSavedCheckoutAddress(domainId);
140
+ var action = checkoutShipmentHydrationAction({
141
+ cartAddress: cart === null || cart === void 0 ? void 0 : cart.receiverAddress,
142
+ savedAddress: draft.address,
143
+ shipmentGroups: cart === null || cart === void 0 ? void 0 : cart.shipmentGroups
144
+ });
145
+ if (!action) return;
146
+ hydratedShipmentOptionsRef.current = true;
147
+ if (action === "apply-saved-address" && draft.address) {
148
+ updateCartShipmentAddress(draft.address);
149
+ return;
150
+ }
151
+ if (action === "refresh-quotes") {
152
+ getCartShipmentOptions === null || getCartShipmentOptions === void 0 || getCartShipmentOptions();
153
+ }
154
+ }, [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]);
134
155
  function saveCartShipmentAddressAndGoToNextTab(_x) {
135
156
  return _saveCartShipmentAddressAndGoToNextTab.apply(this, arguments);
136
157
  }
@@ -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(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _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(arr) { if (Array.isArray(arr)) return arr; }
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(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
8
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(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(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, 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 normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; }
11
+ function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
12
+ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
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
+ });