@rebuy/rebuy-hydrogen 2.3.0 → 3.0.0-beta.1

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.
Files changed (64) hide show
  1. package/package.json +52 -26
  2. package/src/components/AddToCartBtn/AddToCartBtn.tsx +45 -0
  3. package/src/components/AddToCartBtn/HydrogenAddToCartBtn.tsx +43 -0
  4. package/src/components/AddToCartBtn/HydrogenReactAddToCartBtn.tsx +35 -0
  5. package/src/components/AddToCartBtn/index.ts +1 -0
  6. package/src/components/AddToCartBtn/types.ts +27 -0
  7. package/src/components/ProductCard/ProductCard.tsx +70 -0
  8. package/src/components/ProductCard/index.ts +1 -0
  9. package/src/components/ProductCard/types.ts +10 -0
  10. package/src/components/ProductPrice/ProductPrice.tsx +49 -0
  11. package/src/components/ProductPrice/index.ts +1 -0
  12. package/src/components/Title/Title.tsx +20 -0
  13. package/src/components/Title/index.ts +1 -0
  14. package/src/components/Title/types.ts +7 -0
  15. package/src/components/VariantSelect/VariantSelect.tsx +45 -0
  16. package/src/components/VariantSelect/index.ts +1 -0
  17. package/src/components/VariantSelect/types.ts +6 -0
  18. package/src/context/RebuyContext.tsx +9 -0
  19. package/src/hooks/titleLevel.tsx +42 -0
  20. package/src/index.ts +7 -0
  21. package/src/providers/RebuyHydrogenContextProvider.tsx +112 -0
  22. package/src/providers/RebuyHydrogenReactContextProvider.tsx +192 -0
  23. package/src/providers/types.ts +58 -0
  24. package/src/queries/cart.queries.ts +467 -0
  25. package/src/types/common.ts +8 -0
  26. package/src/types/css.d.ts +11 -0
  27. package/src/types/env.d.ts +12 -0
  28. package/src/types/rebuy.d.ts +31 -0
  29. package/src/types/rebuyCustom.ts +263 -0
  30. package/src/types/rebuySmartCart.ts +188 -0
  31. package/src/types/shopify.ts +142 -0
  32. package/src/types/widgets.ts +29 -0
  33. package/src/utils/convertToRebuyProduct.tsx +319 -0
  34. package/src/utils/createContextParameters.ts +142 -0
  35. package/src/utils/getEncodedAttributes.ts +11 -0
  36. package/src/utils/getRebuyConfig.ts +31 -0
  37. package/src/widgetContainer/RebuyWidgetContainer.tsx +183 -0
  38. package/src/widgets/RebuyCompleteTheLook/RebuyCompleteTheLook.tsx +50 -0
  39. package/src/widgets/RebuyCompleteTheLook/index.ts +1 -0
  40. package/src/widgets/RebuyCompleteTheLook/types.ts +5 -0
  41. package/src/widgets/RebuyDynamicBundleProducts/BundleImages.tsx +62 -0
  42. package/src/widgets/RebuyDynamicBundleProducts/BundlePrice.tsx +93 -0
  43. package/src/widgets/RebuyDynamicBundleProducts/BundleSelection.tsx +65 -0
  44. package/src/widgets/RebuyDynamicBundleProducts/RebuyDynamicBundleProducts.tsx +118 -0
  45. package/src/widgets/RebuyDynamicBundleProducts/Select.tsx +41 -0
  46. package/src/widgets/RebuyDynamicBundleProducts/index.ts +1 -0
  47. package/src/widgets/RebuyDynamicBundleProducts/types.ts +23 -0
  48. package/src/widgets/RebuyProductAddOns/RebuyProductAddOnCard.tsx +66 -0
  49. package/src/widgets/RebuyProductAddOns/RebuyProductAddOns.tsx +218 -0
  50. package/src/widgets/RebuyProductAddOns/index.ts +1 -0
  51. package/src/widgets/RebuyProductAddOns/types.ts +24 -0
  52. package/src/widgets/RebuyProductRecommendations/RebuyProductRecommendations.tsx +50 -0
  53. package/src/widgets/RebuyProductRecommendations/index.ts +1 -0
  54. package/src/widgets/RebuyProductRecommendations/types.ts +5 -0
  55. package/RebuyCompleteTheLook.client.jsx +0 -188
  56. package/RebuyContextProvider.client.jsx +0 -222
  57. package/RebuyContexts.client.jsx +0 -3
  58. package/RebuyDynamicBundleProducts.client.jsx +0 -415
  59. package/RebuyProductAddOnCard.client.jsx +0 -89
  60. package/RebuyProductAddOns.client.jsx +0 -227
  61. package/RebuyProductRecommendations.client.jsx +0 -68
  62. package/RebuyProductViewed.client.jsx +0 -62
  63. package/RebuyRecentlyViewedProducts.client.jsx +0 -68
  64. package/RebuyWidgetContainer.client.jsx +0 -136
