@merchi/cart 1.4.4 → 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 +58 -20
- package/esm/forms/FormShipmentAddressAndNotes.js +24 -3
- package/esm/stripe/LegacyStripeCardForm.js +11 -4
- package/esm/stripe/StripeCardForm.js +122 -16
- package/esm/stripe/actions.js +155 -57
- package/esm/stripe/actions.test.js +120 -0
- package/esm/utilities/cookie.js +15 -12
- package/esm/utilities/index.js +2 -0
- package/esm/utilities/job_source.js +139 -0
- package/esm/utilities/job_source.test.js +75 -0
- package/esm/utilities/shipment.js +22 -0
- package/esm/utilities/shipment.test.js +60 -0
- package/esm/utilities/test_checkout.js +25 -0
- package/esm/utilities/test_checkout.test.js +31 -0
- package/lib/CartProvider.js +58 -20
- package/lib/forms/FormShipmentAddressAndNotes.js +22 -1
- package/lib/stripe/LegacyStripeCardForm.js +10 -3
- package/lib/stripe/StripeCardForm.js +123 -17
- package/lib/stripe/actions.d.ts +2 -0
- package/lib/stripe/actions.js +157 -57
- package/lib/stripe/actions.test.d.ts +1 -0
- package/lib/utilities/cookie.d.ts +3 -3
- package/lib/utilities/cookie.js +15 -12
- package/lib/utilities/index.d.ts +2 -0
- package/lib/utilities/index.js +22 -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/lib/utilities/shipment.d.ts +15 -0
- package/lib/utilities/shipment.js +24 -0
- package/lib/utilities/shipment.test.d.ts +1 -0
- package/lib/utilities/test_checkout.d.ts +6 -0
- package/lib/utilities/test_checkout.js +34 -0
- package/lib/utilities/test_checkout.test.d.ts +1 -0
- package/package.json +8 -5
- package/src/CartProvider.tsx +42 -4
- package/src/forms/FormShipmentAddressAndNotes.tsx +34 -2
- package/src/stripe/LegacyStripeCardForm.tsx +9 -3
- package/src/stripe/StripeCardForm.tsx +92 -8
- package/src/stripe/actions.test.ts +50 -0
- package/src/stripe/actions.ts +41 -0
- package/src/utilities/cookie.ts +9 -6
- package/src/utilities/index.ts +2 -0
- package/src/utilities/job_source.test.ts +86 -0
- package/src/utilities/job_source.ts +165 -0
- package/src/utilities/shipment.test.ts +68 -0
- package/src/utilities/shipment.ts +34 -0
- package/src/utilities/test_checkout.test.ts +39 -0
- package/src/utilities/test_checkout.ts +27 -0
|
@@ -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(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _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(r) { if (Array.isArray(r)) return r; }
|
|
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(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; }
|
|
21
|
+
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : 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(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; } } }; }
|
|
24
|
+
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; } }
|
|
25
|
+
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; }
|
|
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 {};
|
|
@@ -1,2 +1,17 @@
|
|
|
1
|
+
import { SavedCheckoutAddress } from './local_storage';
|
|
1
2
|
export declare const shipmentFormId = "_shipment-address-form-id";
|
|
2
3
|
export declare function cartItemsNeedShipment(cart: any): boolean;
|
|
4
|
+
export declare function shipmentGroupsHaveQuotes(groups?: any[] | null): boolean;
|
|
5
|
+
export type CheckoutShipmentHydrationAction = 'apply-saved-address' | 'refresh-quotes' | null;
|
|
6
|
+
/**
|
|
7
|
+
* When checkout hydrates a saved address (localStorage) the form shows it,
|
|
8
|
+
* but quotes are only fetched after the user edits the field. Returning
|
|
9
|
+
* `apply-saved-address` means the cart still needs that address applied so
|
|
10
|
+
* quotes can be generated; `refresh-quotes` means the cart already has an
|
|
11
|
+
* address but no usable shipment options.
|
|
12
|
+
*/
|
|
13
|
+
export declare function checkoutShipmentHydrationAction(options: {
|
|
14
|
+
cartAddress?: SavedCheckoutAddress | null;
|
|
15
|
+
savedAddress?: SavedCheckoutAddress | null;
|
|
16
|
+
shipmentGroups?: any[] | null;
|
|
17
|
+
}): CheckoutShipmentHydrationAction;
|
|
@@ -4,7 +4,10 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.cartItemsNeedShipment = cartItemsNeedShipment;
|
|
7
|
+
exports.checkoutShipmentHydrationAction = checkoutShipmentHydrationAction;
|
|
7
8
|
exports.shipmentFormId = void 0;
|
|
9
|
+
exports.shipmentGroupsHaveQuotes = shipmentGroupsHaveQuotes;
|
|
10
|
+
var _local_storage = require("./local_storage");
|
|
8
11
|
var shipmentFormId = exports.shipmentFormId = '_shipment-address-form-id';
|
|
9
12
|
function cartItemsNeedShipment(cart) {
|
|
10
13
|
var _cart$cartItems = cart.cartItems,
|
|
@@ -15,4 +18,25 @@ function cartItemsNeedShipment(cart) {
|
|
|
15
18
|
}
|
|
16
19
|
}
|
|
17
20
|
return false;
|
|
21
|
+
}
|
|
22
|
+
function shipmentGroupsHaveQuotes(groups) {
|
|
23
|
+
if (!(groups !== null && groups !== void 0 && groups.length)) return false;
|
|
24
|
+
return groups.some(function (group) {
|
|
25
|
+
return Array.isArray(group === null || group === void 0 ? void 0 : group.quotes) && group.quotes.length > 0;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* When checkout hydrates a saved address (localStorage) the form shows it,
|
|
30
|
+
* but quotes are only fetched after the user edits the field. Returning
|
|
31
|
+
* `apply-saved-address` means the cart still needs that address applied so
|
|
32
|
+
* quotes can be generated; `refresh-quotes` means the cart already has an
|
|
33
|
+
* address but no usable shipment options.
|
|
34
|
+
*/
|
|
35
|
+
function checkoutShipmentHydrationAction(options) {
|
|
36
|
+
var hasCartAddress = (0, _local_storage.addressHasContent)(options.cartAddress);
|
|
37
|
+
var hasSavedAddress = (0, _local_storage.addressHasContent)(options.savedAddress);
|
|
38
|
+
var hasQuotes = shipmentGroupsHaveQuotes(options.shipmentGroups);
|
|
39
|
+
if (!hasCartAddress && hasSavedAddress) return 'apply-saved-address';
|
|
40
|
+
if (hasCartAddress && !hasQuotes) return 'refresh-quotes';
|
|
41
|
+
return null;
|
|
18
42
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const TEST_CHECKOUT_QUERY = "merchi_test_checkout";
|
|
2
|
+
export declare const TEST_CHECKOUT_STORAGE_KEY = "merchi_test_checkout";
|
|
3
|
+
export declare const TEST_CARD_NUMBER = "4242 4242 4242 4242";
|
|
4
|
+
export declare function readTestCheckoutFlag(): boolean;
|
|
5
|
+
export declare function persistTestCheckoutFlag(): boolean;
|
|
6
|
+
export declare function cartCookieName(domainId: number | string, isTest?: boolean): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.TEST_CHECKOUT_STORAGE_KEY = exports.TEST_CHECKOUT_QUERY = exports.TEST_CARD_NUMBER = void 0;
|
|
7
|
+
exports.cartCookieName = cartCookieName;
|
|
8
|
+
exports.persistTestCheckoutFlag = persistTestCheckoutFlag;
|
|
9
|
+
exports.readTestCheckoutFlag = readTestCheckoutFlag;
|
|
10
|
+
var TEST_CHECKOUT_QUERY = exports.TEST_CHECKOUT_QUERY = 'merchi_test_checkout';
|
|
11
|
+
var TEST_CHECKOUT_STORAGE_KEY = exports.TEST_CHECKOUT_STORAGE_KEY = 'merchi_test_checkout';
|
|
12
|
+
var TEST_CARD_NUMBER = exports.TEST_CARD_NUMBER = '4242 4242 4242 4242';
|
|
13
|
+
function readTestCheckoutFlag() {
|
|
14
|
+
if (typeof window === 'undefined') {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
var params = new URLSearchParams(window.location.search);
|
|
19
|
+
if (params.get(TEST_CHECKOUT_QUERY) === '1') {
|
|
20
|
+
window.localStorage.setItem(TEST_CHECKOUT_STORAGE_KEY, '1');
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
return window.localStorage.getItem(TEST_CHECKOUT_STORAGE_KEY) === '1';
|
|
24
|
+
} catch (_unused) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function persistTestCheckoutFlag() {
|
|
29
|
+
return readTestCheckoutFlag();
|
|
30
|
+
}
|
|
31
|
+
function cartCookieName(domainId) {
|
|
32
|
+
var isTest = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
|
|
33
|
+
return isTest ? "cart-".concat(domainId, "-test") : "cart-".concat(domainId);
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@merchi/cart",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.6",
|
|
4
4
|
"description": "Merchi's cart",
|
|
5
5
|
"source": "src/index.ts",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"copy-assets": "copyfiles -u 1 src/assets/**/* esm && copyfiles -u 1 src/assets/**/* lib",
|
|
19
19
|
"compile-sass": "sass src/styles/globals.scss esm/styles/globals.css && sass src/styles/globals.scss lib/styles/globals.css",
|
|
20
20
|
"start": "react-scripts start",
|
|
21
|
-
"test": "
|
|
21
|
+
"test": "jest",
|
|
22
22
|
"eject": "react-scripts eject"
|
|
23
23
|
},
|
|
24
24
|
"repository": {
|
|
@@ -81,9 +81,12 @@
|
|
|
81
81
|
"copyfiles": "^2.4.1",
|
|
82
82
|
"eslint": "8.47.0",
|
|
83
83
|
"eslint-config-next": "13.4.16",
|
|
84
|
+
"jest": "28",
|
|
85
|
+
"jest-environment-jsdom": "28",
|
|
84
86
|
"react": "^18.0.0",
|
|
85
87
|
"react-dom": "^18.0.0",
|
|
86
88
|
"sass": "^1.70.0",
|
|
89
|
+
"ts-jest": "28",
|
|
87
90
|
"typescript": "^5.1.6"
|
|
88
91
|
},
|
|
89
92
|
"dependencies": {
|
|
@@ -95,9 +98,9 @@
|
|
|
95
98
|
"browser-or-node": "^3.0.0-pre.0",
|
|
96
99
|
"js-cookie": "^3.0.5",
|
|
97
100
|
"lodash": "^4.17.21",
|
|
98
|
-
"@merchi/product-form": "^1.19.
|
|
99
|
-
"@merchi/product-form-sdk": "^1.0.
|
|
100
|
-
"@merchi/sdk": "^1.
|
|
101
|
+
"@merchi/product-form": "^1.19.14",
|
|
102
|
+
"@merchi/product-form-sdk": "^1.0.11",
|
|
103
|
+
"@merchi/sdk": "^1.42.1",
|
|
101
104
|
"react-country-region-selector": "^3.6.1",
|
|
102
105
|
"react-hook-form": "7.45.4",
|
|
103
106
|
"react-icons": "^4.10.1",
|
package/src/CartProvider.tsx
CHANGED
|
@@ -20,6 +20,13 @@ import {
|
|
|
20
20
|
saveCheckoutAddress,
|
|
21
21
|
} from './utilities/local_storage';
|
|
22
22
|
import { makeUser } from './utilities/user';
|
|
23
|
+
import { persistTestCheckoutFlag, readTestCheckoutFlag } from './utilities/test_checkout';
|
|
24
|
+
import {
|
|
25
|
+
appendJobSourceToFormData,
|
|
26
|
+
captureAndReadJobSource,
|
|
27
|
+
cartNeedsSourceUpdate,
|
|
28
|
+
jobSourceFieldsForApi,
|
|
29
|
+
} from './utilities/job_source';
|
|
23
30
|
|
|
24
31
|
export interface PropsCart {
|
|
25
32
|
cart: any;
|
|
@@ -620,10 +627,22 @@ const CartProvider = ({
|
|
|
620
627
|
async function createCartAndCookie(callback?: () => void) {
|
|
621
628
|
setFetchingCart(true);
|
|
622
629
|
try {
|
|
623
|
-
const
|
|
624
|
-
|
|
630
|
+
const isTest = readTestCheckoutFlag();
|
|
631
|
+
const jobSource = jobSourceFieldsForApi(captureAndReadJobSource());
|
|
632
|
+
const cart = makeCart({domain: {id: domainId}, isTest}, true);
|
|
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);
|
|
625
644
|
let cartJson = await cart.toJson();
|
|
626
|
-
if (domainId) setCartCookie(Number(domainId), cartJson, undefined);
|
|
645
|
+
if (domainId) setCartCookie(Number(domainId), cartJson, undefined, isTest);
|
|
627
646
|
cartJson = await attachSavedClientIfNeeded(cartJson);
|
|
628
647
|
setCart({...cartJson});
|
|
629
648
|
if (callback) callback();
|
|
@@ -643,7 +662,25 @@ const CartProvider = ({
|
|
|
643
662
|
try {
|
|
644
663
|
const cart = await merchi.Cart.get(id, {embed: cartEmbed});
|
|
645
664
|
let cartJson = await cart.toJson();
|
|
646
|
-
|
|
665
|
+
const isTest = readTestCheckoutFlag();
|
|
666
|
+
if (Boolean(cartJson.isTest) !== isTest) {
|
|
667
|
+
await createCartAndCookie();
|
|
668
|
+
return;
|
|
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
|
+
}
|
|
683
|
+
if (stripeIsValidAndActive(cartJson) || isTest) {
|
|
647
684
|
cartJson = await attachSavedClientIfNeeded(cartJson);
|
|
648
685
|
setCart({...cartJson});
|
|
649
686
|
} else {
|
|
@@ -833,6 +870,7 @@ const CartProvider = ({
|
|
|
833
870
|
}
|
|
834
871
|
|
|
835
872
|
useEffect(() => {
|
|
873
|
+
persistTestCheckoutFlag();
|
|
836
874
|
if (domainId) actionGetMerchiCart();
|
|
837
875
|
if (domainId && includeTheme) actionFetchTheme();
|
|
838
876
|
}, [domainId]);
|
|
@@ -2,10 +2,13 @@ import { useForm } from "react-hook-form";
|
|
|
2
2
|
import InputsAddress from "./InputsAddress";
|
|
3
3
|
import { CheckoutContainer, InnerContainer } from "../components/containers";
|
|
4
4
|
import { Title } from "../components";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
checkoutShipmentHydrationAction,
|
|
7
|
+
shipmentFormId,
|
|
8
|
+
} from "../utilities/shipment";
|
|
6
9
|
import { faMapMarkerAlt } from "@fortawesome/free-solid-svg-icons";
|
|
7
10
|
import { useCartContext } from "../CartProvider";
|
|
8
|
-
import { useState } from "react";
|
|
11
|
+
import { useEffect, useRef, useState } from "react";
|
|
9
12
|
import { getCartCookieToken } from "../utilities/cookie";
|
|
10
13
|
import { makeAddress, sanitizeAddressFields } from "../utilities/address";
|
|
11
14
|
import { makeCart } from "../utilities/cart";
|
|
@@ -114,6 +117,8 @@ export function ActiveFormShipmentAddressAndNotes() {
|
|
|
114
117
|
setCart,
|
|
115
118
|
setLoadingTotals,
|
|
116
119
|
setActiveTabAndEditDisabled,
|
|
120
|
+
updateCartShipmentAddress,
|
|
121
|
+
getCartShipmentOptions,
|
|
117
122
|
} = useCartContext();
|
|
118
123
|
const saved = getSavedCheckoutAddress(domainId);
|
|
119
124
|
const cartAddress = cart?.receiverAddress;
|
|
@@ -122,6 +127,33 @@ export function ActiveFormShipmentAddressAndNotes() {
|
|
|
122
127
|
: saved.address || {};
|
|
123
128
|
const receiverNotes = cart?.receiverNotes || saved.receiverNotes || "";
|
|
124
129
|
const [addressFieldsOpen, setAddressFieldsOpen] = useState(false);
|
|
130
|
+
const hydratedShipmentOptionsRef = useRef(false);
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (hydratedShipmentOptionsRef.current || !cart?.id) return;
|
|
134
|
+
const draft = getSavedCheckoutAddress(domainId);
|
|
135
|
+
const action = checkoutShipmentHydrationAction({
|
|
136
|
+
cartAddress: cart?.receiverAddress,
|
|
137
|
+
savedAddress: draft.address,
|
|
138
|
+
shipmentGroups: cart?.shipmentGroups,
|
|
139
|
+
});
|
|
140
|
+
if (!action) return;
|
|
141
|
+
hydratedShipmentOptionsRef.current = true;
|
|
142
|
+
if (action === "apply-saved-address" && draft.address) {
|
|
143
|
+
updateCartShipmentAddress(draft.address);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (action === "refresh-quotes") {
|
|
147
|
+
getCartShipmentOptions?.();
|
|
148
|
+
}
|
|
149
|
+
}, [
|
|
150
|
+
cart?.id,
|
|
151
|
+
cart?.receiverAddress,
|
|
152
|
+
cart?.shipmentGroups,
|
|
153
|
+
domainId,
|
|
154
|
+
getCartShipmentOptions,
|
|
155
|
+
updateCartShipmentAddress,
|
|
156
|
+
]);
|
|
125
157
|
|
|
126
158
|
async function saveCartShipmentAddressAndGoToNextTab(values: any) {
|
|
127
159
|
const { receiverAddress: address, receiverNotes } = values;
|
|
@@ -3,6 +3,7 @@ import { useEffect, useState } from 'react';
|
|
|
3
3
|
import { ButtonPay } from '../buttons';
|
|
4
4
|
import FormStripeCardFields from './StripeCardFields';
|
|
5
5
|
import {
|
|
6
|
+
fetchTestPublishableKey,
|
|
6
7
|
stripeCardFormSubmit,
|
|
7
8
|
stripeInitPromise,
|
|
8
9
|
stripePaymentButtonSubmit,
|
|
@@ -28,6 +29,7 @@ function StripeCardForm() {
|
|
|
28
29
|
const { domain } = cart;
|
|
29
30
|
const [error, setError] = useState(({} as any));
|
|
30
31
|
const company = domain.company;
|
|
32
|
+
const isTestCheckout = Boolean(cart.isTest);
|
|
31
33
|
const hasCompanyPubKey = Boolean(company.isStripeValid && companyStripePubKeyOrTestPubKey(company));
|
|
32
34
|
const companyPubKey = hasCompanyPubKey ? companyStripePubKeyOrTestPubKey(company) : '';
|
|
33
35
|
const canUseConnect = !!company.stripeAccountId;
|
|
@@ -66,13 +68,17 @@ function StripeCardForm() {
|
|
|
66
68
|
setError({message: e.errorMessage || e.message || 'Unable to process card.'});
|
|
67
69
|
}
|
|
68
70
|
}
|
|
69
|
-
const [stripePublicKey, setStripePublicKey] = useState(companyPubKey);
|
|
71
|
+
const [stripePublicKey, setStripePublicKey] = useState(isTestCheckout ? '' : companyPubKey);
|
|
70
72
|
useEffect(() => {
|
|
73
|
+
if (isTestCheckout) {
|
|
74
|
+
fetchTestPublishableKey(url).then(setStripePublicKey).catch(() => setStripePublicKey(''));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
71
77
|
if (!stripePublicKey && canUseConnect) stripeInitPromise(url).then(setStripePublicKey)
|
|
72
|
-
}, [stripePublicKey, canUseConnect]);
|
|
78
|
+
}, [stripePublicKey, canUseConnect, isTestCheckout, url]);
|
|
73
79
|
return (
|
|
74
80
|
<>
|
|
75
|
-
{company.isTesting && badgeTestMode}
|
|
81
|
+
{!isTestCheckout && company.isTesting && badgeTestMode}
|
|
76
82
|
<FormStripeCardFields
|
|
77
83
|
doStripePayment={doStripePayment}
|
|
78
84
|
doStripePaymentRequestForButton={doStripePaymentRequestForButton}
|
|
@@ -1,18 +1,102 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
2
3
|
import { StripePaymentForm, StripePaymentGate } from '@merchi/invoice';
|
|
3
4
|
import LegacyStripeCardForm from './LegacyStripeCardForm';
|
|
5
|
+
import { completeTestPayment, fetchTestPublishableKey } from './actions';
|
|
4
6
|
import { useCartContext } from '../CartProvider';
|
|
5
7
|
import { tabIdPaymentSuccess } from '../utilities/tabs';
|
|
8
|
+
import { TEST_CARD_NUMBER } from '../utilities/test_checkout';
|
|
9
|
+
|
|
10
|
+
const testCheckoutHint = (
|
|
11
|
+
<div style={{marginBottom: '12px'}}>
|
|
12
|
+
<div style={{color: 'red', fontWeight: 600}}>Test checkout</div>
|
|
13
|
+
<p style={{margin: '4px 0 0', fontSize: '13px'}}>
|
|
14
|
+
No real charge. Use card {TEST_CARD_NUMBER}, any future expiry, any CVC,
|
|
15
|
+
any ZIP.
|
|
16
|
+
</p>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
6
19
|
|
|
7
20
|
export default function StripeCardForm() {
|
|
8
|
-
const {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
21
|
+
const {
|
|
22
|
+
alertError,
|
|
23
|
+
apiUrl,
|
|
24
|
+
cart,
|
|
25
|
+
classNameBtnPay,
|
|
26
|
+
setActiveTabIndex,
|
|
27
|
+
setInvoiceJson,
|
|
28
|
+
} = useCartContext();
|
|
29
|
+
const isTestCheckout = Boolean(cart.isTest);
|
|
30
|
+
const url = apiUrl || 'https://api.merchi.co/v6/';
|
|
31
|
+
const [testKeyMissing, setTestKeyMissing] = useState(false);
|
|
32
|
+
const [loadingTestPayment, setLoadingTestPayment] = useState(false);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!isTestCheckout) {
|
|
36
|
+
setTestKeyMissing(false);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
fetchTestPublishableKey(url)
|
|
40
|
+
.then(() => setTestKeyMissing(false))
|
|
41
|
+
.catch(() => setTestKeyMissing(true));
|
|
42
|
+
}, [isTestCheckout, url]);
|
|
43
|
+
|
|
44
|
+
async function doCompleteTestPayment() {
|
|
45
|
+
setLoadingTestPayment(true);
|
|
46
|
+
try {
|
|
47
|
+
const invoice = await completeTestPayment(url, cart);
|
|
14
48
|
setInvoiceJson(invoice);
|
|
15
49
|
setActiveTabIndex(tabIdPaymentSuccess);
|
|
16
|
-
}
|
|
17
|
-
|
|
50
|
+
} catch (e: any) {
|
|
51
|
+
alertError(e.message);
|
|
52
|
+
} finally {
|
|
53
|
+
setLoadingTestPayment(false);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (isTestCheckout && testKeyMissing) {
|
|
58
|
+
return (
|
|
59
|
+
<>
|
|
60
|
+
{testCheckoutHint}
|
|
61
|
+
<p style={{fontSize: '13px', marginBottom: '12px'}}>
|
|
62
|
+
Stripe test keys are not configured. Complete this as a test order
|
|
63
|
+
without a card.
|
|
64
|
+
</p>
|
|
65
|
+
<button
|
|
66
|
+
type='button'
|
|
67
|
+
className={classNameBtnPay}
|
|
68
|
+
disabled={loadingTestPayment}
|
|
69
|
+
onClick={doCompleteTestPayment}
|
|
70
|
+
>
|
|
71
|
+
{loadingTestPayment ? 'Completing…' : 'Complete test order'}
|
|
72
|
+
</button>
|
|
73
|
+
</>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<>
|
|
79
|
+
{isTestCheckout && testCheckoutHint}
|
|
80
|
+
<StripePaymentGate
|
|
81
|
+
engine={isTestCheckout ? 'legacy' : cart.stripePaymentEngine}
|
|
82
|
+
apiUrl={url}
|
|
83
|
+
resource="cart"
|
|
84
|
+
resourceId={cart.id}
|
|
85
|
+
resourceToken={cart.token || cart.cartToken}
|
|
86
|
+
legacy={<LegacyStripeCardForm />}
|
|
87
|
+
>
|
|
88
|
+
<StripePaymentForm
|
|
89
|
+
apiUrl={url}
|
|
90
|
+
resource="cart"
|
|
91
|
+
resourceId={cart.id}
|
|
92
|
+
resourceToken={cart.token || cart.cartToken}
|
|
93
|
+
onError={alertError}
|
|
94
|
+
onSuccess={invoice => {
|
|
95
|
+
setInvoiceJson(invoice);
|
|
96
|
+
setActiveTabIndex(tabIdPaymentSuccess);
|
|
97
|
+
}}
|
|
98
|
+
/>
|
|
99
|
+
</StripePaymentGate>
|
|
100
|
+
</>
|
|
101
|
+
);
|
|
18
102
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { completeTestPayment, fetchTestPublishableKey } from './actions';
|
|
2
|
+
|
|
3
|
+
describe('test checkout stripe actions', () => {
|
|
4
|
+
const originalFetch = global.fetch;
|
|
5
|
+
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
global.fetch = originalFetch;
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('loads the master test publishable key', async () => {
|
|
11
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
12
|
+
ok: true,
|
|
13
|
+
json: async () => ({ key: 'pk_test_master' }),
|
|
14
|
+
}) as any;
|
|
15
|
+
|
|
16
|
+
await expect(fetchTestPublishableKey('https://api.test/v6/')).resolves.toBe(
|
|
17
|
+
'pk_test_master'
|
|
18
|
+
);
|
|
19
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
20
|
+
'https://api.test/v6/stripe/test_publishable_key/',
|
|
21
|
+
{ method: 'GET', mode: 'cors' }
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('fails over when the test-key request is rejected', async () => {
|
|
26
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
27
|
+
ok: false,
|
|
28
|
+
json: async () => ({ message: 'missing' }),
|
|
29
|
+
}) as any;
|
|
30
|
+
|
|
31
|
+
await expect(fetchTestPublishableKey('https://api.test/v6/')).rejects.toThrow(
|
|
32
|
+
'Stripe test keys are not configured'
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('completes a no-Stripe test payment with the cart token', async () => {
|
|
37
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
38
|
+
ok: true,
|
|
39
|
+
json: async () => ({ invoice: { id: 9, isTest: true } }),
|
|
40
|
+
}) as any;
|
|
41
|
+
|
|
42
|
+
await expect(
|
|
43
|
+
completeTestPayment('https://api.test/v6/', { id: 4, token: 'tok' })
|
|
44
|
+
).resolves.toEqual({ id: 9, isTest: true });
|
|
45
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
46
|
+
'https://api.test/v6/carts/4/complete_test_payment/?cart_token=tok',
|
|
47
|
+
{ method: 'POST', mode: 'cors' }
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
});
|