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.
- package/esm/CartProvider.js +52 -44
- package/esm/utilities/index.js +1 -0
- package/esm/utilities/job_source.js +139 -0
- package/esm/utilities/job_source.test.js +75 -0
- package/lib/CartProvider.js +52 -44
- package/lib/utilities/index.d.ts +1 -0
- package/lib/utilities/index.js +11 -0
- package/lib/utilities/job_source.d.ts +23 -0
- package/lib/utilities/job_source.js +152 -0
- package/lib/utilities/job_source.test.d.ts +1 -0
- package/package.json +1 -1
- package/src/CartProvider.tsx +31 -14
- package/src/utilities/index.ts +1 -0
- package/src/utilities/job_source.test.ts +86 -0
- package/src/utilities/job_source.ts +165 -0
package/esm/CartProvider.js
CHANGED
|
@@ -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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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:
|
|
523
|
+
body: appendJobSourceToFormData(_cart.toFormData(), jobSource),
|
|
524
|
+
query: query
|
|
525
525
|
});
|
|
526
|
-
case
|
|
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
|
|
531
|
+
case 13:
|
|
540
532
|
_cartJson = _context2.sent;
|
|
541
533
|
if (domainId) setCartCookie(Number(domainId), _cartJson, undefined, isTest);
|
|
542
|
-
_context2.next =
|
|
534
|
+
_context2.next = 17;
|
|
543
535
|
return attachSavedClientIfNeeded(_cartJson);
|
|
544
|
-
case
|
|
536
|
+
case 17:
|
|
545
537
|
_cartJson = _context2.sent;
|
|
546
538
|
setCart(_objectSpread({}, _cartJson));
|
|
547
539
|
if (callback) callback();
|
|
548
|
-
_context2.next =
|
|
540
|
+
_context2.next = 25;
|
|
549
541
|
break;
|
|
550
|
-
case
|
|
551
|
-
_context2.prev =
|
|
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
|
|
555
|
-
_context2.prev =
|
|
546
|
+
case 25:
|
|
547
|
+
_context2.prev = 25;
|
|
556
548
|
setFetchingCart(false);
|
|
557
|
-
return _context2.finish(
|
|
558
|
-
case
|
|
549
|
+
return _context2.finish(25);
|
|
550
|
+
case 28:
|
|
559
551
|
case "end":
|
|
560
552
|
return _context2.stop();
|
|
561
553
|
}
|
|
562
|
-
}, _callee2, null, [[1,
|
|
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
|
-
|
|
591
|
+
jobSource = jobSourceFieldsForApi(captureAndReadJobSource());
|
|
592
|
+
if (!cartNeedsSourceUpdate(_cartJson2, jobSource)) {
|
|
600
593
|
_context3.next = 22;
|
|
601
594
|
break;
|
|
602
595
|
}
|
|
603
|
-
_context3.next =
|
|
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
|
|
613
|
+
case 25:
|
|
606
614
|
_cartJson2 = _context3.sent;
|
|
607
615
|
setCart(_objectSpread({}, _cartJson2));
|
|
608
|
-
_context3.next =
|
|
616
|
+
_context3.next = 31;
|
|
609
617
|
break;
|
|
610
|
-
case
|
|
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
|
|
614
|
-
_context3.next =
|
|
621
|
+
case 31:
|
|
622
|
+
_context3.next = 37;
|
|
615
623
|
break;
|
|
616
|
-
case
|
|
617
|
-
_context3.prev =
|
|
624
|
+
case 33:
|
|
625
|
+
_context3.prev = 33;
|
|
618
626
|
_context3.t0 = _context3["catch"](3);
|
|
619
|
-
_context3.next =
|
|
627
|
+
_context3.next = 37;
|
|
620
628
|
return createCartAndCookie();
|
|
621
|
-
case
|
|
622
|
-
_context3.prev =
|
|
629
|
+
case 37:
|
|
630
|
+
_context3.prev = 37;
|
|
623
631
|
setFetchingCart(false);
|
|
624
|
-
return _context3.finish(
|
|
625
|
-
case
|
|
632
|
+
return _context3.finish(37);
|
|
633
|
+
case 40:
|
|
626
634
|
case "end":
|
|
627
635
|
return _context3.stop();
|
|
628
636
|
}
|
|
629
|
-
}, _callee3, null, [[3,
|
|
637
|
+
}, _callee3, null, [[3, 33, 37, 40]]);
|
|
630
638
|
}));
|
|
631
639
|
return _getCart.apply(this, arguments);
|
|
632
640
|
}
|
package/esm/utilities/index.js
CHANGED
|
@@ -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
|
+
});
|
package/lib/CartProvider.js
CHANGED
|
@@ -19,6 +19,7 @@ var _product = require("./utilities/product");
|
|
|
19
19
|
var _local_storage = require("./utilities/local_storage");
|
|
20
20
|
var _user = require("./utilities/user");
|
|
21
21
|
var _test_checkout = require("./utilities/test_checkout");
|
|
22
|
+
var _job_source = require("./utilities/job_source");
|
|
22
23
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
23
24
|
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); }
|
|
24
25
|
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != _typeof(e) && "function" != typeof e) return { "default": e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n["default"] = e, t && t.set(e, n), n; }
|
|
@@ -507,66 +508,57 @@ var CartProvider = function CartProvider(_ref) {
|
|
|
507
508
|
}
|
|
508
509
|
function _createCartAndCookie() {
|
|
509
510
|
_createCartAndCookie = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee2(callback) {
|
|
510
|
-
var isTest, _cart, created, _cartJson;
|
|
511
|
+
var isTest, jobSource, _cart, query, created, _cartJson;
|
|
511
512
|
return _regeneratorRuntime().wrap(function _callee2$(_context2) {
|
|
512
513
|
while (1) switch (_context2.prev = _context2.next) {
|
|
513
514
|
case 0:
|
|
514
515
|
setFetchingCart(true);
|
|
515
516
|
_context2.prev = 1;
|
|
516
517
|
isTest = (0, _test_checkout.readTestCheckoutFlag)();
|
|
518
|
+
jobSource = (0, _job_source.jobSourceFieldsForApi)((0, _job_source.captureAndReadJobSource)());
|
|
517
519
|
_cart = (0, _cart3.makeCart)({
|
|
518
520
|
domain: {
|
|
519
521
|
id: domainId
|
|
520
522
|
},
|
|
521
523
|
isTest: isTest
|
|
522
524
|
}, true);
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
}
|
|
527
|
-
_context2.next = 7;
|
|
525
|
+
query = [['embed', JSON.stringify(_helpers.cartEmbed)], ['skip_rights', 'y']];
|
|
526
|
+
if (isTest) query.push(['is_test', 'true']);
|
|
527
|
+
_context2.next = 9;
|
|
528
528
|
return merchi.authenticatedFetch('/carts/', {
|
|
529
529
|
method: 'POST',
|
|
530
|
-
body: _cart.toFormData(),
|
|
531
|
-
query:
|
|
530
|
+
body: (0, _job_source.appendJobSourceToFormData)(_cart.toFormData(), jobSource),
|
|
531
|
+
query: query
|
|
532
532
|
});
|
|
533
|
-
case
|
|
533
|
+
case 9:
|
|
534
534
|
created = _context2.sent;
|
|
535
535
|
_cart.fromJson(created.cart);
|
|
536
536
|
_context2.next = 13;
|
|
537
|
-
break;
|
|
538
|
-
case 11:
|
|
539
|
-
_context2.next = 13;
|
|
540
|
-
return _cart.create({
|
|
541
|
-
embed: _helpers.cartEmbed
|
|
542
|
-
});
|
|
543
|
-
case 13:
|
|
544
|
-
_context2.next = 15;
|
|
545
537
|
return _cart.toJson();
|
|
546
|
-
case
|
|
538
|
+
case 13:
|
|
547
539
|
_cartJson = _context2.sent;
|
|
548
540
|
if (domainId) (0, _cookie.setCartCookie)(Number(domainId), _cartJson, undefined, isTest);
|
|
549
|
-
_context2.next =
|
|
541
|
+
_context2.next = 17;
|
|
550
542
|
return attachSavedClientIfNeeded(_cartJson);
|
|
551
|
-
case
|
|
543
|
+
case 17:
|
|
552
544
|
_cartJson = _context2.sent;
|
|
553
545
|
setCart(_objectSpread({}, _cartJson));
|
|
554
546
|
if (callback) callback();
|
|
555
|
-
_context2.next =
|
|
547
|
+
_context2.next = 25;
|
|
556
548
|
break;
|
|
557
|
-
case
|
|
558
|
-
_context2.prev =
|
|
549
|
+
case 22:
|
|
550
|
+
_context2.prev = 22;
|
|
559
551
|
_context2.t0 = _context2["catch"](1);
|
|
560
552
|
alertError(_context2.t0.errorMessage || _context2.t0.message || 'Unable to fetch cart.');
|
|
561
|
-
case
|
|
562
|
-
_context2.prev =
|
|
553
|
+
case 25:
|
|
554
|
+
_context2.prev = 25;
|
|
563
555
|
setFetchingCart(false);
|
|
564
|
-
return _context2.finish(
|
|
565
|
-
case
|
|
556
|
+
return _context2.finish(25);
|
|
557
|
+
case 28:
|
|
566
558
|
case "end":
|
|
567
559
|
return _context2.stop();
|
|
568
560
|
}
|
|
569
|
-
}, _callee2, null, [[1,
|
|
561
|
+
}, _callee2, null, [[1, 22, 25, 28]]);
|
|
570
562
|
}));
|
|
571
563
|
return _createCartAndCookie.apply(this, arguments);
|
|
572
564
|
}
|
|
@@ -575,7 +567,7 @@ var CartProvider = function CartProvider(_ref) {
|
|
|
575
567
|
}
|
|
576
568
|
function _getCart() {
|
|
577
569
|
_getCart = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee3(cartIdAndToken) {
|
|
578
|
-
var id, _cart2, _cartJson2, isTest;
|
|
570
|
+
var id, _cart2, _cartJson2, isTest, jobSource, saved;
|
|
579
571
|
return _regeneratorRuntime().wrap(function _callee3$(_context3) {
|
|
580
572
|
while (1) switch (_context3.prev = _context3.next) {
|
|
581
573
|
case 0:
|
|
@@ -603,37 +595,53 @@ var CartProvider = function CartProvider(_ref) {
|
|
|
603
595
|
case 14:
|
|
604
596
|
return _context3.abrupt("return");
|
|
605
597
|
case 15:
|
|
606
|
-
|
|
598
|
+
jobSource = (0, _job_source.jobSourceFieldsForApi)((0, _job_source.captureAndReadJobSource)());
|
|
599
|
+
if (!(0, _job_source.cartNeedsSourceUpdate)(_cartJson2, jobSource)) {
|
|
607
600
|
_context3.next = 22;
|
|
608
601
|
break;
|
|
609
602
|
}
|
|
610
|
-
_context3.next =
|
|
603
|
+
_context3.next = 19;
|
|
604
|
+
return merchi.authenticatedFetch("/carts/".concat(_cart2.id, "/"), {
|
|
605
|
+
method: 'PATCH',
|
|
606
|
+
body: (0, _job_source.appendJobSourceToFormData)(_cart2.toFormData(), jobSource),
|
|
607
|
+
query: [['embed', JSON.stringify(_helpers.cartEmbed)], ['skip_rights', 'y']]
|
|
608
|
+
});
|
|
609
|
+
case 19:
|
|
610
|
+
saved = _context3.sent;
|
|
611
|
+
_cart2.fromJson(saved.cart);
|
|
612
|
+
_cartJson2 = _cart2.toJson();
|
|
613
|
+
case 22:
|
|
614
|
+
if (!((0, _cart3.stripeIsValidAndActive)(_cartJson2) || isTest)) {
|
|
615
|
+
_context3.next = 29;
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
_context3.next = 25;
|
|
611
619
|
return attachSavedClientIfNeeded(_cartJson2);
|
|
612
|
-
case
|
|
620
|
+
case 25:
|
|
613
621
|
_cartJson2 = _context3.sent;
|
|
614
622
|
setCart(_objectSpread({}, _cartJson2));
|
|
615
|
-
_context3.next =
|
|
623
|
+
_context3.next = 31;
|
|
616
624
|
break;
|
|
617
|
-
case
|
|
625
|
+
case 29:
|
|
618
626
|
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.");
|
|
619
627
|
setCartSettingsInvalid(true);
|
|
620
|
-
case
|
|
621
|
-
_context3.next =
|
|
628
|
+
case 31:
|
|
629
|
+
_context3.next = 37;
|
|
622
630
|
break;
|
|
623
|
-
case
|
|
624
|
-
_context3.prev =
|
|
631
|
+
case 33:
|
|
632
|
+
_context3.prev = 33;
|
|
625
633
|
_context3.t0 = _context3["catch"](3);
|
|
626
|
-
_context3.next =
|
|
634
|
+
_context3.next = 37;
|
|
627
635
|
return createCartAndCookie();
|
|
628
|
-
case
|
|
629
|
-
_context3.prev =
|
|
636
|
+
case 37:
|
|
637
|
+
_context3.prev = 37;
|
|
630
638
|
setFetchingCart(false);
|
|
631
|
-
return _context3.finish(
|
|
632
|
-
case
|
|
639
|
+
return _context3.finish(37);
|
|
640
|
+
case 40:
|
|
633
641
|
case "end":
|
|
634
642
|
return _context3.stop();
|
|
635
643
|
}
|
|
636
|
-
}, _callee3, null, [[3,
|
|
644
|
+
}, _callee3, null, [[3, 33, 37, 40]]);
|
|
637
645
|
}));
|
|
638
646
|
return _getCart.apply(this, arguments);
|
|
639
647
|
}
|
package/lib/utilities/index.d.ts
CHANGED
package/lib/utilities/index.js
CHANGED
|
@@ -91,6 +91,17 @@ Object.keys(_job).forEach(function (key) {
|
|
|
91
91
|
}
|
|
92
92
|
});
|
|
93
93
|
});
|
|
94
|
+
var _job_source = require("./job_source");
|
|
95
|
+
Object.keys(_job_source).forEach(function (key) {
|
|
96
|
+
if (key === "default" || key === "__esModule") return;
|
|
97
|
+
if (key in exports && exports[key] === _job_source[key]) return;
|
|
98
|
+
Object.defineProperty(exports, key, {
|
|
99
|
+
enumerable: true,
|
|
100
|
+
get: function get() {
|
|
101
|
+
return _job_source[key];
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
94
105
|
var _local_storage = require("./local_storage");
|
|
95
106
|
Object.keys(_local_storage).forEach(function (key) {
|
|
96
107
|
if (key === "default" || key === "__esModule") return;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const JOB_SOURCE_STORAGE_KEY = "merchi_job_source";
|
|
2
|
+
export interface JobSourceFields {
|
|
3
|
+
utmSource?: string;
|
|
4
|
+
utmMedium?: string;
|
|
5
|
+
utmCampaign?: string;
|
|
6
|
+
utmContent?: string;
|
|
7
|
+
utmTerm?: string;
|
|
8
|
+
sourceClickId?: string;
|
|
9
|
+
sourceLanding?: string;
|
|
10
|
+
sourceReferrer?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function sourceFromLocation(): JobSourceFields;
|
|
13
|
+
export declare function hasPaidClick(source: JobSourceFields | null | undefined): boolean;
|
|
14
|
+
export declare function hasSourceData(source: JobSourceFields | null | undefined): boolean;
|
|
15
|
+
export declare function captureAndReadJobSource(): JobSourceFields;
|
|
16
|
+
export declare function jobSourceFieldsForApi(source?: JobSourceFields | null): JobSourceFields;
|
|
17
|
+
export declare function cartNeedsSourceUpdate(cart: {
|
|
18
|
+
utmSource?: string;
|
|
19
|
+
sourceClickId?: string;
|
|
20
|
+
sourceChannel?: string;
|
|
21
|
+
} | null | undefined, source: JobSourceFields): boolean;
|
|
22
|
+
export declare const JOB_SOURCE_URL_KEYS: ("gclid" | "gbraid" | "wbraid" | "fbclid" | "msclkid" | "utm_source" | "utm_medium" | "utm_campaign" | "utm_content" | "utm_term")[];
|
|
23
|
+
export declare function appendJobSourceToFormData(data: FormData, source?: JobSourceFields | null): FormData;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.JOB_SOURCE_URL_KEYS = exports.JOB_SOURCE_STORAGE_KEY = void 0;
|
|
7
|
+
exports.appendJobSourceToFormData = appendJobSourceToFormData;
|
|
8
|
+
exports.captureAndReadJobSource = captureAndReadJobSource;
|
|
9
|
+
exports.cartNeedsSourceUpdate = cartNeedsSourceUpdate;
|
|
10
|
+
exports.hasPaidClick = hasPaidClick;
|
|
11
|
+
exports.hasSourceData = hasSourceData;
|
|
12
|
+
exports.jobSourceFieldsForApi = jobSourceFieldsForApi;
|
|
13
|
+
exports.sourceFromLocation = sourceFromLocation;
|
|
14
|
+
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
|
|
15
|
+
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."); }
|
|
16
|
+
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; } }
|
|
17
|
+
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
|
|
18
|
+
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; }
|
|
19
|
+
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; }
|
|
20
|
+
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; }
|
|
21
|
+
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); }
|
|
22
|
+
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); }
|
|
23
|
+
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; } } }; }
|
|
24
|
+
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); }
|
|
25
|
+
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; }
|
|
26
|
+
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); }
|
|
27
|
+
var JOB_SOURCE_STORAGE_KEY = exports.JOB_SOURCE_STORAGE_KEY = 'merchi_job_source';
|
|
28
|
+
var CLICK_ID_PARAMS = ['gclid', 'gbraid', 'wbraid', 'fbclid', 'msclkid'];
|
|
29
|
+
var UTM_PARAMS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'];
|
|
30
|
+
function trimTo(value, limit) {
|
|
31
|
+
if (!value) return undefined;
|
|
32
|
+
var text = value.trim().slice(0, limit);
|
|
33
|
+
return text || undefined;
|
|
34
|
+
}
|
|
35
|
+
function readSearchParams() {
|
|
36
|
+
if (typeof window === 'undefined') return new URLSearchParams();
|
|
37
|
+
try {
|
|
38
|
+
return new URLSearchParams(window.location.search);
|
|
39
|
+
} catch (_unused) {
|
|
40
|
+
return new URLSearchParams();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function readStoredSource() {
|
|
44
|
+
if (typeof window === 'undefined') return null;
|
|
45
|
+
try {
|
|
46
|
+
var raw = window.localStorage.getItem(JOB_SOURCE_STORAGE_KEY);
|
|
47
|
+
if (!raw) return null;
|
|
48
|
+
var parsed = JSON.parse(raw);
|
|
49
|
+
if (!parsed || _typeof(parsed) !== 'object') return null;
|
|
50
|
+
return parsed;
|
|
51
|
+
} catch (_unused2) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function writeStoredSource(source) {
|
|
56
|
+
if (typeof window === 'undefined') return;
|
|
57
|
+
try {
|
|
58
|
+
window.localStorage.setItem(JOB_SOURCE_STORAGE_KEY, JSON.stringify(source));
|
|
59
|
+
} catch (_unused3) {
|
|
60
|
+
// Ignore quota / private-mode failures.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function currentLanding() {
|
|
64
|
+
if (typeof window === 'undefined') return undefined;
|
|
65
|
+
try {
|
|
66
|
+
var path = "".concat(window.location.pathname || '/').concat(window.location.search || '');
|
|
67
|
+
return trimTo(path, 512);
|
|
68
|
+
} catch (_unused4) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function currentReferrer() {
|
|
73
|
+
if (typeof document === 'undefined') return undefined;
|
|
74
|
+
try {
|
|
75
|
+
return trimTo(document.referrer, 512);
|
|
76
|
+
} catch (_unused5) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function sourceFromLocation() {
|
|
81
|
+
var params = readSearchParams();
|
|
82
|
+
var source = {
|
|
83
|
+
utmSource: trimTo(params.get('utm_source'), 256),
|
|
84
|
+
utmMedium: trimTo(params.get('utm_medium'), 256),
|
|
85
|
+
utmCampaign: trimTo(params.get('utm_campaign'), 256),
|
|
86
|
+
utmContent: trimTo(params.get('utm_content'), 256),
|
|
87
|
+
utmTerm: trimTo(params.get('utm_term'), 256),
|
|
88
|
+
sourceLanding: currentLanding(),
|
|
89
|
+
sourceReferrer: currentReferrer()
|
|
90
|
+
};
|
|
91
|
+
var _iterator = _createForOfIteratorHelper(CLICK_ID_PARAMS),
|
|
92
|
+
_step;
|
|
93
|
+
try {
|
|
94
|
+
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
95
|
+
var key = _step.value;
|
|
96
|
+
var value = trimTo(params.get(key), 256);
|
|
97
|
+
if (value) {
|
|
98
|
+
source.sourceClickId = value;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch (err) {
|
|
103
|
+
_iterator.e(err);
|
|
104
|
+
} finally {
|
|
105
|
+
_iterator.f();
|
|
106
|
+
}
|
|
107
|
+
return source;
|
|
108
|
+
}
|
|
109
|
+
function hasPaidClick(source) {
|
|
110
|
+
return Boolean(source === null || source === void 0 ? void 0 : source.sourceClickId);
|
|
111
|
+
}
|
|
112
|
+
function hasSourceData(source) {
|
|
113
|
+
if (!source) return false;
|
|
114
|
+
return Boolean(source.utmSource || source.utmMedium || source.utmCampaign || source.utmContent || source.utmTerm || source.sourceClickId || source.sourceLanding || source.sourceReferrer);
|
|
115
|
+
}
|
|
116
|
+
function captureAndReadJobSource() {
|
|
117
|
+
var incoming = sourceFromLocation();
|
|
118
|
+
var stored = readStoredSource();
|
|
119
|
+
if (hasPaidClick(incoming) || !hasSourceData(stored)) {
|
|
120
|
+
var next = _objectSpread(_objectSpread({}, stored || {}), Object.fromEntries(Object.entries(incoming).filter(function (_ref) {
|
|
121
|
+
var _ref2 = _slicedToArray(_ref, 2),
|
|
122
|
+
value = _ref2[1];
|
|
123
|
+
return Boolean(value);
|
|
124
|
+
})));
|
|
125
|
+
if (hasSourceData(next)) writeStoredSource(next);
|
|
126
|
+
return next;
|
|
127
|
+
}
|
|
128
|
+
return stored || incoming;
|
|
129
|
+
}
|
|
130
|
+
function jobSourceFieldsForApi(source) {
|
|
131
|
+
var resolved = source || captureAndReadJobSource();
|
|
132
|
+
var out = {};
|
|
133
|
+
Object.keys(resolved).forEach(function (key) {
|
|
134
|
+
var value = resolved[key];
|
|
135
|
+
if (value) out[key] = value;
|
|
136
|
+
});
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
function cartNeedsSourceUpdate(cart, source) {
|
|
140
|
+
if (!hasSourceData(source)) return false;
|
|
141
|
+
if (!(cart !== null && cart !== void 0 && cart.utmSource) && !(cart !== null && cart !== void 0 && cart.sourceClickId) && !(cart !== null && cart !== void 0 && cart.sourceChannel)) return true;
|
|
142
|
+
return Boolean(source.sourceClickId && !(cart !== null && cart !== void 0 && cart.sourceClickId));
|
|
143
|
+
}
|
|
144
|
+
var JOB_SOURCE_URL_KEYS = exports.JOB_SOURCE_URL_KEYS = [].concat(UTM_PARAMS, CLICK_ID_PARAMS);
|
|
145
|
+
function appendJobSourceToFormData(data, source) {
|
|
146
|
+
var fields = jobSourceFieldsForApi(source);
|
|
147
|
+
Object.keys(fields).forEach(function (key) {
|
|
148
|
+
var value = fields[key];
|
|
149
|
+
if (value) data.append(key, value);
|
|
150
|
+
});
|
|
151
|
+
return data;
|
|
152
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
package/src/CartProvider.tsx
CHANGED
|
@@ -21,6 +21,12 @@ import {
|
|
|
21
21
|
} from './utilities/local_storage';
|
|
22
22
|
import { makeUser } from './utilities/user';
|
|
23
23
|
import { persistTestCheckoutFlag, readTestCheckoutFlag } from './utilities/test_checkout';
|
|
24
|
+
import {
|
|
25
|
+
appendJobSourceToFormData,
|
|
26
|
+
captureAndReadJobSource,
|
|
27
|
+
cartNeedsSourceUpdate,
|
|
28
|
+
jobSourceFieldsForApi,
|
|
29
|
+
} from './utilities/job_source';
|
|
24
30
|
|
|
25
31
|
export interface PropsCart {
|
|
26
32
|
cart: any;
|
|
@@ -622,21 +628,19 @@ const CartProvider = ({
|
|
|
622
628
|
setFetchingCart(true);
|
|
623
629
|
try {
|
|
624
630
|
const isTest = readTestCheckoutFlag();
|
|
631
|
+
const jobSource = jobSourceFieldsForApi(captureAndReadJobSource());
|
|
625
632
|
const cart = makeCart({domain: {id: domainId}, isTest}, true);
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
} else {
|
|
638
|
-
await cart.create({embed: cartEmbed});
|
|
639
|
-
}
|
|
633
|
+
const query: Array<[string, string]> = [
|
|
634
|
+
['embed', JSON.stringify(cartEmbed)],
|
|
635
|
+
['skip_rights', 'y'],
|
|
636
|
+
];
|
|
637
|
+
if (isTest) query.push(['is_test', 'true']);
|
|
638
|
+
const created = await merchi.authenticatedFetch('/carts/', {
|
|
639
|
+
method: 'POST',
|
|
640
|
+
body: appendJobSourceToFormData(cart.toFormData() as FormData, jobSource),
|
|
641
|
+
query,
|
|
642
|
+
});
|
|
643
|
+
cart.fromJson(created.cart);
|
|
640
644
|
let cartJson = await cart.toJson();
|
|
641
645
|
if (domainId) setCartCookie(Number(domainId), cartJson, undefined, isTest);
|
|
642
646
|
cartJson = await attachSavedClientIfNeeded(cartJson);
|
|
@@ -663,6 +667,19 @@ const CartProvider = ({
|
|
|
663
667
|
await createCartAndCookie();
|
|
664
668
|
return;
|
|
665
669
|
}
|
|
670
|
+
const jobSource = jobSourceFieldsForApi(captureAndReadJobSource());
|
|
671
|
+
if (cartNeedsSourceUpdate(cartJson, jobSource)) {
|
|
672
|
+
const saved = await merchi.authenticatedFetch(`/carts/${cart.id}/`, {
|
|
673
|
+
method: 'PATCH',
|
|
674
|
+
body: appendJobSourceToFormData(cart.toFormData() as FormData, jobSource),
|
|
675
|
+
query: [
|
|
676
|
+
['embed', JSON.stringify(cartEmbed)],
|
|
677
|
+
['skip_rights', 'y'],
|
|
678
|
+
],
|
|
679
|
+
});
|
|
680
|
+
cart.fromJson(saved.cart);
|
|
681
|
+
cartJson = cart.toJson();
|
|
682
|
+
}
|
|
666
683
|
if (stripeIsValidAndActive(cartJson) || isTest) {
|
|
667
684
|
cartJson = await attachSavedClientIfNeeded(cartJson);
|
|
668
685
|
setCart({...cartJson});
|
package/src/utilities/index.ts
CHANGED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JOB_SOURCE_STORAGE_KEY,
|
|
3
|
+
captureAndReadJobSource,
|
|
4
|
+
cartNeedsSourceUpdate,
|
|
5
|
+
hasPaidClick,
|
|
6
|
+
} from './job_source';
|
|
7
|
+
|
|
8
|
+
describe('job source capture', () => {
|
|
9
|
+
const originalLocation = window.location;
|
|
10
|
+
const originalReferrer = document.referrer;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
window.localStorage.clear();
|
|
14
|
+
delete (window as any).location;
|
|
15
|
+
(window as any).location = {
|
|
16
|
+
search: '',
|
|
17
|
+
pathname: '/tyvek-wristbands',
|
|
18
|
+
};
|
|
19
|
+
Object.defineProperty(document, 'referrer', {
|
|
20
|
+
configurable: true,
|
|
21
|
+
value: '',
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
(window as any).location = originalLocation;
|
|
27
|
+
Object.defineProperty(document, 'referrer', {
|
|
28
|
+
configurable: true,
|
|
29
|
+
value: originalReferrer,
|
|
30
|
+
});
|
|
31
|
+
window.localStorage.clear();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('captures google ads click ids and landing path', () => {
|
|
35
|
+
(window as any).location = {
|
|
36
|
+
search: '?utm_source=google&utm_medium=cpc&gclid=EAIa&utm_campaign=bands',
|
|
37
|
+
pathname: '/tyvek-wristbands',
|
|
38
|
+
};
|
|
39
|
+
const source = captureAndReadJobSource();
|
|
40
|
+
expect(source.utmSource).toBe('google');
|
|
41
|
+
expect(source.utmMedium).toBe('cpc');
|
|
42
|
+
expect(source.utmCampaign).toBe('bands');
|
|
43
|
+
expect(source.sourceClickId).toBe('EAIa');
|
|
44
|
+
expect(source.sourceLanding).toBe(
|
|
45
|
+
'/tyvek-wristbands?utm_source=google&utm_medium=cpc&gclid=EAIa&utm_campaign=bands'
|
|
46
|
+
);
|
|
47
|
+
expect(hasPaidClick(source)).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('keeps first-touch UTMs unless a paid click arrives later', () => {
|
|
51
|
+
(window as any).location = {
|
|
52
|
+
search: '?utm_source=newsletter&utm_medium=email',
|
|
53
|
+
pathname: '/',
|
|
54
|
+
};
|
|
55
|
+
captureAndReadJobSource();
|
|
56
|
+
(window as any).location = {
|
|
57
|
+
search: '?utm_source=google&utm_medium=cpc',
|
|
58
|
+
pathname: '/product',
|
|
59
|
+
};
|
|
60
|
+
expect(captureAndReadJobSource().utmSource).toBe('newsletter');
|
|
61
|
+
(window as any).location = {
|
|
62
|
+
search: '?gclid=NEWCLICK&utm_source=google',
|
|
63
|
+
pathname: '/product',
|
|
64
|
+
};
|
|
65
|
+
expect(captureAndReadJobSource().sourceClickId).toBe('NEWCLICK');
|
|
66
|
+
expect(window.localStorage.getItem(JOB_SOURCE_STORAGE_KEY)).toContain('NEWCLICK');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('knows when an existing cart still needs source fields', () => {
|
|
70
|
+
expect(
|
|
71
|
+
cartNeedsSourceUpdate({}, { utmSource: 'google', sourceClickId: 'x' })
|
|
72
|
+
).toBe(true);
|
|
73
|
+
expect(
|
|
74
|
+
cartNeedsSourceUpdate(
|
|
75
|
+
{ utmSource: 'google' },
|
|
76
|
+
{ utmSource: 'google', sourceClickId: 'x' }
|
|
77
|
+
)
|
|
78
|
+
).toBe(true);
|
|
79
|
+
expect(
|
|
80
|
+
cartNeedsSourceUpdate(
|
|
81
|
+
{ utmSource: 'google', sourceClickId: 'x' },
|
|
82
|
+
{ utmSource: 'google', sourceClickId: 'x' }
|
|
83
|
+
)
|
|
84
|
+
).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
export const JOB_SOURCE_STORAGE_KEY = 'merchi_job_source';
|
|
2
|
+
|
|
3
|
+
const CLICK_ID_PARAMS = ['gclid', 'gbraid', 'wbraid', 'fbclid', 'msclkid'] as const;
|
|
4
|
+
const UTM_PARAMS = [
|
|
5
|
+
'utm_source',
|
|
6
|
+
'utm_medium',
|
|
7
|
+
'utm_campaign',
|
|
8
|
+
'utm_content',
|
|
9
|
+
'utm_term',
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
export interface JobSourceFields {
|
|
13
|
+
utmSource?: string;
|
|
14
|
+
utmMedium?: string;
|
|
15
|
+
utmCampaign?: string;
|
|
16
|
+
utmContent?: string;
|
|
17
|
+
utmTerm?: string;
|
|
18
|
+
sourceClickId?: string;
|
|
19
|
+
sourceLanding?: string;
|
|
20
|
+
sourceReferrer?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function trimTo(value: string | null | undefined, limit: number): string | undefined {
|
|
24
|
+
if (!value) return undefined;
|
|
25
|
+
const text = value.trim().slice(0, limit);
|
|
26
|
+
return text || undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readSearchParams(): URLSearchParams {
|
|
30
|
+
if (typeof window === 'undefined') return new URLSearchParams();
|
|
31
|
+
try {
|
|
32
|
+
return new URLSearchParams(window.location.search);
|
|
33
|
+
} catch {
|
|
34
|
+
return new URLSearchParams();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readStoredSource(): JobSourceFields | null {
|
|
39
|
+
if (typeof window === 'undefined') return null;
|
|
40
|
+
try {
|
|
41
|
+
const raw = window.localStorage.getItem(JOB_SOURCE_STORAGE_KEY);
|
|
42
|
+
if (!raw) return null;
|
|
43
|
+
const parsed = JSON.parse(raw);
|
|
44
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
45
|
+
return parsed as JobSourceFields;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function writeStoredSource(source: JobSourceFields) {
|
|
52
|
+
if (typeof window === 'undefined') return;
|
|
53
|
+
try {
|
|
54
|
+
window.localStorage.setItem(JOB_SOURCE_STORAGE_KEY, JSON.stringify(source));
|
|
55
|
+
} catch {
|
|
56
|
+
// Ignore quota / private-mode failures.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function currentLanding(): string | undefined {
|
|
61
|
+
if (typeof window === 'undefined') return undefined;
|
|
62
|
+
try {
|
|
63
|
+
const path = `${window.location.pathname || '/'}${window.location.search || ''}`;
|
|
64
|
+
return trimTo(path, 512);
|
|
65
|
+
} catch {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function currentReferrer(): string | undefined {
|
|
71
|
+
if (typeof document === 'undefined') return undefined;
|
|
72
|
+
try {
|
|
73
|
+
return trimTo(document.referrer, 512);
|
|
74
|
+
} catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function sourceFromLocation(): JobSourceFields {
|
|
80
|
+
const params = readSearchParams();
|
|
81
|
+
const source: JobSourceFields = {
|
|
82
|
+
utmSource: trimTo(params.get('utm_source'), 256),
|
|
83
|
+
utmMedium: trimTo(params.get('utm_medium'), 256),
|
|
84
|
+
utmCampaign: trimTo(params.get('utm_campaign'), 256),
|
|
85
|
+
utmContent: trimTo(params.get('utm_content'), 256),
|
|
86
|
+
utmTerm: trimTo(params.get('utm_term'), 256),
|
|
87
|
+
sourceLanding: currentLanding(),
|
|
88
|
+
sourceReferrer: currentReferrer(),
|
|
89
|
+
};
|
|
90
|
+
for (const key of CLICK_ID_PARAMS) {
|
|
91
|
+
const value = trimTo(params.get(key), 256);
|
|
92
|
+
if (value) {
|
|
93
|
+
source.sourceClickId = value;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return source;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function hasPaidClick(source: JobSourceFields | null | undefined): boolean {
|
|
101
|
+
return Boolean(source?.sourceClickId);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function hasSourceData(source: JobSourceFields | null | undefined): boolean {
|
|
105
|
+
if (!source) return false;
|
|
106
|
+
return Boolean(
|
|
107
|
+
source.utmSource ||
|
|
108
|
+
source.utmMedium ||
|
|
109
|
+
source.utmCampaign ||
|
|
110
|
+
source.utmContent ||
|
|
111
|
+
source.utmTerm ||
|
|
112
|
+
source.sourceClickId ||
|
|
113
|
+
source.sourceLanding ||
|
|
114
|
+
source.sourceReferrer
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function captureAndReadJobSource(): JobSourceFields {
|
|
119
|
+
const incoming = sourceFromLocation();
|
|
120
|
+
const stored = readStoredSource();
|
|
121
|
+
if (hasPaidClick(incoming) || !hasSourceData(stored)) {
|
|
122
|
+
const next = {
|
|
123
|
+
...(stored || {}),
|
|
124
|
+
...Object.fromEntries(
|
|
125
|
+
Object.entries(incoming).filter(([, value]) => Boolean(value))
|
|
126
|
+
),
|
|
127
|
+
} as JobSourceFields;
|
|
128
|
+
if (hasSourceData(next)) writeStoredSource(next);
|
|
129
|
+
return next;
|
|
130
|
+
}
|
|
131
|
+
return stored || incoming;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function jobSourceFieldsForApi(source?: JobSourceFields | null): JobSourceFields {
|
|
135
|
+
const resolved = source || captureAndReadJobSource();
|
|
136
|
+
const out: JobSourceFields = {};
|
|
137
|
+
(Object.keys(resolved) as Array<keyof JobSourceFields>).forEach((key) => {
|
|
138
|
+
const value = resolved[key];
|
|
139
|
+
if (value) out[key] = value;
|
|
140
|
+
});
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function cartNeedsSourceUpdate(
|
|
145
|
+
cart: { utmSource?: string; sourceClickId?: string; sourceChannel?: string } | null | undefined,
|
|
146
|
+
source: JobSourceFields
|
|
147
|
+
): boolean {
|
|
148
|
+
if (!hasSourceData(source)) return false;
|
|
149
|
+
if (!cart?.utmSource && !cart?.sourceClickId && !cart?.sourceChannel) return true;
|
|
150
|
+
return Boolean(source.sourceClickId && !cart?.sourceClickId);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const JOB_SOURCE_URL_KEYS = [...UTM_PARAMS, ...CLICK_ID_PARAMS];
|
|
154
|
+
|
|
155
|
+
export function appendJobSourceToFormData(
|
|
156
|
+
data: FormData,
|
|
157
|
+
source?: JobSourceFields | null
|
|
158
|
+
): FormData {
|
|
159
|
+
const fields = jobSourceFieldsForApi(source);
|
|
160
|
+
(Object.keys(fields) as Array<keyof JobSourceFields>).forEach((key) => {
|
|
161
|
+
const value = fields[key];
|
|
162
|
+
if (value) data.append(key, value);
|
|
163
|
+
});
|
|
164
|
+
return data;
|
|
165
|
+
}
|