@ordergroove/offers 2.26.9 → 2.27.0
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/CHANGELOG.md +39 -0
- package/README.md +34 -0
- package/build.js +3 -1
- package/dist/bundle-report.html +186 -117
- package/dist/examples.js +18 -3
- package/dist/examples.js.map +1 -1
- package/dist/offers.js +65 -76
- package/dist/offers.js.map +3 -3
- package/examples/cart.js +105 -0
- package/examples/index.html +2 -2
- package/examples/products/cheap-watch.js +183 -0
- package/examples/shopify-cart.html +26 -0
- package/examples/shopify-pdp.html +34 -0
- package/karma.conf.js +2 -1
- package/package.json +5 -5
- package/src/__tests__/offers.spec.js +35 -10
- package/src/components/FrequencyStatus.js +14 -11
- package/src/components/IncentiveText.js +2 -1
- package/src/components/Offer.js +14 -7
- package/src/components/OptinButton.js +1 -1
- package/src/components/OptinSelect.js +2 -2
- package/src/components/OptinToggle.js +2 -2
- package/src/components/OptoutButton.js +1 -1
- package/src/components/Price.js +8 -4
- package/src/components/Select.js +3 -13
- package/src/components/SelectFrequency.js +24 -6
- package/src/components/TestWizard.js +1 -1
- package/src/components/__tests__/OG.fspec.js +24 -0
- package/src/components/__tests__/Offer.spec.js +4 -4
- package/src/components/__tests__/OptinButton.spec.js +2 -2
- package/src/components/__tests__/OptinToggle.spec.js +2 -2
- package/src/components/__tests__/OptoutButton.spec.js +1 -1
- package/src/components/__tests__/SelectFrequency.fspec.js +1 -0
- package/src/components/__tests__/SelectFrequency.spec.js +1 -1
- package/src/components/__tests__/TestWizard.spec.js +2 -2
- package/src/components/__tests__/Text.spec.js +5 -1
- package/src/core/__tests__/actions.spec.js +6 -6
- package/src/core/actions.js +22 -17
- package/src/core/constants.js +21 -0
- package/src/core/descriptors.js +2 -1
- package/src/core/middleware.js +41 -1
- package/src/core/reducer.js +22 -21
- package/src/core/resolveProperties.js +2 -7
- package/src/core/selectors.js +1 -1
- package/src/core/store.js +17 -9
- package/src/core/utils.ts +67 -0
- package/src/index.js +46 -203
- package/src/make-api.js +195 -0
- package/src/platform.ts +9 -0
- package/src/shopify/__tests__/shopifyMiddleware.spec.js +126 -0
- package/src/shopify/__tests__/shopifyReducer.spec.js +489 -0
- package/src/shopify/shopifyBootstrap.ts +136 -0
- package/src/shopify/shopifyMiddleware.ts +336 -0
- package/src/shopify/shopifyReducer.js +254 -0
- package/tsconfig.json +35 -0
- package/examples/5starnutrition-main.js +0 -3
- package/examples/single-offer.html +0 -9
- package/src/init-test.js +0 -3
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { authorize } from '../core/actions';
|
|
2
|
+
import { clearCookie, getCookieValue, getMainJs, resolveEnvAndMerchant } from '../core/utils';
|
|
3
|
+
|
|
4
|
+
const SHOPIFY_OG_AUTH_ENDPOINT = '/apps/subscriptions/auth/';
|
|
5
|
+
const SHOPIFY_OG_AUTH_BEGIN = 'og_auth_begin';
|
|
6
|
+
const SHOPIFY_OG_AUTH_END = 'og_auth_end';
|
|
7
|
+
|
|
8
|
+
type ShopifyOGAuth = {
|
|
9
|
+
customerId: string;
|
|
10
|
+
timestamp: Number;
|
|
11
|
+
signature: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* We tag shopify liquid this way.
|
|
16
|
+
* window.ogShopifyConfig = {
|
|
17
|
+
* {%- if customer -%}
|
|
18
|
+
* customer: {
|
|
19
|
+
* id: "{{customer.id}}",
|
|
20
|
+
* email: "{{customer.email}}",
|
|
21
|
+
* signature: "{{signature}}",
|
|
22
|
+
* timestamp: "{{timestamp}}",
|
|
23
|
+
* },
|
|
24
|
+
* {%- else -%}
|
|
25
|
+
* customer: null,
|
|
26
|
+
* {%- endif -%}
|
|
27
|
+
*/
|
|
28
|
+
type OgShopifyConfigCustomer = {
|
|
29
|
+
id: string;
|
|
30
|
+
signature: string;
|
|
31
|
+
timestamp: string;
|
|
32
|
+
email?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type OgShopifyConfig = {
|
|
36
|
+
customer?: OgShopifyConfigCustomer;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* We rely on window.ogShopifyConfig side of integration
|
|
41
|
+
*/
|
|
42
|
+
declare global {
|
|
43
|
+
interface Window {
|
|
44
|
+
og: {
|
|
45
|
+
previewMode: boolean;
|
|
46
|
+
};
|
|
47
|
+
ogShopifyConfig: OgShopifyConfig;
|
|
48
|
+
Shopify: { routes?: { root: String } };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const parseIntegrationTempAuth = (raw: string) => {
|
|
53
|
+
const [id, timestamp, signature, email] = atob(raw).split('|');
|
|
54
|
+
return {
|
|
55
|
+
id,
|
|
56
|
+
signature,
|
|
57
|
+
timestamp,
|
|
58
|
+
email
|
|
59
|
+
} as OgShopifyConfigCustomer;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
*
|
|
63
|
+
* Markup needed for integration:
|
|
64
|
+
* ```html
|
|
65
|
+
* <script src="https://static.ordergroove.com/<merchant_id>/main.js" {% if customer -%}
|
|
66
|
+
* {%- assign secret_key = shop.metafields.accentuate.theme_hash_key -%}
|
|
67
|
+
* {%- assign timestamp = "now" | date: "%s" -%}
|
|
68
|
+
* {%- assign signature = customer.id | append: "|" | append: timestamp | hmac_sha256: secret_key -%}
|
|
69
|
+
* data-customer="{{customer.id | append: "|" | append: timestamp | append: "|" | append: signature | append: "|" | append: customer.email | base64_encode }}"
|
|
70
|
+
* {%- endif %}></script>
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export async function authorizeShopifyCustomer({ store }) {
|
|
74
|
+
const [merchantId] = resolveEnvAndMerchant();
|
|
75
|
+
const script = getMainJs();
|
|
76
|
+
|
|
77
|
+
let customer = script?.dataset.customer
|
|
78
|
+
? parseIntegrationTempAuth(script.dataset.customer)
|
|
79
|
+
: window.ogShopifyConfig?.customer;
|
|
80
|
+
if (customer) {
|
|
81
|
+
const val = await getOrCreateAuthCookie(customer);
|
|
82
|
+
if (val) {
|
|
83
|
+
const [sig_field, ts, sig] = val.split('|');
|
|
84
|
+
store.dispatch(authorize(merchantId, sig_field, Number(ts), sig));
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
clearCookie('og_auth');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Borrow from here https://github.com/ordergroove/shopify-app/blob/88becb621b29776a946ab0cd3ae215043e174626/server/lib/theme-partials/js/helpers/cookies.js#L18
|
|
92
|
+
* @param customer
|
|
93
|
+
* @returns
|
|
94
|
+
*/
|
|
95
|
+
export async function fetchOGSignature(customer: OgShopifyConfigCustomer) {
|
|
96
|
+
try {
|
|
97
|
+
const response = await fetch(
|
|
98
|
+
`${SHOPIFY_OG_AUTH_ENDPOINT}?customer=${customer.id}&customer_signature=${customer.signature}&customer_timestamp=${customer.timestamp}`
|
|
99
|
+
);
|
|
100
|
+
const data = await response.text();
|
|
101
|
+
const beginningIndex = data.lastIndexOf(SHOPIFY_OG_AUTH_BEGIN);
|
|
102
|
+
|
|
103
|
+
if (beginningIndex < 0) throw 'Invalid response from OG auth endpoint';
|
|
104
|
+
|
|
105
|
+
return JSON.parse(
|
|
106
|
+
data.substring(beginningIndex + SHOPIFY_OG_AUTH_BEGIN.length, data.lastIndexOf(SHOPIFY_OG_AUTH_END))
|
|
107
|
+
) as ShopifyOGAuth;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error(err);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Original source https://github.com/ordergroove/shopify-app/blob/88becb621b29776a946ab0cd3ae215043e174626/server/lib/theme-partials/js/helpers/cookies.js#L56
|
|
114
|
+
*
|
|
115
|
+
* @param customer
|
|
116
|
+
* @returns
|
|
117
|
+
*/
|
|
118
|
+
export async function getOrCreateAuthCookie(customer: OgShopifyConfigCustomer) {
|
|
119
|
+
const ogAuthCookie = getCookieValue('og_auth');
|
|
120
|
+
|
|
121
|
+
// The cookie hasn't expired yet so we don't need to refresh the auth
|
|
122
|
+
if (ogAuthCookie) {
|
|
123
|
+
return ogAuthCookie;
|
|
124
|
+
}
|
|
125
|
+
const { customerId, timestamp, signature } = await fetchOGSignature(customer);
|
|
126
|
+
|
|
127
|
+
if (!customerId) return '';
|
|
128
|
+
|
|
129
|
+
// set expiration to now + 2hrs
|
|
130
|
+
const ogToday = new Date();
|
|
131
|
+
const binarySignature = btoa(signature);
|
|
132
|
+
ogToday.setTime(ogToday.getTime() + 2 * 60 * 60 * 1000);
|
|
133
|
+
const value = `${customerId}|${timestamp}|${binarySignature};expires=${ogToday.toUTCString()}`;
|
|
134
|
+
document.cookie = `og_auth=${value};secure;path=/`;
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import memoize from 'lodash.memoize';
|
|
2
|
+
import { debounce } from 'throttle-debounce';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
CART_UPDATED_EVENT,
|
|
6
|
+
OPTIN_PRODUCT,
|
|
7
|
+
OPTOUT_PRODUCT,
|
|
8
|
+
PRODUCT_CHANGE_FREQUENCY,
|
|
9
|
+
RECEIVE_OFFER,
|
|
10
|
+
REQUEST_OFFER,
|
|
11
|
+
SETUP_CART,
|
|
12
|
+
SETUP_PRODUCT
|
|
13
|
+
} from '../core/constants';
|
|
14
|
+
|
|
15
|
+
import { makeSubscribedSelector } from '../core/selectors';
|
|
16
|
+
import { safeProductId } from '../core/utils';
|
|
17
|
+
|
|
18
|
+
const SHOPIFY_ROOT = window.Shopify?.routes?.root || '/';
|
|
19
|
+
const CART_PAGE_URL = '/cart';
|
|
20
|
+
const CART_JS_URL = `${SHOPIFY_ROOT}cart.js`;
|
|
21
|
+
const PRODUCTS_URL = `${SHOPIFY_ROOT}products/`;
|
|
22
|
+
|
|
23
|
+
const syncProductId = debounce(100, false, function(form, offer) {
|
|
24
|
+
const { id } = Object.fromEntries([...new FormData(form).entries()]);
|
|
25
|
+
offer.setAttribute('product', id);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
async function setupPdp(store, offer) {
|
|
29
|
+
const handle = guessProductHandle();
|
|
30
|
+
if (handle) {
|
|
31
|
+
try {
|
|
32
|
+
store.dispatch({ type: SETUP_PRODUCT, payload: await getProduct(handle) });
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.warn('OG: Unable to fetch product details for PDP', err);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const form = offer.closest('form');
|
|
39
|
+
|
|
40
|
+
// keep product.id form in sync with offer serving.
|
|
41
|
+
new MutationObserver(() => syncProductId(form, offer)).observe(form, { subtree: true, childList: true });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const getCart = async () => await (await fetch(CART_JS_URL)).json();
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Attemps to guess the product handle o
|
|
48
|
+
* @returns
|
|
49
|
+
*/
|
|
50
|
+
function guessProductHandle(): String {
|
|
51
|
+
return (
|
|
52
|
+
[
|
|
53
|
+
() =>
|
|
54
|
+
// Use the oembed to get the product handle
|
|
55
|
+
(document
|
|
56
|
+
.querySelector('[href$=".oembed"]')
|
|
57
|
+
?.getAttribute('href')
|
|
58
|
+
?.match(/\/([^\/]+)\.oembed$/) || [])[1],
|
|
59
|
+
|
|
60
|
+
() =>
|
|
61
|
+
// Use the open graph og:type==product and og:url to get the product handle
|
|
62
|
+
((document.querySelector('meta[property="og:type"][content="product"]') &&
|
|
63
|
+
document
|
|
64
|
+
.querySelector('meta[property="og:url"][content]')
|
|
65
|
+
?.getAttribute('content')
|
|
66
|
+
?.match(/\/([^\/]+)$/)) ||
|
|
67
|
+
[])[1],
|
|
68
|
+
|
|
69
|
+
() =>
|
|
70
|
+
// use any json in the markup
|
|
71
|
+
[...document.querySelectorAll('[type$=json]')]
|
|
72
|
+
.map(it => JSON.parse(it.textContent || '{}'))
|
|
73
|
+
.find(it => it.handle && it.price)?.handle
|
|
74
|
+
]
|
|
75
|
+
// returns the first truthy and prevent call next functions
|
|
76
|
+
.reduce((acc, cur) => acc || cur(), '')
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const getProduct = memoize(async handle => (await fetch(`${PRODUCTS_URL}${handle}.js`)).json());
|
|
81
|
+
|
|
82
|
+
async function setupCart(store, offer) {
|
|
83
|
+
const cart = await getCart();
|
|
84
|
+
const { items } = cart;
|
|
85
|
+
store.dispatch({ type: SETUP_CART, payload: cart });
|
|
86
|
+
|
|
87
|
+
// some minicart templates does not contains line.key but contains line which corresponds to
|
|
88
|
+
// the index on the cart items (Vedge)
|
|
89
|
+
|
|
90
|
+
const productAsCartLine = Number(offer.product.id);
|
|
91
|
+
if (productAsCartLine <= items.length) {
|
|
92
|
+
offer.setAttribute('product', items[productAsCartLine - 1].key);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const products = await Promise.all(Array.from(new Set(items.map(({ handle }) => handle))).map(getProduct));
|
|
96
|
+
products.forEach(product => store.dispatch({ type: SETUP_PRODUCT, payload: product }));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Synchronizes the optins/optouts using shopify cart ajax api
|
|
101
|
+
*
|
|
102
|
+
* @param action
|
|
103
|
+
* @param store
|
|
104
|
+
*/
|
|
105
|
+
export async function synchronizeCartOptin(action: any, store: any) {
|
|
106
|
+
const offerElement = action.payload.offer;
|
|
107
|
+
const selling_plan = action.payload.frequency || null;
|
|
108
|
+
const trackingEvent = getTrackingEvent(action);
|
|
109
|
+
|
|
110
|
+
if (!offerElement?.isCart) {
|
|
111
|
+
if (offerElement) {
|
|
112
|
+
updateTrackingInputs(offerElement.product.id, trackingEvent[0], trackingEvent[1]);
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
// disable the interactions on the offer since we need to process its side-effects first.
|
|
119
|
+
offerElement.style.pointerEvents = 'none';
|
|
120
|
+
offerElement.style.opacity = '.7';
|
|
121
|
+
|
|
122
|
+
const closestSection = offerElement.closest('.shopify-section');
|
|
123
|
+
const closestSectionId = (closestSection?.id.match(/^shopify-section-(.+)/) || [])[1];
|
|
124
|
+
|
|
125
|
+
const key = action.payload.product.id; // shopify cart.item.key
|
|
126
|
+
const cart = await getCart();
|
|
127
|
+
const offerIx = cart?.items?.findIndex(it => it.key === key); // cart.items[offerIx];
|
|
128
|
+
const item = cart.items[offerIx];
|
|
129
|
+
const qty = item.quantity;
|
|
130
|
+
const productId = safeProductId(key);
|
|
131
|
+
|
|
132
|
+
const res = await fetch('/cart/change.js', {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
credentials: 'same-origin',
|
|
135
|
+
headers: { 'Content-Type': 'application/json' },
|
|
136
|
+
body: JSON.stringify({
|
|
137
|
+
id: key,
|
|
138
|
+
quantity: qty,
|
|
139
|
+
attributes: Object.fromEntries([trackingEvent]),
|
|
140
|
+
properties: item.properties,
|
|
141
|
+
selling_plan: selling_plan || null,
|
|
142
|
+
sections: closestSectionId ? [closestSectionId] : undefined
|
|
143
|
+
})
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (res.status !== 200) throw new Error('Cart not updated');
|
|
147
|
+
|
|
148
|
+
const newCart = await res.json();
|
|
149
|
+
|
|
150
|
+
// If both carts have same length we can update the item.key
|
|
151
|
+
// to the original offer element, at least provide
|
|
152
|
+
// some graceful degradations if no sections nor cart page
|
|
153
|
+
const newKey =
|
|
154
|
+
cart.items.length === newCart.items.length
|
|
155
|
+
? newCart.items[offerIx].key
|
|
156
|
+
: newCart.items.find(
|
|
157
|
+
line =>
|
|
158
|
+
line.quantity === qty &&
|
|
159
|
+
line.product_id === productId &&
|
|
160
|
+
((!selling_plan && !line.selling_plan_allocation) ||
|
|
161
|
+
line?.selling_plan_allocation.selling_plan.id === selling_plan)
|
|
162
|
+
)?.key;
|
|
163
|
+
|
|
164
|
+
if (newKey) offerElement.setAttribute('product', newKey);
|
|
165
|
+
|
|
166
|
+
// dispatch SETUP_CART so offer does not flip the state
|
|
167
|
+
store.dispatch({ type: SETUP_CART, payload: newCart });
|
|
168
|
+
|
|
169
|
+
// Use a custom event to hook custom cart updates.
|
|
170
|
+
const cartUpdateEvent = new CustomEvent(CART_UPDATED_EVENT, { bubbles: true, cancelable: true });
|
|
171
|
+
offerElement.dispatchEvent(cartUpdateEvent);
|
|
172
|
+
|
|
173
|
+
// Let client uses preventDefault if they want to skip default logic after event.
|
|
174
|
+
if (cartUpdateEvent.defaultPrevented) return;
|
|
175
|
+
|
|
176
|
+
const sections = newCart.sections;
|
|
177
|
+
|
|
178
|
+
if (sections && closestSectionId in sections) {
|
|
179
|
+
const sectionRawHtml = sections[closestSectionId];
|
|
180
|
+
|
|
181
|
+
const el = new DOMParser()
|
|
182
|
+
.parseFromString(sectionRawHtml.toString() || '', 'text/html')
|
|
183
|
+
.getElementById('shopify-section-' + closestSectionId);
|
|
184
|
+
if (el) {
|
|
185
|
+
closestSection.innerHTML = el.innerHTML;
|
|
186
|
+
}
|
|
187
|
+
} else if (window.location.pathname.startsWith(CART_PAGE_URL)) {
|
|
188
|
+
// only do if we are on the cart page
|
|
189
|
+
window.location.reload();
|
|
190
|
+
}
|
|
191
|
+
} catch (err) {
|
|
192
|
+
console.log('OG Error updating cart', err);
|
|
193
|
+
} finally {
|
|
194
|
+
offerElement.style.pointerEvents = 'auto';
|
|
195
|
+
offerElement.style.opacity = '1';
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Returns a tracking event adhering to the below format:
|
|
201
|
+
*
|
|
202
|
+
* og__<ts in seconds>: "<product_id>,<action>,<location>,<selling_plan optional>"
|
|
203
|
+
*
|
|
204
|
+
* Examples:
|
|
205
|
+
* og__165653130: "offer_show,pdp,123456789"
|
|
206
|
+
* og__165653137: "optin,pdp,123,456"
|
|
207
|
+
* og__165653138: "optout,pdp,123,"
|
|
208
|
+
* og__165653139: "optin,cart,123,456",
|
|
209
|
+
*
|
|
210
|
+
* @param action a Redux action
|
|
211
|
+
* @return {Array} an array with positional values key, value
|
|
212
|
+
*/
|
|
213
|
+
export function getTrackingEvent(action): Array<string> {
|
|
214
|
+
const product_id = action.payload.product.id;
|
|
215
|
+
if (!product_id) return [];
|
|
216
|
+
const key = `og__${Math.ceil(new Date().getTime() / 1000)}`;
|
|
217
|
+
const location = action.payload.offer?.location || '';
|
|
218
|
+
const value = [product_id, action.type.toLowerCase(), location];
|
|
219
|
+
|
|
220
|
+
switch (action.type) {
|
|
221
|
+
case REQUEST_OFFER:
|
|
222
|
+
case OPTOUT_PRODUCT:
|
|
223
|
+
value.push('');
|
|
224
|
+
break;
|
|
225
|
+
case OPTIN_PRODUCT:
|
|
226
|
+
case PRODUCT_CHANGE_FREQUENCY:
|
|
227
|
+
value.push(action.payload.frequency);
|
|
228
|
+
break;
|
|
229
|
+
default:
|
|
230
|
+
return []; // we dont track anything else
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return [key, value.join(',')];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Creates or updates a hidden input used for tracking on non-cart pages
|
|
238
|
+
*
|
|
239
|
+
* @param product_id a product ID
|
|
240
|
+
* @param name an input name, og_<timestamp in seconds>
|
|
241
|
+
* @param value an input value, <tracking event>
|
|
242
|
+
* @return {undefined}
|
|
243
|
+
*/
|
|
244
|
+
export function updateTrackingInputs(product_id: string, name: string, value: string) {
|
|
245
|
+
const store2FormElementSelector = `[name="id"][value="${product_id}"]`;
|
|
246
|
+
const store1FormElementSelector = `form[action="/cart/add"] option[value="${product_id}"]`;
|
|
247
|
+
if (!name) return;
|
|
248
|
+
let cartAddFormElements = document.querySelectorAll(store2FormElementSelector);
|
|
249
|
+
if (!cartAddFormElements.length) {
|
|
250
|
+
cartAddFormElements = document.querySelectorAll(store1FormElementSelector);
|
|
251
|
+
}
|
|
252
|
+
[...cartAddFormElements].forEach((cartAddFormElement: HTMLInputElement) => {
|
|
253
|
+
const parent = cartAddFormElement.form;
|
|
254
|
+
|
|
255
|
+
let input = parent?.querySelector(`[name="${name}"]`) as HTMLInputElement;
|
|
256
|
+
if (!input) {
|
|
257
|
+
input = document.createElement('input');
|
|
258
|
+
input.type = 'hidden';
|
|
259
|
+
input.name = `attributes[${name}]`;
|
|
260
|
+
parent?.appendChild(input);
|
|
261
|
+
}
|
|
262
|
+
input.value = value;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function getOrCreateHidden(parent, name, value) {
|
|
267
|
+
let input = parent.querySelector(`[name="${name}"]`);
|
|
268
|
+
if (input && !value) {
|
|
269
|
+
input.remove();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (!input && value) {
|
|
273
|
+
input = document.createElement('input');
|
|
274
|
+
input.type = 'hidden';
|
|
275
|
+
input.name = name;
|
|
276
|
+
parent.appendChild(input);
|
|
277
|
+
}
|
|
278
|
+
if (input) {
|
|
279
|
+
input.value = value;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* // update <input type="hidden" name="selling_plan"/> if available
|
|
284
|
+
*
|
|
285
|
+
* @param store
|
|
286
|
+
*/
|
|
287
|
+
function synchronizeSellingPlan(store: any, offerElement?: HTMLElement) {
|
|
288
|
+
[...document.querySelectorAll('[name=id]')].forEach((productIdInput: HTMLInputElement) => {
|
|
289
|
+
const productId = productIdInput.value;
|
|
290
|
+
|
|
291
|
+
const subscribedSelector = makeSubscribedSelector({ id: productId });
|
|
292
|
+
const sellingPlanId = subscribedSelector(store.getState())?.frequency;
|
|
293
|
+
|
|
294
|
+
getOrCreateHidden(productIdInput.form, 'selling_plan', sellingPlanId);
|
|
295
|
+
if (offerElement) {
|
|
296
|
+
// use this to update the product attributes in future
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export default function shopifyMiddleware(store) {
|
|
302
|
+
return next => action => {
|
|
303
|
+
/**
|
|
304
|
+
* This redux middleware will perform Shopify specific side-effects such as change
|
|
305
|
+
* the product selling plan when offer is cart
|
|
306
|
+
*/
|
|
307
|
+
switch (action.type) {
|
|
308
|
+
case OPTIN_PRODUCT:
|
|
309
|
+
case OPTOUT_PRODUCT:
|
|
310
|
+
case PRODUCT_CHANGE_FREQUENCY:
|
|
311
|
+
break;
|
|
312
|
+
case REQUEST_OFFER:
|
|
313
|
+
if (action.payload.offer?.isCart) {
|
|
314
|
+
setupCart(store, action.payload.offer);
|
|
315
|
+
} else {
|
|
316
|
+
setupPdp(store, action.payload.offer);
|
|
317
|
+
}
|
|
318
|
+
default:
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
next(action);
|
|
322
|
+
|
|
323
|
+
switch (action.type) {
|
|
324
|
+
case OPTIN_PRODUCT:
|
|
325
|
+
case OPTOUT_PRODUCT:
|
|
326
|
+
case PRODUCT_CHANGE_FREQUENCY:
|
|
327
|
+
synchronizeCartOptin(action, store);
|
|
328
|
+
case REQUEST_OFFER:
|
|
329
|
+
case RECEIVE_OFFER:
|
|
330
|
+
case SETUP_PRODUCT:
|
|
331
|
+
synchronizeSellingPlan(store, action.payload.offer);
|
|
332
|
+
break;
|
|
333
|
+
default:
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|