@redotech/redo-hydrogen 1.1.2 → 1.2.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/dist/types.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { CartReturn } from '@shopify/hydrogen';
2
2
  import { ReactNode, DependencyList } from 'react';
3
+ import { CartWithActionsDocs } from '@shopify/hydrogen-react/dist/types/cart-types';
3
4
  import { ProductVariant } from '@shopify/hydrogen-react/storefront-api-types';
4
5
  import * as react_jsx_runtime from 'react/jsx-runtime';
5
6
 
@@ -13,7 +14,7 @@ interface RedoCoverageClient {
13
14
  get eligible(): boolean;
14
15
  get price(): number | undefined;
15
16
  get storeId(): string | undefined;
16
- get cart(): CartReturn | undefined;
17
+ get cart(): CartReturn | CartWithActionsDocs | undefined;
17
18
  get cartProduct(): CartProductVariantFragment | undefined;
18
19
  get cartAttribute(): CartAttributeKey | undefined;
19
20
  get errors(): RedoError[] | undefined;
@@ -29,7 +30,7 @@ type RedoContextValue = {
29
30
  loading: boolean;
30
31
  storeId?: string;
31
32
  cartInfoToEnable?: CartInfoToEnable;
32
- cart?: CartReturn;
33
+ cart?: CartReturn | CartWithActionsDocs;
33
34
  errors?: RedoError[];
34
35
  };
35
36
  declare enum RedoErrorType {
@@ -44,14 +45,14 @@ type RedoError = {
44
45
  };
45
46
 
46
47
  declare const RedoProvider: ({ cart, storeId, children }: {
47
- cart: CartReturn;
48
+ cart: CartReturn | CartWithActionsDocs;
48
49
  storeId: string;
49
50
  children: ReactNode;
50
51
  }) => ReactNode;
51
52
  declare const useRedoCoverageClient: () => RedoCoverageClient;
52
53
 
53
54
  declare const RedoCheckoutButtons: (props: {
54
- cart: CartReturn;
55
+ cart: CartReturn | CartWithActionsDocs;
55
56
  children?: ReactNode;
56
57
  onClick?: (enabled: boolean) => void;
57
58
  }) => react_jsx_runtime.JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redotech/redo-hydrogen",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Utilities to enable and disable Redo coverage on Hydrogen stores",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -8,6 +8,8 @@ import { useRedoCoverageClient } from "../providers/redo-coverage-client";
8
8
  import { CartInfoToEnable, RedoCoverageClient } from "../types";
9
9
  import { REDO_PUBLIC_API_HOSTNAME } from "../utils/security";
10
10
  import { CurrencyCode } from "@shopify/hydrogen-react/storefront-api-types";
11
+ import { CartWithActionsDocs } from "@shopify/hydrogen-react/dist/types/cart-types";
12
+ import { getCartLines, isCartWithActionsDocs } from "../utils/cart";
11
13
 
12
14
  type CheckoutButtonUIResponse = {
13
15
  html: string;
@@ -20,7 +22,7 @@ const getButtonsToShow = ({
20
22
  storeId
21
23
  }: {
22
24
  redoCoverageClient: RedoCoverageClient,
23
- cart: CartReturn,
25
+ cart: CartReturn | CartWithActionsDocs,
24
26
  storeId: string;
25
27
  }): Promise<CheckoutButtonUIResponse | null> => {
26
28
  return new Promise<CheckoutButtonUIResponse | null>((resolve, reject) => {
@@ -60,10 +62,10 @@ const applyButtonVariables = ({
60
62
  ui
61
63
  }: {
62
64
  redoCoverageClient: RedoCoverageClient,
63
- cart: CartReturn,
65
+ cart: CartReturn | CartWithActionsDocs,
64
66
  ui: CheckoutButtonUIResponse
65
67
  }): CheckoutButtonUIResponse | null => {
66
- if(!redoCoverageClient.eligible || !redoCoverageClient.price) {
68
+ if(!redoCoverageClient.eligible || !redoCoverageClient.price || !cart?.cost) {
67
69
  return null;
68
70
  }
69
71
 
@@ -72,7 +74,7 @@ const applyButtonVariables = ({
72
74
  currencyCode = 'USD';
73
75
  }
74
76
 
75
- const cartContainsRedo = !!(cart.lines.nodes.some((cartItem) => cartItem.merchandise?.product?.vendor === 're:do'));
77
+ const cartContainsRedo = !!(getCartLines(cart).some((cartItem) => cartItem.merchandise?.product?.vendor === 're:do'));
76
78
  const combinedPrice = new Intl.NumberFormat('en-US', {
77
79
  style: 'currency',
78
80
  currency: currencyCode
@@ -101,7 +103,7 @@ const findAncestor = (
101
103
  };
102
104
 
103
105
  const RedoCheckoutButtons = (props: {
104
- cart: CartReturn;
106
+ cart: CartReturn | CartWithActionsDocs;
105
107
  children?: ReactNode;
106
108
  onClick?: (enabled: boolean) => void;
107
109
  }) => {
@@ -3,7 +3,8 @@ import { CartReturn } from "@shopify/hydrogen";
3
3
  import { createContext, ReactNode, useContext, useEffect, useState } from "react";
4
4
  import { CartProductVariantFragment, CartAttributeKey, CartInfoToEnable, RedoContextValue, RedoCoverageClient, RedoError, RedoErrorType } from "../types";
5
5
  import { REDO_PUBLIC_API_HOSTNAME } from "../utils/security";
6
- import { addProductToCartIfNeeded, removeProductFromCartIfNeeded, setCartRedoEnabledAttribute, useFetcherWithPromise } from "../utils/cart";
6
+ import { addProductToCartIfNeeded, removeProductFromCartIfNeeded, setCartRedoEnabledAttribute, useFetcherWithPromise, isCartWithActionsDocs, getCartLines } from "../utils/cart";
7
+ import { CartWithActionsDocs } from "@shopify/hydrogen-react/dist/types/cart-types";
7
8
 
8
9
  const DEFAULT_REDO_CONTEXT_VALUE: RedoContextValue = {
9
10
  enabled: false,
@@ -17,7 +18,7 @@ const RedoProvider = ({
17
18
  storeId,
18
19
  children
19
20
  }: {
20
- cart: CartReturn,
21
+ cart: CartReturn | CartWithActionsDocs,
21
22
  storeId: string,
22
23
  children: ReactNode,
23
24
  }): ReactNode => {
@@ -40,6 +41,8 @@ const RedoProvider = ({
40
41
  return;
41
42
  }
42
43
 
44
+ let cartLines = getCartLines(cart);
45
+
43
46
  fetch(`https://${REDO_PUBLIC_API_HOSTNAME}/v2.2/stores/${storeId}/coverage-products`, {
44
47
  method: 'POST',
45
48
  headers: {
@@ -47,7 +50,7 @@ const RedoProvider = ({
47
50
  },
48
51
  body: JSON.stringify({
49
52
  cart: {
50
- lineItems: cart.lines.nodes.map((cartLine) => ({
53
+ lineItems: cartLines.map((cartLine) => ({
51
54
  id: cartLine.id,
52
55
  originalPrice: {
53
56
  amount: cartLine.merchandise?.price?.amount,
@@ -161,6 +164,7 @@ const useRedoCoverageClient = (): RedoCoverageClient => {
161
164
  cartInfoToEnable: redoContext.cartInfoToEnable,
162
165
  });
163
166
  await setCartRedoEnabledAttribute({
167
+ cart: redoContext.cart,
164
168
  fetcher,
165
169
  cartInfoToEnable: redoContext.cartInfoToEnable,
166
170
  enabled: true
@@ -177,6 +181,7 @@ const useRedoCoverageClient = (): RedoCoverageClient => {
177
181
  cartInfoToEnable: redoContext.cartInfoToEnable
178
182
  });
179
183
  await setCartRedoEnabledAttribute({
184
+ cart: redoContext.cart,
180
185
  fetcher,
181
186
  cartInfoToEnable: redoContext.cartInfoToEnable,
182
187
  enabled: false
@@ -187,7 +192,7 @@ const useRedoCoverageClient = (): RedoCoverageClient => {
187
192
  return redoContext.loading;
188
193
  },
189
194
  get eligible() {
190
- return !this.loading && !!this.price && !!this.cartProduct;
195
+ return !this.loading && !!this.price && !!this.cartProduct && !!this.cart?.cost;
191
196
  },
192
197
  get enabled() {
193
198
  return redoContext.enabled;
package/src/types.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { CartReturn } from "@shopify/hydrogen";
2
+ import { CartWithActionsDocs } from "@shopify/hydrogen-react/dist/types/cart-types";
2
3
  import { ProductVariant } from "@shopify/hydrogen-react/storefront-api-types";
3
4
 
4
5
  type CartProductVariantFragment = Omit<ProductVariant,
@@ -15,7 +16,7 @@ interface RedoCoverageClient {
15
16
  get eligible(): boolean;
16
17
  get price(): number | undefined;
17
18
  get storeId(): string | undefined;
18
- get cart(): CartReturn | undefined;
19
+ get cart(): CartReturn | CartWithActionsDocs | undefined;
19
20
  get cartProduct(): CartProductVariantFragment | undefined;
20
21
  get cartAttribute(): CartAttributeKey | undefined;
21
22
  get errors(): RedoError[] | undefined;
@@ -33,7 +34,7 @@ type RedoContextValue = {
33
34
  loading: boolean,
34
35
  storeId?: string,
35
36
  cartInfoToEnable?: CartInfoToEnable,
36
- cart?: CartReturn,
37
+ cart?: CartReturn | CartWithActionsDocs,
37
38
  errors?: RedoError[],
38
39
  };
39
40
 
package/src/utils/cart.ts CHANGED
@@ -3,49 +3,76 @@ import { CartInfoToEnable } from "../types";
3
3
  import { CartForm, CartReturn } from "@shopify/hydrogen";
4
4
  import type { AppData } from '@remix-run/react/dist/data';
5
5
  import React from 'react'
6
+ import { CartWithActionsDocs } from "@shopify/hydrogen-react/dist/types/cart-types";
7
+ import { CartLine, ComponentizableCartLine } from "@shopify/hydrogen-react/storefront-api-types";
6
8
 
7
9
  const DEFAULT_REDO_ENABLED_CART_ATTRIBUTE = 'redo_opted_in_from_cart';
8
10
 
11
+ const isCartWithActionsDocs = (cart: CartReturn | CartWithActionsDocs): cart is CartWithActionsDocs => {
12
+ return (Array.isArray(cart.lines) && 'linesAdd' in cart && typeof cart.linesAdd === 'function');
13
+ }
14
+
15
+ const getCartLines = (cart: CartReturn | CartWithActionsDocs): Array<CartLine | ComponentizableCartLine> => {
16
+ if(isCartWithActionsDocs(cart)) {
17
+ return cart.lines;
18
+ } else {
19
+ return cart.lines.nodes ?? cart.lines.edges.map((edge) => edge.node);
20
+ }
21
+ }
22
+
23
+ const waitUntilCartIdle = (cart: CartWithActionsDocs): Promise<void> => {
24
+ return new Promise((resolve, reject) => {
25
+ let interval = setInterval(() => {
26
+ if(cart.status === 'idle') {
27
+ clearInterval(interval);
28
+ return resolve();
29
+ }
30
+ }, 100);
31
+ });
32
+ }
33
+
9
34
  const addProductToCartIfNeeded = async ({
10
35
  cart,
11
36
  fetcher,
12
37
  cartInfoToEnable
13
38
  }: {
14
- cart: CartReturn | undefined,
39
+ cart: CartReturn | CartWithActionsDocs | undefined,
15
40
  fetcher: FetcherWithComponents<unknown>,
16
41
  cartInfoToEnable: CartInfoToEnable
17
42
  }) => {
18
43
  if(!cart) {
19
- return await addProductToCart({ fetcher, cartInfoToEnable });
44
+ return await addProductToCart({ cart, fetcher, cartInfoToEnable });
20
45
  }
21
46
 
22
- const redoProductsInCart = cart.lines.nodes.filter((cartLine) => {
47
+ const redoProductsInCart = getCartLines(cart).filter((cartLine) => {
23
48
  return cartLine.merchandise.product.vendor === 're:do';
24
49
  });
25
50
  const correctRedoProductInCart = redoProductsInCart?.filter((cartLine) => {
26
51
  return cartLine.merchandise.id === `gid://shopify/ProductVariant/${cartInfoToEnable.variantId}`;
27
52
  });
28
53
  if(redoProductsInCart.length === 0) {
29
- return await addProductToCart({ fetcher, cartInfoToEnable });
54
+ return await addProductToCart({ cart, fetcher, cartInfoToEnable });
30
55
  } else if (redoProductsInCart.length === 1 && correctRedoProductInCart.length === 1 && correctRedoProductInCart[0].quantity === 1) {
31
56
  // No action needed
32
57
  return;
33
58
  } else {
34
59
  let isSuccess = true;
35
60
 
36
- await removeLinesFromCart({ fetcher, lineIds: redoProductsInCart.map((cartLine) => cartLine.id) });
37
- await addProductToCart({ fetcher, cartInfoToEnable });
61
+ await removeLinesFromCart({ cart, fetcher, lineIds: redoProductsInCart.map((cartLine) => cartLine.id) });
62
+ await addProductToCart({ cart, fetcher, cartInfoToEnable });
38
63
 
39
64
  return;
40
65
  }
41
66
  };
42
67
 
43
68
  const removeLinesFromCart = async ({
69
+ cart,
44
70
  fetcher,
45
71
  lineIds
46
72
  }: {
47
- fetcher: FetcherWithComponents<unknown>,
48
- lineIds: string[]
73
+ cart: CartReturn | CartWithActionsDocs | undefined;
74
+ fetcher: FetcherWithComponents<unknown>;
75
+ lineIds: string[];
49
76
  }) => {
50
77
  const formInput = {
51
78
  action: CartForm.ACTIONS.LinesRemove,
@@ -54,12 +81,17 @@ const removeLinesFromCart = async ({
54
81
  }
55
82
  }
56
83
 
57
- await fetcher.submit(
58
- {
59
- [CartForm.INPUT_NAME]: JSON.stringify(formInput),
60
- },
61
- {method: 'POST', action: '/cart'},
62
- );
84
+ if(cart && isCartWithActionsDocs(cart)) {
85
+ cart.linesRemove(lineIds);
86
+ await waitUntilCartIdle(cart);
87
+ } else {
88
+ await fetcher.submit(
89
+ {
90
+ [CartForm.INPUT_NAME]: JSON.stringify(formInput),
91
+ },
92
+ {method: 'POST', action: '/cart'},
93
+ );
94
+ }
63
95
  };
64
96
 
65
97
  const removeProductFromCartIfNeeded = async ({
@@ -67,7 +99,7 @@ const removeProductFromCartIfNeeded = async ({
67
99
  fetcher,
68
100
  cartInfoToEnable
69
101
  }: {
70
- cart: CartReturn | undefined,
102
+ cart: CartReturn | CartWithActionsDocs | undefined,
71
103
  fetcher: FetcherWithComponents<unknown>,
72
104
  cartInfoToEnable: CartInfoToEnable
73
105
  }) => {
@@ -76,20 +108,22 @@ const removeProductFromCartIfNeeded = async ({
76
108
  return;
77
109
  }
78
110
 
79
- const redoProductsInCart = cart.lines.nodes.filter((cartLine) => {
111
+ const redoProductsInCart = getCartLines(cart).filter((cartLine) => {
80
112
  return cartLine.merchandise.product.vendor === 're:do';
81
113
  });
82
114
 
83
115
  if(redoProductsInCart.length !== 0) {
84
- await removeLinesFromCart({ fetcher, lineIds: redoProductsInCart.map((cartLine) => cartLine.id) });
116
+ await removeLinesFromCart({ cart, fetcher, lineIds: redoProductsInCart.map((cartLine) => cartLine.id) });
85
117
  } else {
86
118
  }
87
119
  };
88
120
 
89
121
  const addProductToCart = async ({
122
+ cart,
90
123
  fetcher,
91
- cartInfoToEnable
124
+ cartInfoToEnable,
92
125
  }: {
126
+ cart: CartReturn | CartWithActionsDocs | undefined,
93
127
  fetcher: FetcherWithComponents<unknown>,
94
128
  cartInfoToEnable: CartInfoToEnable
95
129
  }) => {
@@ -108,41 +142,55 @@ const addProductToCart = async ({
108
142
  }
109
143
  }
110
144
 
111
- await fetcher.submit(
112
- {
113
- [CartForm.INPUT_NAME]: JSON.stringify(formInput),
114
- },
115
- {method: 'POST', action: '/cart'},
116
- );
145
+ if(cart && isCartWithActionsDocs(cart)) {
146
+ cart.linesAdd([redoProductLine]);
147
+ await waitUntilCartIdle(cart);
148
+ } else {
149
+ await fetcher.submit(
150
+ {
151
+ [CartForm.INPUT_NAME]: JSON.stringify(formInput),
152
+ },
153
+ {method: 'POST', action: '/cart'},
154
+ );
155
+ }
117
156
  };
118
157
 
119
158
  const setCartRedoEnabledAttribute = async ({
159
+ cart,
120
160
  fetcher,
121
161
  cartInfoToEnable,
122
162
  enabled
123
163
  }: {
124
- fetcher: FetcherWithComponents<unknown>,
125
- cartInfoToEnable: CartInfoToEnable | null,
126
- enabled: boolean
164
+ cart: CartReturn | CartWithActionsDocs | undefined;
165
+ fetcher: FetcherWithComponents<unknown>;
166
+ cartInfoToEnable: CartInfoToEnable | null;
167
+ enabled: boolean;
127
168
  }) => {
169
+ const redoCartAttribute = {
170
+ key: cartInfoToEnable?.cartAttribute || DEFAULT_REDO_ENABLED_CART_ATTRIBUTE,
171
+ value: enabled.toString()
172
+ };
173
+
128
174
  const formInput = {
129
175
  action: CartForm.ACTIONS.AttributesUpdateInput,
130
176
  inputs: {
131
177
  attributes: [
132
- {
133
- key: cartInfoToEnable?.cartAttribute || DEFAULT_REDO_ENABLED_CART_ATTRIBUTE,
134
- value: enabled.toString()
135
- }
178
+ redoCartAttribute
136
179
  ]
137
180
  }
138
181
  }
139
182
 
140
- await fetcher.submit(
141
- {
142
- [CartForm.INPUT_NAME]: JSON.stringify(formInput),
143
- },
144
- {method: 'POST', action: '/cart'},
145
- );
183
+ if(cart && isCartWithActionsDocs(cart)) {
184
+ cart.cartAttributesUpdate([redoCartAttribute]);
185
+ await waitUntilCartIdle(cart);
186
+ } else {
187
+ await fetcher.submit(
188
+ {
189
+ [CartForm.INPUT_NAME]: JSON.stringify(formInput),
190
+ },
191
+ {method: 'POST', action: '/cart'},
192
+ );
193
+ }
146
194
  };
147
195
 
148
196
  type FetcherData<T> = NonNullable<T | unknown> // FIXME: used to use SerializeFrom which is deprecated. Can this be better typed?
@@ -190,5 +238,7 @@ export {
190
238
  addProductToCartIfNeeded,
191
239
  removeProductFromCartIfNeeded,
192
240
  setCartRedoEnabledAttribute,
193
- useFetcherWithPromise
241
+ useFetcherWithPromise,
242
+ isCartWithActionsDocs,
243
+ getCartLines
194
244
  };