@@ -1,222 +0,0 @@
1
- import { RebuyClient } from '@rebuy/rebuy';
2
- import { RebuyContext } from '@rebuy/rebuy-hydrogen/RebuyContexts.client';
3
- import * as Utilities from '@rebuy/rebuy/utilities';
4
- import { useCart, useShop, useUrl } from '@shopify/hydrogen';
5
- import { useEffect, useMemo, useState } from 'react';
6
-
7
- const REBUY_API_KEY = import.meta.env.PUBLIC_REBUY_API_KEY;
8
- const API = '/api/v1';
9
-
10
- const getEncodedAttributes = (attributes) =>
11
- encodeURIComponent(
12
- JSON.stringify(
13
- attributes.reduce(
14
- (merged, { key, value }) => ({ ...merged, [key]: value }),
15
- {}
16
- )
17
- )
18
- );
19
-
20
- export const RebuyContextProvider = ({ children }) => {
21
- // Shopify
22
- const cart = useCart();
23
- const shop = useShop();
24
- const url = useUrl();
25
-
26
- // Default state
27
- const [initialized, setInitialized] = useState(false);
28
- const [rebuyConfig, setRebuyConfig] = useState(null);
29
- const [config, setConfig] = useState({ shop: null });
30
- // const [Rebuy, setRebuy] = useState(null);
31
- const queryObject = Utilities.queryStringToObject(url.search);
32
- const utmObject = Utilities.utmObjectFromString(url);
33
-
34
- if (utmObject != null) {
35
- for (const key in utmObject) {
36
- url.searchParams.set(key, utmObject[key]);
37
- }
38
- }
39
-
40
- // Initialization
41
- useEffect(() => {
42
- const getRebuyConfig = async () => {
43
- try {
44
- const request = {
45
- url: `${API}/user/config`,
46
- parameters: { shop: shop.storeDomain },
47
- };
48
-
49
- const { data: rebuy, ...response } = await new RebuyClient(
50
- REBUY_API_KEY
51
- ).getDataFromCDN(request.url, request.parameters);
52
-
53
- // Missing Rebuy shop data?
54
- if (!rebuy?.shop) {
55
- throw new Error(
56
- 'Rebuy configuration is not properly set up - missing shop',
57
- { cause: response }
58
- );
59
- }
60
-
61
- setRebuyConfig(rebuy);
62
- } catch (err) {
63
- console.warn('Error fetching Rebuy shop config');
64
- console.error(err, err.cause);
65
- }
66
- };
67
-
68
- const onBeforeReady = async () => {
69
- // Initializing...
70
- if (!rebuyConfig?.shop) {
71
- return await getRebuyConfig();
72
- }
73
- };
74
-
75
- onBeforeReady();
76
- }, [rebuyConfig, shop]);
77
-
78
- useEffect(() => {
79
- const applyConfig = async () => {
80
- // Still fetching Rebuy config OR config already applied. Abort!
81
- if (!rebuyConfig || config.shop) return;
82
-
83
- // Bring it all together
84
- const appConfig = { ...config, ...rebuyConfig };
85
-
86
- setConfig(appConfig);
87
- };
88
-
89
- applyConfig();
90
- }, [config, rebuyConfig]);
91
-
92
- useEffect(() => {
93
- const onReady = () => {
94
- // Still initializing... Abort!
95
- if (!config.shop) return;
96
-
97
- // Initialized!
98
- setInitialized(true);
99
- // const { api_key, cache_key } = config.shop;
100
- // setRebuy(new RebuyClient(api_key, { cache_key }));
101
- };
102
-
103
- onReady();
104
- }, [config]);
105
-
106
- const contextParameters = useMemo(() => {
107
- // Still initializing... Abort!
108
- if (!config.shop) return null;
109
-
110
- const { cache_key } = config.shop;
111
- const contextParameters = {
112
- url: url.href,
113
- cache_key,
114
- };
115
- // Cart object
116
- const cartContext = {};
117
-
118
- // Set time
119
- if (Object.prototype.hasOwnProperty.call(queryObject, 'time')) {
120
- contextParameters.time = queryObject.time;
121
- }
122
-
123
- // Set Cart: token
124
- if (cart.id) {
125
- cartContext.token = Utilities.getIdFromGraphUrl(cart.id, 'Cart');
126
- contextParameters.cart_token = Utilities.getIdFromGraphUrl(
127
- cart.id,
128
- 'Cart'
129
- );
130
- }
131
-
132
- // Set Cart: subtotal
133
- if (cart.cost?.subtotalAmount?.amount) {
134
- const { amount } = cart.cost.subtotalAmount;
135
-
136
- cartContext.subtotal = Utilities.amountToCents(amount);
137
- contextParameters.cart_subtotal = Utilities.amountToCents(amount);
138
- }
139
-
140
- // Set Cart: line count
141
- if (typeof cart.lines != 'undefined') {
142
- const totalLines = cart.lines.length;
143
-
144
- cartContext.line_count = totalLines;
145
- contextParameters.cart_count = totalLines;
146
- contextParameters.cart_line_count = totalLines;
147
- }
148
-
149
- // Set Cart: item count
150
- if (typeof cart.totalQuantity != 'undefined') {
151
- const { totalQuantity } = cart;
152
-
153
- cartContext.item_count = totalQuantity;
154
- contextParameters.cart_item_count = totalQuantity;
155
- }
156
-
157
- // Set Cart: line items
158
- if (typeof cart.lines != 'undefined') {
159
- cartContext.items = [];
160
-
161
- for (const cartItem of cart.lines) {
162
- const item = {
163
- quantity: cartItem.quantity,
164
- };
165
-
166
- if (cartItem.product?.id) {
167
- item.product_id = Utilities.getIdFromGraphUrl(
168
- cartItem.product.id,
169
- 'Product'
170
- );
171
- }
172
-
173
- if (cartItem.merchandise?.id) {
174
- item.variant_id = Utilities.getIdFromGraphUrl(
175
- cartItem.merchandise.id,
176
- 'ProductVariant'
177
- );
178
- }
179
-
180
- if (cartItem.attributes) {
181
- item.properties = getEncodedAttributes(cartItem.attributes);
182
- }
183
-
184
- // TBD: item.selling_plan
185
-
186
- cartContext.items.push(item);
187
- }
188
- }
189
-
190
- // Set Cart: attributes
191
- if (cart.attributes) {
192
- cartContext.attributes = getEncodedAttributes(cart.attributes);
193
- }
194
-
195
- // Set Cart: notes
196
- if (cart.note) {
197
- cartContext.note = cart.note;
198
- }
199
-
200
- // Set cart
201
- contextParameters.cart = cartContext;
202
-
203
- return contextParameters;
204
- }, [cart, config, queryObject, url]);
205
-
206
- // Static reference (JSON) + memoization
207
- // ^ prevent re-rendering children when context params are unchanged
208
- const contextParametersJSON = JSON.stringify(contextParameters);
209
- const contextValue = useMemo(
210
- () => ({ contextParameters: JSON.parse(contextParametersJSON) }),
211
- [contextParametersJSON]
212
- );
213
-
214
- // Still initializing...
215
- if (!initialized) return null;
216
-
217
- return (
218
- <RebuyContext.Provider value={contextValue}>
219
- {children}
220
- </RebuyContext.Provider>
221
- );
222
- };
@@ -1,3 +0,0 @@
1
- import { createContext } from 'react';
2
-
3
- export const RebuyContext = createContext(null);
@@ -1,415 +0,0 @@
1
- import {
2
- Image,
3
- Link,
4
- Money,
5
- useCart,
6
- useProductOptions,
7
- } from '@shopify/hydrogen';
8
- import clsx from 'clsx';
9
- import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
10
- import { Heading, Text } from '~/components';
11
- import { Button } from '~/components/elements';
12
-
13
- const isDiscounted = (price, compareAtPrice) =>
14
- Number(compareAtPrice?.amount) > Number(price?.amount);
15
-
16
- const CompareAtPrice = ({ data: compareAtPrice, className }) => {
17
- const styles = clsx('strike', className);
18
-
19
- return (
20
- <Money
21
- withoutTrailingZeros
22
- data={compareAtPrice}
23
- as="span"
24
- className={styles}
25
- />
26
- );
27
- };
28
-
29
- const RebuyProductPrice = ({ selectedVariant = {} }) => {
30
- const { priceV2: price, compareAtPriceV2: compareAtPrice } =
31
- selectedVariant;
32
-
33
- return (
34
- price && (
35
- <div className="gap-4">
36
- <Text className="flex gap-2">
37
- <Money data={price} withoutTrailingZeros as="span" />
38
- {isDiscounted(price, compareAtPrice) && (
39
- <CompareAtPrice
40
- data={compareAtPrice}
41
- className={'opacity-50'}
42
- />
43
- )}
44
- </Text>
45
- </div>
46
- )
47
- );
48
- };
49
-
50
- const VariantSelector = ({ product, onSelectVariant, className = '' }) => {
51
- const styles = clsx('rounded py-1', className);
52
-
53
- return (
54
- product && (
55
- <select
56
- onChange={(e) => onSelectVariant(product, e.target.value)}
57
- aria-label="select variant"
58
- className={styles}
59
- value={product.selectedVariant.id}
60
- >
61
- <optgroup label={getOptionsLabel(product)}>
62
- {product.variants.nodes.map(({ id, title }) => (
63
- <option key={id + '-BundleVariant'} value={id}>
64
- {title}
65
- </option>
66
- ))}
67
- </optgroup>
68
- </select>
69
- )
70
- );
71
- };
72
-
73
- const getOptionsLabel = (product) => {
74
- const options = product.variants.nodes[0].selectedOptions;
75
- const optionsFromKeys = Object.keys(options[0]);
76
- const optionsFromValues = options.map((option) => option.name);
77
- const useValues = optionsFromKeys.every((key) =>
78
- ['name', 'value'].includes(key)
79
- );
80
-
81
- // Return delimited label for available option(s) e.g. Color / Size, Scent, etc
82
- return (useValues ? optionsFromValues : optionsFromKeys).join(' / ');
83
- };
84
-
85
- const AddToCartPriceMarkup = ({ products, addLineItemsToCart }) => {
86
- const isDisabled =
87
- products.filter((product) => product.selected).length < 1;
88
-
89
- const totalBundlePrice = () => {
90
- let total = 0;
91
- let currencyCode = 'USD';
92
-
93
- for (const product of products) {
94
- if (product.selected && product.selectedVariant) {
95
- const { priceV2: price } = product.selectedVariant;
96
-
97
- total += Number(price.amount);
98
- currencyCode = price.currencyCode;
99
- }
100
- }
101
-
102
- return {
103
- amount: String(total),
104
- currencyCode,
105
- };
106
- };
107
-
108
- const totalBundleCompareAtPrice = () => {
109
- let compareAtTotal = 0;
110
- let currencyCode = 'USD';
111
-
112
- for (const product of products) {
113
- if (product.selected && product.selectedVariant) {
114
- const { priceV2: price, compareAtPriceV2: compareAtPrice } =
115
- product.selectedVariant;
116
-
117
- currencyCode = price.currencyCode;
118
- compareAtTotal += Number((compareAtPrice || price).amount);
119
- }
120
- }
121
-
122
- return {
123
- amount: String(compareAtTotal),
124
- currencyCode,
125
- };
126
- };
127
-
128
- const price = totalBundlePrice();
129
- const compareAtPrice = totalBundleCompareAtPrice();
130
-
131
- return (
132
- products.length > 0 && (
133
- <div className="flex items-center flex-col">
134
- {!isDisabled && (
135
- <Text className="flex items-center gap-2 mb-2">
136
- <span>Total Price:</span>
137
- <Money data={price} withoutTrailingZeros as="span" />
138
- {isDiscounted(price, compareAtPrice) && (
139
- <CompareAtPrice
140
- data={compareAtPrice}
141
- className={'opacity-50'}
142
- />
143
- )}
144
- </Text>
145
- )}
146
-
147
- <Button
148
- as="button"
149
- variant={isDisabled ? 'secondary' : 'primary'}
150
- disabled={isDisabled}
151
- onClick={addLineItemsToCart}
152
- >
153
- <Text as="span">
154
- {isDisabled ? 'Create your bundle below' : 'Add to bag'}
155
- </Text>
156
- </Button>
157
- </div>
158
- )
159
- );
160
- };
161
-
162
- const BundleImages = ({ products }) => {
163
- const selected = products.filter((product) => product.selected);
164
-
165
- return (
166
- <ul className="flex flex-nowrap justify-center gap-1 mb-4">
167
- {products.map((product, index) => {
168
- const image = product.selectedVariant.image;
169
- const productImage = image ? (
170
- <Image
171
- className="fadeIn"
172
- data={image}
173
- alt={image.altText || `Picture of ${product.title}`}
174
- title={product.title}
175
- width={80}
176
- height={80}
177
- loaderOptions={{ scale: 2 }}
178
- />
179
- ) : (
180
- // No image defined
181
- product.title
182
- );
183
- // Hide delimiter for first selected item
184
- const showDelimiter = selected[0]?.id !== product.id;
185
-
186
- return (
187
- <Fragment key={product.id + '-BundleImages-' + index}>
188
- {product.selected && (
189
- <>
190
- {showDelimiter && (
191
- <li className="flex items-center">
192
- <span className="font-semibold">+</span>
193
- </li>
194
- )}
195
- <li className="flex items-center">
196
- {product.default ? (
197
- // Already on product page
198
- productImage
199
- ) : (
200
- // Link to product
201
- <Link
202
- to={`/products/${product.handle}`}
203
- title={product.title}
204
- >
205
- {productImage}
206
- </Link>
207
- )}
208
- </li>
209
- </>
210
- )}
211
- </Fragment>
212
- );
213
- })}
214
- </ul>
215
- );
216
- };
217
-
218
- const BundleSelection = ({ products, onToggleBundleItem, onSelectVariant }) => {
219
- return (
220
- <ul>
221
- {products.map((product, index) => {
222
- const bundleItemRowStyle = clsx(
223
- 'mb-4',
224
- !product.selected ? 'opacity-50' : ''
225
- );
226
- const bundleItemLabelStyle = clsx(
227
- 'flex flex-row gap-2 mb-1 cursor-pointer'
228
- );
229
- const { availableForSale } = product.selectedVariant;
230
- const isOutOfStock = !availableForSale;
231
-
232
- return (
233
- <li
234
- key={product.id + '-BundleSelection' + index}
235
- className={bundleItemRowStyle}
236
- >
237
- <div className="flex flex-row items-start gap-2">
238
- <input
239
- className="mt-1 rounded-sm accent-black cursor-pointer"
240
- type="checkbox"
241
- value={product.id}
242
- id={`${product.id}-toggle`}
243
- checked={product.selected && availableForSale}
244
- disabled={isOutOfStock}
245
- onChange={() => onToggleBundleItem(product)}
246
- />
247
- <div className="flex-grow">
248
- <label
249
- htmlFor={`${product.id}-toggle`}
250
- className={bundleItemLabelStyle}
251
- >
252
- {isOutOfStock && <b>SOLD OUT</b>}
253
- {product.default && <b>This item:</b>}
254
- {product.title}
255
-
256
- <RebuyProductPrice
257
- selectedVariant={
258
- product.selectedVariant
259
- }
260
- />
261
- </label>
262
-
263
- {product.variants?.nodes.length > 1 && (
264
- <VariantSelector
265
- product={product}
266
- onSelectVariant={onSelectVariant}
267
- className="w-full"
268
- />
269
- )}
270
- </div>
271
- </div>
272
- </li>
273
- );
274
- })}
275
- </ul>
276
- );
277
- };
278
-
279
- export const RebuyDynamicBundleProducts = ({
280
- product = {},
281
- products = [],
282
- metadata = {},
283
- title = 'Frequently purchased together',
284
- className = '',
285
- }) => {
286
- // Hooks
287
- const { linesAdd } = useCart();
288
- const { selectedVariant } = useProductOptions();
289
-
290
- // State
291
- const [bundleProducts, setBundleProduct] = useState([]);
292
- const language = metadata.widget?.language ?? {};
293
- const componentTitle = language?.title || title;
294
- const styles = clsx('grid gap-4 py-6 md:py-8 lg:py-12', className);
295
-
296
- const rebuyAttributes = useMemo(() => {
297
- return [
298
- { key: '_source', value: 'Rebuy' },
299
- {
300
- key: '_attribution',
301
- value: 'Rebuy Dynamic Bundle Recommendations',
302
- },
303
- ];
304
- }, []);
305
-
306
- // Initializiation
307
- useEffect(() => {
308
- const formatProducts = (products = []) => {
309
- for (let product of products) {
310
- product.selectedVariant = { ...product.variants.nodes[0] };
311
- product.selected = product.selectedVariant.availableForSale;
312
- }
313
-
314
- return products;
315
- };
316
-
317
- // Flag the main product (PDP)
318
- product.default = true;
319
-
320
- setBundleProduct(formatProducts([product, ...products]));
321
- }, [product, products]);
322
-
323
- // Mirror selectedVariant from PDP + allow override
324
- useEffect(() => {
325
- const bundleWithVariantMatch = (currentBundle) => {
326
- for (let product of currentBundle) {
327
- if (product.default) {
328
- product.selectedVariant = selectedVariant;
329
- product.selected = product.selectedVariant.availableForSale;
330
- break;
331
- }
332
- }
333
-
334
- return [...currentBundle];
335
- };
336
-
337
- setBundleProduct(bundleWithVariantMatch);
338
- }, [selectedVariant]);
339
-
340
- const onToggleBundleItem = useCallback(
341
- (product) => {
342
- product.selected = !product.selected;
343
-
344
- setBundleProduct([...bundleProducts]);
345
- },
346
- [bundleProducts]
347
- );
348
-
349
- const onSelectVariant = useCallback(
350
- (product, variantId) => {
351
- const variant = product.variants.nodes.find(
352
- ({ id }) => id === variantId
353
- );
354
-
355
- if (variant) {
356
- product.selectedVariant = variant;
357
- product.selected = variant.availableForSale;
358
-
359
- setBundleProduct([...bundleProducts]);
360
- }
361
- },
362
- [bundleProducts]
363
- );
364
-
365
- const addLineItemsToCart = useCallback(() => {
366
- const itemsToAdd = [];
367
-
368
- for (let product of bundleProducts) {
369
- if (product.selected && product.selectedVariant) {
370
- itemsToAdd.push({
371
- quantity: 1,
372
- merchandiseId: product.selectedVariant.id,
373
- attributes: rebuyAttributes,
374
- });
375
- }
376
- }
377
-
378
- linesAdd(itemsToAdd);
379
- }, [bundleProducts, linesAdd, rebuyAttributes]);
380
-
381
- return (
382
- bundleProducts.length > 0 && (
383
- <div className={styles}>
384
- {componentTitle && (
385
- <Heading
386
- size="lead"
387
- width="full"
388
- className="px-6 md:px-8 lg:px-12"
389
- >
390
- {componentTitle}
391
- </Heading>
392
- )}
393
-
394
- <div className="px-6 md:px-8 lg:px-12">
395
- <div className="flex flex-col lg:flex-row mb-4">
396
- <BundleImages products={bundleProducts} />
397
-
398
- <AddToCartPriceMarkup
399
- addLineItemsToCart={addLineItemsToCart}
400
- products={bundleProducts}
401
- />
402
- </div>
403
-
404
- <BundleSelection
405
- products={bundleProducts}
406
- onToggleBundleItem={onToggleBundleItem}
407
- onSelectVariant={onSelectVariant}
408
- />
409
- </div>
410
- </div>
411
- )
412
- );
413
- };
414
-
415
- export default RebuyDynamicBundleProducts;