@rebuy/rebuy 1.0.0-rc.3

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/README.md ADDED
@@ -0,0 +1 @@
1
+ # Rebuy
package/api.js ADDED
@@ -0,0 +1,59 @@
1
+ import { serialize } from './utilities.js';
2
+
3
+ const config = {
4
+ key: null,
5
+ domain: 'https://rebuyengine.com',
6
+ cdnDomain: 'https://cdn.rebuyengine.com',
7
+ eventDomain: 'https://rebuyengine.com',
8
+ };
9
+
10
+ const makeCall = async (method, path, data, origin) => {
11
+ const url = `${origin}${path}`;
12
+ const requestUrl = new URL(url);
13
+
14
+ const requestData = {
15
+ key: config.key,
16
+ };
17
+
18
+ if (typeof data == 'object' && data != null) {
19
+ Object.assign(requestData, data);
20
+ }
21
+
22
+ const requestObject = {
23
+ method,
24
+ };
25
+
26
+ if (method == 'GET') {
27
+ requestUrl.search = serialize(requestData);
28
+ } else if (method == 'POST') {
29
+ requestObject.headers = {
30
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
31
+ };
32
+ requestObject.body = new URLSearchParams(requestData);
33
+ }
34
+
35
+ const request = await fetch(requestUrl, requestObject);
36
+ return await request.json();
37
+ };
38
+
39
+ export default class Api {
40
+ constructor(options) {
41
+ if (typeof options == 'string') {
42
+ config.key = options;
43
+ } else if (typeof options == 'object' && options != null) {
44
+ Object.assign(config, options);
45
+ }
46
+ }
47
+
48
+ async callEvent(method, path, data) {
49
+ return await makeCall(method, path, data, config.eventDomain);
50
+ }
51
+
52
+ async callCdn(method, path, data) {
53
+ return await makeCall(method, path, data, config.cdnDomain);
54
+ }
55
+
56
+ async callApi(method, path, data) {
57
+ return await makeCall(method, path, data, config.domain);
58
+ }
59
+ }
package/client.js ADDED
@@ -0,0 +1,104 @@
1
+ import { convertProductToStorefrontFormat } from './utilities.js';
2
+ import Identity from './identity.js';
3
+ import Api from './api.js';
4
+
5
+ const config = {
6
+ key: null,
7
+ defaultParameters: null,
8
+ contextParameters: null,
9
+ api: null,
10
+ identity: null,
11
+ };
12
+
13
+ const trackEvent = async (eventData) => {
14
+ if (config.identity.visitorId()) {
15
+ eventData.uuid = config.identity.visitorId();
16
+ }
17
+
18
+ return await config.api.callEvent('POST', '/api/v1/analytics/event', eventData);
19
+ };
20
+
21
+ const makeCall = async (endpoint, params, format) => {
22
+ const query = {};
23
+
24
+ if (config.defaultParameters != null) {
25
+ Object.assign(query, config.defaultParameters);
26
+ }
27
+
28
+ if (config.contextParameters != null) {
29
+ Object.assign(query, config.contextParameters);
30
+ }
31
+
32
+ if (typeof params == 'object' && params != null) {
33
+ Object.assign(query, params);
34
+ }
35
+
36
+ if (config.identity.visitorId()) {
37
+ query.uuid = config.identity.visitorId();
38
+ }
39
+
40
+ const response = await config.api.callApi('GET', endpoint, query);
41
+
42
+ if (response.data && format == 'storefront') {
43
+ for (let i = 0; i < response.data.length; i++) {
44
+ response.data[i] = convertProductToStorefrontFormat(response.data[i]);
45
+ }
46
+ }
47
+
48
+ return response;
49
+ };
50
+
51
+ export default class RebuyClient {
52
+ constructor(key, defaultParameters) {
53
+ if (typeof key == 'string') {
54
+ config.key = key;
55
+ }
56
+
57
+ if (typeof defaultParameters == 'object' && defaultParameters != null) {
58
+ config.defaultParameters = defaultParameters;
59
+ }
60
+
61
+ config.api = new Api(config.key);
62
+ config.identity = new Identity();
63
+ }
64
+
65
+ setDefaultParameters(defaultParameters) {
66
+ if (typeof defaultParameters == 'object' && defaultParameters != null) {
67
+ config.defaultParameters = defaultParameters;
68
+ }
69
+ }
70
+
71
+ setContextParameters(contextParameters) {
72
+ if (typeof contextParameters == 'object' && contextParameters != null) {
73
+ config.contextParameters = contextParameters;
74
+ }
75
+ }
76
+
77
+ async getData(endpoint, params) {
78
+ return await makeCall(endpoint, params);
79
+ }
80
+
81
+ async getStorefrontData(endpoint, params) {
82
+ return await makeCall(endpoint, params, 'storefront');
83
+ }
84
+
85
+ async trackProductViewed(data) {
86
+ const requiredKeys = ['shopify_product_id', 'shopify_product_handle'];
87
+
88
+ const defaultData = {
89
+ subject: 'user',
90
+ verb: 'viewed',
91
+ noun: 'product',
92
+ };
93
+
94
+ if (typeof data != 'undefined' && data != null) {
95
+ const dataKeys = Object.keys(data);
96
+ if (dataKeys.some((key) => requiredKeys.includes(key))) {
97
+ const payload = Object.assign(data, defaultData);
98
+ return await trackEvent(payload);
99
+ }
100
+ }
101
+
102
+ return null;
103
+ }
104
+ }
package/cookie.js ADDED
@@ -0,0 +1,153 @@
1
+ import { dataToString, stringToData, isBase64Encoded } from './utilities.js';
2
+
3
+ export function get(name) {
4
+ if (typeof document == 'undefined') {
5
+ return null;
6
+ }
7
+
8
+ // Get cookie
9
+ const cookie = document.cookie.match('(^|;) ?' + decodeURIComponent(name) + '=([^;]*)(;|$)');
10
+
11
+ let value = null;
12
+
13
+ if (cookie != null) {
14
+ // Get data
15
+ const data = decodeURIComponent(cookie[2]);
16
+
17
+ // Auto decode
18
+ const decode = isBase64Encoded(data) ? true : false;
19
+
20
+ // Convert to data object
21
+ value = stringToData(data, decode);
22
+ }
23
+
24
+ return value;
25
+ }
26
+
27
+ export function getAll() {
28
+ const cookies = {};
29
+
30
+ if (document && document.cookie && document.cookie != '') {
31
+ const split = document.cookie.split(';');
32
+
33
+ for (let i = 0; i < split.length; i++) {
34
+ const pairs = split[i].split('=');
35
+
36
+ pairs[0] = pairs[0].replace(/^ /, '');
37
+
38
+ const key = decodeURIComponent(pairs[0]);
39
+ const value = decodeURIComponent(pairs[1]);
40
+
41
+ // Auto decode
42
+ const decode = isBase64Encoded(value) ? true : false;
43
+
44
+ cookies[key] = stringToData(value, decode);
45
+ }
46
+ }
47
+
48
+ return cookies;
49
+ }
50
+
51
+ export function set(name, value, config) {
52
+ if (typeof document == 'undefined') {
53
+ return null;
54
+ }
55
+
56
+ const attributes = ['path', 'domain', 'maxAge', 'expires', 'secure', 'sameSite'];
57
+ const convenienceTimes = ['seconds', 'minutes', 'hours', 'days', 'weeks', 'months', 'years'];
58
+
59
+ let cookieAttributes = {
60
+ path: '/',
61
+ };
62
+
63
+ if (typeof config != 'undefined' && Number.isInteger(config)) {
64
+ cookieAttributes['max-age'] = config;
65
+ } else if (typeof config === 'object' && config !== null) {
66
+ for (let key in config) {
67
+ if (attributes.includes(key)) {
68
+ if (key == 'maxAge') {
69
+ cookieAttributes['max-age'] = config[key];
70
+ } else if (key == 'sameSite') {
71
+ cookieAttributes['samesite'] = config[key];
72
+ } else if (key == 'expires') {
73
+ cookieAttributes[key] = new Date(config[key]).toGMTString();
74
+ } else {
75
+ cookieAttributes[key] = config[key];
76
+ }
77
+ } else if (convenienceTimes.includes(key)) {
78
+ let duration = config[key];
79
+
80
+ if (key == 'seconds') {
81
+ duration = duration * 1;
82
+ } else if (key == 'minutes') {
83
+ duration = duration * 60;
84
+ } else if (key == 'hours') {
85
+ duration = duration * 60 * 60;
86
+ } else if (key == 'days') {
87
+ duration = duration * 60 * 60 * 24;
88
+ } else if (key == 'weeks') {
89
+ duration = duration * 60 * 60 * 24 * 7;
90
+ } else if (key == 'months') {
91
+ duration = duration * 60 * 60 * 24 * 30;
92
+ } else if (key == 'years') {
93
+ duration = duration * 60 * 60 * 24 * 365;
94
+ }
95
+
96
+ cookieAttributes['max-age'] = duration;
97
+ }
98
+ }
99
+ }
100
+
101
+ // Convert data to string
102
+ value = dataToString(value, config.encode);
103
+
104
+ // Define cookie
105
+ let cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value);
106
+
107
+ // Add optional cookie attributes
108
+ for (let key in cookieAttributes) {
109
+ cookie += ';' + key + '=' + cookieAttributes[key];
110
+ }
111
+
112
+ // Write cookie
113
+ document.cookie = cookie;
114
+ }
115
+
116
+ export function find(name) {
117
+ const matches = [];
118
+ const cookies = getAll();
119
+
120
+ for (let key in cookies) {
121
+ if (key.includes(name)) {
122
+ matches.push({
123
+ name: key,
124
+ value: cookies[key],
125
+ });
126
+ }
127
+ }
128
+
129
+ return matches;
130
+ }
131
+
132
+ export function destroy(name) {
133
+ set(name, '', { seconds: 0 });
134
+ }
135
+
136
+ export function enabled() {
137
+ const test = {
138
+ key: '__cookie_test',
139
+ value: 1,
140
+ };
141
+
142
+ set(test.key, test.value);
143
+
144
+ const enabled = get(test.key) == test.value ? true : false;
145
+
146
+ if (enabled) {
147
+ destroy(test.key);
148
+ }
149
+
150
+ return enabled;
151
+ }
152
+
153
+ export default { get, set, getAll, find, destroy, enabled };
package/geolocation.js ADDED
@@ -0,0 +1,57 @@
1
+ import Cookie from './cookie.js';
2
+ import Api from './api.js';
3
+
4
+ const config = {
5
+ key: null,
6
+ geolocation: null,
7
+ geolocationCookie: '_rebuyGeolocation',
8
+ geolocationDuration: {
9
+ minutes: 30,
10
+ },
11
+ };
12
+
13
+ const getGeolocation = async () => {
14
+ const api = new Api(config.key);
15
+ const response = await api.callApi('GET', '/api/v1/customers/geolocation');
16
+
17
+ if (response.data) {
18
+ // Update config with geolocation
19
+ config.geolocation = response.data;
20
+
21
+ // Write cookie with geolocation
22
+ const cookieOptions = {
23
+ secure: true,
24
+ };
25
+
26
+ // Merge cookie options with geolocation config
27
+ Object.assign(cookieOptions, config.geolocationDuration);
28
+
29
+ // Write cookie with geolocation
30
+ Cookie.set(config.geolocationCookie, config.geolocation, cookieOptions);
31
+ }
32
+
33
+ return config.geolocation;
34
+ };
35
+
36
+ export default class Geolocation {
37
+ constructor(key) {
38
+ if (typeof key == 'string') {
39
+ config.key = key;
40
+ }
41
+
42
+ config.geolocation = Cookie.get(config.geolocationCookie);
43
+
44
+ // Create a new geolocation (if needed)
45
+ if (config.geolocation === null) {
46
+ getGeolocation();
47
+ }
48
+ }
49
+
50
+ async geolocation() {
51
+ if (config.geolocation == null) {
52
+ await getGeolocation();
53
+ }
54
+
55
+ return config.geolocation;
56
+ }
57
+ }
package/identity.js ADDED
@@ -0,0 +1,66 @@
1
+ import { uuid } from './utilities.js';
2
+ import Session from './session.js';
3
+ import Cookie from './cookie.js';
4
+ import Geolocation from './geolocation.js';
5
+
6
+ const config = {
7
+ key: null,
8
+ visitorId: null,
9
+ visitorIdCookie: '_rebuyVisitorId',
10
+ visitorDuration: {
11
+ years: 1,
12
+ },
13
+ session: null,
14
+ };
15
+
16
+ export default class Identity {
17
+ constructor(key) {
18
+ if (typeof key == 'string') {
19
+ config.key = key;
20
+ }
21
+
22
+ config.visitorId = Cookie.get(config.visitorIdCookie);
23
+
24
+ // Create a new identifier (if needed)
25
+ if (config.visitorId === null) {
26
+ config.visitorId = uuid();
27
+ }
28
+
29
+ // Write cookie with visitor ID
30
+ const cookieOptions = {
31
+ secure: true,
32
+ };
33
+
34
+ // Merge cookie options with visitor config
35
+ Object.assign(cookieOptions, config.visitorDuration);
36
+
37
+ // Write cookie with session ID
38
+ Cookie.set(config.visitorIdCookie, config.visitorId, cookieOptions);
39
+
40
+ // Create visitor session
41
+ config.session = new Session();
42
+
43
+ // Create visitor geolocation
44
+ config.geolocation = new Geolocation(config.key);
45
+ }
46
+
47
+ visitorId() {
48
+ return config.visitorId;
49
+ }
50
+
51
+ sessionId() {
52
+ return config.session.sessionId();
53
+ }
54
+
55
+ sessionStart() {
56
+ return config.session.sessionStart();
57
+ }
58
+
59
+ sessionDuration() {
60
+ return config.session.sessionDuration();
61
+ }
62
+
63
+ async geolocation() {
64
+ return await config.geolocation.geolocation();
65
+ }
66
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@rebuy/rebuy",
3
+ "description": "This is the default library for Rebuy",
4
+ "version": "1.0.0-rc.3",
5
+ "license": "MIT",
6
+ "author": "Rebuy, Inc.",
7
+ "type": "module",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "files": [
12
+ "**/*.js"
13
+ ],
14
+ "scripts": {
15
+ "lint": "eslint .",
16
+ "test": "echo \"Error: no test specified\" && exit 1"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+ssh://git@bitbucket.org/rebuyengine/npm-rebuy.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://bitbucket.org/rebuyengine/npm-rebuy/issues"
24
+ },
25
+ "homepage": "https://bitbucket.org/rebuyengine/npm-rebuy#readme",
26
+ "devDependencies": {
27
+ "@semantic-release/changelog": "^6.0.1",
28
+ "@semantic-release/commit-analyzer": "^9.0.2",
29
+ "@semantic-release/git": "^10.0.1",
30
+ "@semantic-release/npm": "^9.0.1",
31
+ "@semantic-release/release-notes-generator": "^10.0.3",
32
+ "conventional-changelog-eslint": "^3.0.9",
33
+ "eslint": "^8.17.0",
34
+ "semantic-release": "^19.0.3"
35
+ }
36
+ }
package/session.js ADDED
@@ -0,0 +1,46 @@
1
+ import { sessionId } from './utilities.js';
2
+ import Cookie from './cookie.js';
3
+
4
+ const config = {
5
+ now: null,
6
+ sessionId: null,
7
+ sessionIdCookie: '_rebuySessionId',
8
+ sessionDuration: {
9
+ minutes: 30,
10
+ },
11
+ };
12
+
13
+ export default class Session {
14
+ constructor() {
15
+ config.now = new Date().getTime();
16
+ config.sessionId = Cookie.get(config.sessionIdCookie);
17
+
18
+ // Create a new session (if needed)
19
+ if (config.sessionId === null) {
20
+ config.sessionId = sessionId();
21
+ }
22
+
23
+ // Write cookie with session ID
24
+ const cookieOptions = {
25
+ secure: true,
26
+ };
27
+
28
+ // Merge cookie options with session config
29
+ Object.assign(cookieOptions, config.sessionDuration);
30
+
31
+ // Write cookie with session ID
32
+ Cookie.set(config.sessionIdCookie, config.sessionId, cookieOptions);
33
+ }
34
+
35
+ sessionId() {
36
+ return config.sessionId;
37
+ }
38
+
39
+ sessionStart() {
40
+ return Number(config.sessionId.split('.')[1]);
41
+ }
42
+
43
+ sessionDuration() {
44
+ return parseInt((config.now - this.sessionStart()) / 1000 / 60);
45
+ }
46
+ }
package/utilities.js ADDED
@@ -0,0 +1,341 @@
1
+ export function isBase64Encoded(str) {
2
+ const base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/;
3
+ return base64Regex.test(str) ? true : false;
4
+ }
5
+
6
+ export function stringToData(data, decode) {
7
+ let response = data;
8
+
9
+ // Base64 decode
10
+ if (decode == true) {
11
+ response = atob(response);
12
+ }
13
+
14
+ try {
15
+ response = JSON.parse(response);
16
+ } catch (e) {
17
+ // failure to parse cookie
18
+ }
19
+
20
+ return response;
21
+ }
22
+
23
+ export function dataToString(data, encode) {
24
+ let response = data;
25
+
26
+ if (typeof response != 'string') {
27
+ response = JSON.stringify(response);
28
+ }
29
+
30
+ // Base64 encode
31
+ if (encode == true) {
32
+ response = btoa(response);
33
+ }
34
+
35
+ return response;
36
+ }
37
+
38
+ export function stripHtml(str) {
39
+ return str.replace(/<(.|\n)*?>/g, '');
40
+ }
41
+
42
+ export function getIdFromGraphUrl(graphId, objectType) {
43
+ let id = null;
44
+
45
+ const regex = new RegExp(`gid://shopify/${objectType}/(.*)`);
46
+ const matches = graphId.match(regex);
47
+
48
+ if (matches != null) {
49
+ id = matches[1];
50
+
51
+ if (!isNaN(id)) {
52
+ id = Number(id);
53
+ }
54
+ }
55
+
56
+ return id;
57
+ }
58
+
59
+ export function variantAvailable(variant) {
60
+ return !(variant.inventory_management && variant.inventory_policy == 'deny' && variant.inventory_quantity <= 0);
61
+ }
62
+
63
+ export function firstAvailableVariant(product) {
64
+ // Select first variant
65
+ let selectedVariant = product.variants[0];
66
+
67
+ // Upgrade to first available
68
+ for (let i = 0; i < product.variants.length; i++) {
69
+ if (variantAvailable(product.variants[i])) {
70
+ selectedVariant = product.variants[i];
71
+ break;
72
+ }
73
+ }
74
+
75
+ return selectedVariant;
76
+ }
77
+
78
+ export function uuid() {
79
+ let d = new Date().getTime();
80
+ let d2 = (performance && performance.now && performance.now() * 1000) || 0;
81
+
82
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
83
+ let r = Math.random() * 16;
84
+ if (d > 0) {
85
+ r = (d + r) % 16 | 0;
86
+ d = Math.floor(d / 16);
87
+ } else {
88
+ r = (d2 + r) % 16 | 0;
89
+ d2 = Math.floor(d2 / 16);
90
+ }
91
+ return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
92
+ });
93
+ }
94
+
95
+ export function sessionId() {
96
+ const config = {
97
+ chars: '0123456789',
98
+ length: 12,
99
+ };
100
+
101
+ let sessionId = '';
102
+ let sessionStart = new Date().getTime();
103
+
104
+ for (let i = config.length; i > 0; i--) {
105
+ sessionId += config.chars[Math.floor(Math.random() * config.chars.length)];
106
+ }
107
+
108
+ return `REBUY.${sessionStart}.${sessionId}`;
109
+ }
110
+
111
+ export function convertProductToStorefrontFormat(product) {
112
+ let defaultVariant = firstAvailableVariant(product);
113
+ let minVariantPrice = variantMinMaxPriceObject(product, 'price', 'min');
114
+ let maxVariantPrice = variantMinMaxPriceObject(product, 'price', 'max');
115
+ let minVariantCompareAtPrice = variantMinMaxPriceObject(product, 'compare_at_price', 'min');
116
+ let maxVariantCompareAtPrice = variantMinMaxPriceObject(product, 'compare_at_price', 'max');
117
+ let selectedOptions = selectedVariantOptions(product, defaultVariant);
118
+
119
+ let variants = product.variants.map((variant) => convertVariantToStorefrontFormat(product, variant));
120
+
121
+ return {
122
+ id: `gid://shopify/Product/${product.id}`,
123
+ title: product.title,
124
+ handle: product.handle,
125
+ description: stripHtml(product.body_html),
126
+ descriptionHtml: product.body_html,
127
+ vendor: product.vendor,
128
+ featuredImage: productImageObject(product),
129
+ options: [
130
+ ...product.options.map((option) => {
131
+ return { name: option.name, values: option.values };
132
+ }),
133
+ ],
134
+ selectedOptions,
135
+ priceRange: {
136
+ minVariantPrice,
137
+ maxVariantPrice,
138
+ },
139
+ compareAtPriceRange: {
140
+ minVariantCompareAtPrice,
141
+ maxVariantCompareAtPrice,
142
+ },
143
+ variants: convertToNodes(variants),
144
+ images: null,
145
+ media: [],
146
+ metafields: [],
147
+ collections: null,
148
+ selectedSellingPlan: null,
149
+ selectedSellingPlanAllocation: null,
150
+ sellingPlanGroups: [],
151
+ seo: { title: null, description: null },
152
+ };
153
+ }
154
+
155
+ export function convertToNodes(arr) {
156
+ return {
157
+ edges: [
158
+ ...arr.map((node) => {
159
+ return { node };
160
+ }),
161
+ ],
162
+ };
163
+ }
164
+
165
+ export function convertVariantToStorefrontFormat(product, variant) {
166
+ let selectedOptions = selectedVariantOptions(product, variant);
167
+ let image = productImageObject(product, variant.image_id);
168
+
169
+ return {
170
+ id: `gid://shopify/ProductVariant/${variant.id}`,
171
+ availableForSale: variantAvailable(variant),
172
+ priceV2: variantPriceObject(variant, 'price'),
173
+ compareAtPriceV2: variantPriceObject(variant, 'compare_at_price'),
174
+ image: image,
175
+ selectedOptions: [selectedOptions],
176
+ sku: variant.sku,
177
+ title: variant.title,
178
+ };
179
+ }
180
+
181
+ export function variantMinMaxPriceObject(product, key = 'price', operator = 'min') {
182
+ let priceObject = variantPriceObject(product.variants[0], key);
183
+
184
+ for (let i = 0; i < product.variants.length; i++) {
185
+ const variantPrice = variantPriceObject(product.variants[i], key);
186
+
187
+ if (variantPrice != null) {
188
+ const variantPriceAmount = Number(variantPrice.amount);
189
+ const currentPriceAmount = priceObject != null ? Number(priceObject.amount) : null;
190
+
191
+ if (
192
+ currentPriceAmount == null ||
193
+ (operator == 'min' && variantPriceAmount < currentPriceAmount) ||
194
+ (operator == 'max' && variantPriceAmount > currentPriceAmount)
195
+ ) {
196
+ priceObject = variantPrice;
197
+ }
198
+ }
199
+ }
200
+
201
+ return priceObject;
202
+ }
203
+
204
+ export function variantPriceObject(variant, key = 'price') {
205
+ if (variant[key] != null) {
206
+ return {
207
+ amount: variant[key],
208
+ currencyCode: 'USD',
209
+ };
210
+ } else {
211
+ return null;
212
+ }
213
+ }
214
+
215
+ export function selectedVariantOptions(product, variant) {
216
+ let selectedOptions = {};
217
+
218
+ if (variant.option1 != null) {
219
+ selectedOptions[product.options[0].name] = variant.option1;
220
+ }
221
+
222
+ if (variant.option2 != null) {
223
+ selectedOptions[product.options[1].name] = variant.option2;
224
+ }
225
+
226
+ if (variant.option3 != null) {
227
+ selectedOptions[product.options[2].name] = variant.option3;
228
+ }
229
+
230
+ return selectedOptions;
231
+ }
232
+
233
+ export function productImageObject(product, id) {
234
+ let image = null;
235
+
236
+ if (product.image) {
237
+ image = {
238
+ id: `gid://shopify/ProductImage/${product.image.id}`,
239
+ url: product.image.src,
240
+ altText: product.image.alt,
241
+ width: product.image.width,
242
+ height: product.image.height,
243
+ };
244
+ }
245
+
246
+ if (product.images && product.images.length > 0 && id != null) {
247
+ const matchingImage = product.images.find((i) => i.id == id);
248
+
249
+ if (matchingImage) {
250
+ image = {
251
+ id: `gid://shopify/ProductImage/${matchingImage.id}`,
252
+ url: matchingImage.src,
253
+ altText: matchingImage.alt,
254
+ width: matchingImage.width,
255
+ height: matchingImage.height,
256
+ };
257
+ }
258
+ }
259
+
260
+ return image;
261
+ }
262
+
263
+ export function queryStringToObject(str) {
264
+ const params = new URLSearchParams(str);
265
+ return Object.fromEntries(params.entries());
266
+ }
267
+
268
+ export function utmObjectFromString(str) {
269
+ const utmKeys = ['utm_campaign', 'utm_medium', 'utm_source', 'utm_term', 'utm_content'];
270
+
271
+ const matches = {};
272
+
273
+ const url = new URL(str);
274
+ const queryObject = queryStringToObject(url.search);
275
+
276
+ for (const [key, value] of Object.entries(queryObject)) {
277
+ if (utmKeys.includes(key)) {
278
+ matches[key] = value;
279
+ }
280
+ }
281
+
282
+ return Object.keys(matches).length > 0 ? matches : null;
283
+ }
284
+
285
+ export function amountToCents(amount) {
286
+ if (isNaN(amount)) {
287
+ amount = 0;
288
+ }
289
+
290
+ if (typeof amount != 'string') {
291
+ amount = amount.toString();
292
+ }
293
+
294
+ if (amount.indexOf('.') != -1) {
295
+ amount = parseFloat(amount).toFixed(2) * 100;
296
+ } else {
297
+ amount = parseInt(amount);
298
+ }
299
+
300
+ return parseInt(amount);
301
+ }
302
+
303
+ export function serialize(obj) {
304
+ const serialized = [];
305
+
306
+ const add = (key, value) => {
307
+ value = typeof value === 'function' ? value() : value;
308
+ value = value === null ? '' : value === undefined ? '' : value;
309
+ serialized[serialized.length] = encodeURIComponent(key) + '=' + encodeURIComponent(value);
310
+ };
311
+
312
+ const buildParameters = (prefix, obj) => {
313
+ let i, len, key;
314
+
315
+ if (prefix) {
316
+ if (Array.isArray(obj)) {
317
+ for (i = 0, len = obj.length; i < len; i++) {
318
+ buildParameters(prefix + '[' + (typeof obj[i] === 'object' && obj[i] ? i : '') + ']', obj[i]);
319
+ }
320
+ } else if (Object.prototype.toString.call(obj) === '[object Object]') {
321
+ for (key in obj) {
322
+ buildParameters(prefix + '[' + key + ']', obj[key]);
323
+ }
324
+ } else {
325
+ add(prefix, obj);
326
+ }
327
+ } else if (Array.isArray(obj)) {
328
+ for (i = 0, len = obj.length; i < len; i++) {
329
+ add(obj[i].name, obj[i].value);
330
+ }
331
+ } else {
332
+ for (key in obj) {
333
+ buildParameters(key, obj[key]);
334
+ }
335
+ }
336
+
337
+ return serialized;
338
+ };
339
+
340
+ return buildParameters('', obj).join('&');
341
+ }