@magento/peregrine 12.2.0 → 12.3.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.
Files changed (47) hide show
  1. package/lib/Apollo/clearCustomerDataFromCache.js +1 -0
  2. package/lib/Apollo/policies/index.js +9 -4
  3. package/lib/hooks/useGoogleReCaptcha/googleReCaptchaConfig.gql.js +16 -0
  4. package/lib/hooks/useGoogleReCaptcha/index.js +1 -0
  5. package/lib/hooks/useGoogleReCaptcha/useGoogleReCaptcha.js +210 -0
  6. package/lib/hooks/useMediaQuery.js +83 -0
  7. package/lib/hooks/useScript.js +68 -0
  8. package/lib/hooks/useSort.js +13 -2
  9. package/lib/talons/AccountInformationPage/useAccountInformationPage.js +23 -6
  10. package/lib/talons/CartPage/PriceAdjustments/GiftOptions/giftOptions.gql.js +36 -11
  11. package/lib/talons/CartPage/PriceAdjustments/GiftOptions/giftOptionsFragments.gql.js +19 -0
  12. package/lib/talons/CartPage/PriceAdjustments/GiftOptions/useGiftOptions.js +315 -94
  13. package/lib/talons/CartPage/PriceAdjustments/giftOptionsSection.gql.js +17 -0
  14. package/lib/talons/CartPage/PriceAdjustments/useGiftOptionsSection.js +61 -0
  15. package/lib/talons/CartPage/PriceSummary/__fixtures__/priceSummary.js +7 -2
  16. package/lib/talons/CartPage/PriceSummary/priceSummaryFragments.gql.js +7 -0
  17. package/lib/talons/CartPage/PriceSummary/queries/giftOptionsSummary.ce.js +8 -0
  18. package/lib/talons/CartPage/PriceSummary/queries/giftOptionsSummary.ee.js +15 -0
  19. package/lib/talons/CartPage/PriceSummary/useDiscountSummary.js +71 -0
  20. package/lib/talons/CartPage/PriceSummary/usePriceSummary.js +3 -1
  21. package/lib/talons/CartPage/ProductListing/EditModal/__fixtures__/configurableThumbnailSource.js +8 -0
  22. package/lib/talons/CartPage/ProductListing/EditModal/productForm.gql.js +11 -0
  23. package/lib/talons/CartPage/ProductListing/EditModal/useProductForm.js +15 -1
  24. package/lib/talons/CheckoutPage/OrderConfirmationPage/useCreateAccount.js +27 -5
  25. package/lib/talons/CheckoutPage/PaymentInformation/useCreditCard.js +36 -15
  26. package/lib/talons/CheckoutPage/useCheckoutPage.js +18 -3
  27. package/lib/talons/CmsDynamicBlock/client-schema.graphql +4 -0
  28. package/lib/talons/CmsDynamicBlock/cmsDynamicBlock.gql.js +113 -0
  29. package/lib/talons/CmsDynamicBlock/useCmsDynamicBlock.js +211 -0
  30. package/lib/talons/CreateAccount/useCreateAccount.js +29 -5
  31. package/lib/talons/ForgotPassword/useForgotPassword.js +26 -5
  32. package/lib/talons/Link/useLink.js +2 -1
  33. package/lib/talons/MiniCart/miniCartFragments.gql.js +4 -0
  34. package/lib/talons/MyAccount/useResetPassword.js +23 -5
  35. package/lib/talons/RootComponents/Category/useCategory.js +1 -1
  36. package/lib/talons/RootComponents/Product/useProduct.js +1 -6
  37. package/lib/talons/SearchPage/useSearchPage.js +1 -1
  38. package/lib/talons/SignIn/useSignIn.js +25 -6
  39. package/lib/talons/WishlistPage/createWishlist.gql.js +1 -0
  40. package/lib/talons/WishlistPage/useActionMenu.js +4 -4
  41. package/lib/talons/WishlistPage/useCreateWishlist.js +7 -4
  42. package/lib/talons/WishlistPage/useWishlistItem.js +3 -2
  43. package/lib/talons/WishlistPage/wishlistConfig.gql.ee.js +1 -0
  44. package/lib/talons/WishlistPage/wishlistItemFragments.gql.js +1 -0
  45. package/lib/util/configuredVariant.js +10 -6
  46. package/package.json +1 -1
  47. package/lib/talons/CartPage/PriceAdjustments/GiftOptions/client-schema.graphql +0 -7
@@ -12,6 +12,7 @@ export const clearCustomerDataFromCache = async client => {
12
12
  // Cached ROOT_QUERY
13
13
  client.cache.evict({ fieldName: 'customer' });
14
14
  client.cache.evict({ fieldName: 'customerWishlistProducts' });
15
+ client.cache.evict({ fieldName: 'dynamicBlocks' });
15
16
 
16
17
  client.cache.gc();
17
18
 
@@ -177,10 +177,15 @@ const typePolicies = {
177
177
  SelectedConfigurableOption: {
178
178
  // id alone is not enough to identify a selected option as it can refer
179
179
  // to something like "size" where value_id refers to "large".
180
- keyFields: [
181
- 'configurable_product_option_uid',
182
- 'configurable_product_option_value_uid'
183
- ]
180
+ // TODO: Use configurable_product_option_uid for ConfigurableWishlistItem when available in 2.4.5
181
+ keyFields: fields => {
182
+ return fields.configurable_product_option_uid
183
+ ? [
184
+ 'configurable_product_option_uid',
185
+ 'configurable_product_option_value_uid'
186
+ ]
187
+ : ['id', 'value_id'];
188
+ }
184
189
  },
185
190
  SelectedPaymentMethod: {
186
191
  keyFields: ['code']
@@ -0,0 +1,16 @@
1
+ import { gql } from '@apollo/client';
2
+
3
+ export const GET_RECAPTCHAV3_CONFIG = gql`
4
+ query GetReCaptchaV3Config {
5
+ recaptchaV3Config {
6
+ website_key
7
+ badge_position
8
+ language_code
9
+ forms
10
+ }
11
+ }
12
+ `;
13
+
14
+ export default {
15
+ getReCaptchaV3ConfigQuery: GET_RECAPTCHAV3_CONFIG
16
+ };
@@ -0,0 +1 @@
1
+ export { useGoogleReCaptcha } from './useGoogleReCaptcha';
@@ -0,0 +1,210 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import { useQuery } from '@apollo/client';
3
+
4
+ import useScript from '@magento/peregrine/lib/hooks/useScript';
5
+ import mergeOperations from '@magento/peregrine/lib/util/shallowMerge';
6
+
7
+ import defaultOperations from './googleReCaptchaConfig.gql';
8
+
9
+ const GOOGLE_RECAPTCHA_HEADER = 'X-ReCaptcha';
10
+ const GOOGLE_RECAPTCHA_URL = 'https://www.google.com/recaptcha/api.js';
11
+
12
+ /**
13
+ * Returns props necessary to attach Google ReCaptcha V3 to a form.
14
+ *
15
+ * @function
16
+ *
17
+ * @param {String} props.currentForm - Form name to match GraphQl ReCaptchaFormEnum.
18
+ * @param {String} props.formAction - Action name to use for logging in API.
19
+ * @param {Object} [props.operations] - GraphQL operations to be run by the hook.
20
+ *
21
+ * @returns {GoogleReCaptchaProps}
22
+ *
23
+ * @example <caption>Importing into your project</caption>
24
+ * import { useGoogleReCaptcha } from '@magento/peregrine/lib/hooks/useGoogleReCaptcha';
25
+ */
26
+ export const useGoogleReCaptcha = props => {
27
+ const operations = mergeOperations(defaultOperations, props.operations);
28
+ const { currentForm, formAction } = props;
29
+
30
+ const {
31
+ data: configData,
32
+ error: configError,
33
+ loading: configLoading
34
+ } = useQuery(operations.getReCaptchaV3ConfigQuery, {
35
+ fetchPolicy: 'cache-and-network'
36
+ });
37
+
38
+ if (!globalThis['recaptchaCallbacks']) {
39
+ globalThis['recaptchaCallbacks'] = {};
40
+ }
41
+ const [apiIsReady, setApiIsReady] = useState(
42
+ globalThis.hasOwnProperty('grecaptcha')
43
+ );
44
+ const [isGenerating, setIsGenerating] = useState(false);
45
+ const [widgetId, setWidgetId] = useState(null);
46
+
47
+ // Container Reference to be used for the GoogleReCaptcha component
48
+ const [inlineContainer, setInlineContainer] = useState(null);
49
+
50
+ // callback to update container element ref in case of mount/unmount
51
+ const updateInlineContainerRef = useCallback(node => {
52
+ if (node !== null) {
53
+ setInlineContainer(node);
54
+ }
55
+ }, []);
56
+
57
+ const recaptchaBadge =
58
+ configData?.recaptchaV3Config?.badge_position &&
59
+ configData.recaptchaV3Config.badge_position.length > 0
60
+ ? configData.recaptchaV3Config.badge_position
61
+ : 'bottomright';
62
+ const recaptchaKey = configData?.recaptchaV3Config?.website_key;
63
+ const recaptchaLang = configData?.recaptchaV3Config?.language_code;
64
+ const activeForms = configData?.recaptchaV3Config?.forms || [];
65
+ const isEnabled =
66
+ !(configError instanceof Error) &&
67
+ recaptchaKey &&
68
+ recaptchaKey.length > 0 &&
69
+ activeForms.includes(currentForm);
70
+
71
+ // Determine which type of badge should be loaded
72
+ const isInline = recaptchaBadge === 'inline';
73
+
74
+ // Construct script url with configs
75
+ const scriptUrl = new URL(GOOGLE_RECAPTCHA_URL);
76
+
77
+ scriptUrl.searchParams.append('badge', recaptchaBadge);
78
+
79
+ // Render separate widgets with GoogleReCaptcha component when inline
80
+ scriptUrl.searchParams.append(
81
+ 'render',
82
+ isInline ? 'explicit' : recaptchaKey
83
+ );
84
+ scriptUrl.searchParams.append('onload', 'onloadRecaptchaCallback');
85
+
86
+ if (recaptchaLang && recaptchaLang.length > 0) {
87
+ scriptUrl.searchParams.append('hl', recaptchaLang);
88
+ }
89
+
90
+ // Load Script only if the API is not already set, if the key is set
91
+ // and if the current form is enabled in the V3 configs
92
+ const status = useScript(!apiIsReady && isEnabled ? scriptUrl : null);
93
+
94
+ // Wait for config to be loaded and script to be ready
95
+ const isLoading =
96
+ configLoading || (isEnabled && !apiIsReady && status !== 'ready');
97
+
98
+ // Render inline widget manually
99
+ useEffect(() => {
100
+ // Only render if container is set and API is available
101
+ if (
102
+ inlineContainer !== null &&
103
+ isInline &&
104
+ apiIsReady &&
105
+ widgetId === null
106
+ ) {
107
+ // Avoid loading twice if already rendered
108
+ if ('widgetId' in inlineContainer.dataset) {
109
+ setWidgetId(inlineContainer.dataset.widgetId);
110
+ } else {
111
+ const id = globalThis.grecaptcha.render(inlineContainer, {
112
+ sitekey: recaptchaKey,
113
+ size: 'invisible'
114
+ });
115
+
116
+ setWidgetId(id);
117
+ inlineContainer.dataset.widgetId = id;
118
+ }
119
+ }
120
+ }, [apiIsReady, isInline, recaptchaKey, widgetId, inlineContainer]);
121
+
122
+ // Callback sets API as ready
123
+ if (!globalThis['recaptchaCallbacks'][formAction] && isEnabled) {
124
+ globalThis['recaptchaCallbacks'][formAction] = () => {
125
+ // Update non inline styles
126
+ if (!isInline) {
127
+ const floatingBadge = document.getElementsByClassName(
128
+ 'grecaptcha-badge'
129
+ );
130
+
131
+ if (floatingBadge && floatingBadge.length > 0) {
132
+ floatingBadge[0].style.zIndex = 999;
133
+ }
134
+ }
135
+
136
+ setApiIsReady(true);
137
+ };
138
+ }
139
+
140
+ // Callback loops through each instance and set API as ready
141
+ globalThis['onloadRecaptchaCallback'] = useCallback(() => {
142
+ for (const key in globalThis['recaptchaCallbacks']) {
143
+ globalThis['recaptchaCallbacks'][key]();
144
+ }
145
+ // Reset value after
146
+ globalThis['recaptchaCallbacks'] = {};
147
+ }, []);
148
+
149
+ // Generate the object that will be sent with the request
150
+ const generateReCaptchaData = useCallback(async () => {
151
+ if (apiIsReady) {
152
+ try {
153
+ setIsGenerating(true);
154
+
155
+ const token = await globalThis.grecaptcha.execute(
156
+ isInline ? widgetId : recaptchaKey,
157
+ {
158
+ action: formAction
159
+ }
160
+ );
161
+
162
+ const result = {
163
+ // TODO: Use Apollo Link middleware when solution is found
164
+ context: {
165
+ headers: {
166
+ [GOOGLE_RECAPTCHA_HEADER]: token
167
+ }
168
+ }
169
+ };
170
+
171
+ setIsGenerating(false);
172
+
173
+ return result;
174
+ } catch (error) {
175
+ // Log API error
176
+ console.error(error);
177
+
178
+ setIsGenerating(false);
179
+ }
180
+ }
181
+
182
+ return {};
183
+ }, [apiIsReady, formAction, isInline, recaptchaKey, widgetId]);
184
+
185
+ const recaptchaWidgetProps = {
186
+ containerElement: updateInlineContainerRef,
187
+ shouldRender: !!(isEnabled && isInline && apiIsReady)
188
+ };
189
+
190
+ return {
191
+ recaptchaLoading: isGenerating || isLoading,
192
+ generateReCaptchaData,
193
+ recaptchaWidgetProps
194
+ };
195
+ };
196
+
197
+ /** JSDocs type definitions */
198
+
199
+ /**
200
+ * Object type returned by the {@link useGoogleReCaptcha} hook.
201
+ * It provides props data to use when attaching Google ReCaptcha V3 to a form.
202
+ *
203
+ * @typedef {Object} GoogleReCaptchaProps
204
+ *
205
+ * @property {Boolean} recaptchaLoading - Indicates if hook is loading data or loading the script.
206
+ * @property {Function} generateReCaptchaData - The function to generate ReCaptcha Mutation data.
207
+ * @property {Object} recaptchaWidgetProps - Props for the GoogleReCaptcha component.
208
+ * @property {Function} recaptchaWidgetProps.containerElement - Container reference callback.
209
+ * @property {Boolean} recaptchaWidgetProps.shouldRender - Checks if component should be rendered.
210
+ */
@@ -0,0 +1,83 @@
1
+ import { useEffect, useState, useRef } from 'react';
2
+ import { string, shape, object, arrayOf } from 'prop-types';
3
+
4
+ const { matchMedia } = globalThis;
5
+
6
+ /**
7
+ * A hook that will return all matched styles for any given media queries using the
8
+ * matchMedia API (https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia)
9
+ *
10
+ * @param {Object} props
11
+ * @param {Array} props.mediaQueries The array of media rules and respective styles to apply
12
+ *
13
+ * @returns {Object}
14
+ */
15
+ export const useMediaQuery = (props = { mediaQueries: [] }) => {
16
+ const [styles, setStyles] = useState({});
17
+
18
+ const isMountedRef = useRef(null);
19
+
20
+ const { mediaQueries } = props;
21
+
22
+ useEffect(() => {
23
+ isMountedRef.current = true;
24
+ if (!mediaQueries) return;
25
+
26
+ const mqlList = mediaQueries.map(({ media }) => matchMedia(media));
27
+
28
+ const handleMatch = (query, i) => {
29
+ if (!isMountedRef.current) return;
30
+
31
+ if (query.matches) {
32
+ setStyles(prevState => ({
33
+ ...prevState,
34
+ ...mediaQueries[i].style
35
+ }));
36
+ } else {
37
+ setStyles(prevState => {
38
+ const filteredState = Object.keys(prevState)
39
+ .filter(
40
+ key => mediaQueries[i].style[key] !== prevState[key]
41
+ )
42
+ .reduce((obj, key) => {
43
+ return {
44
+ ...obj,
45
+ [key]: prevState[key]
46
+ };
47
+ }, {});
48
+ return filteredState;
49
+ });
50
+ }
51
+ };
52
+
53
+ mqlList.forEach((mql, i) => {
54
+ if (mql.matches) {
55
+ setStyles(prevState => ({
56
+ ...prevState,
57
+ ...mediaQueries[i].style
58
+ }));
59
+ }
60
+ mql.addEventListener('change', query => handleMatch(query, i));
61
+ });
62
+
63
+ return () => {
64
+ isMountedRef.current = false;
65
+ mqlList.forEach((mql, i) => {
66
+ mql.removeEventListener('change', query =>
67
+ handleMatch(query, i)
68
+ );
69
+ });
70
+ };
71
+ }, [mediaQueries]);
72
+
73
+ return { styles };
74
+ };
75
+
76
+ useMediaQuery.propTypes = {
77
+ mediaQueries: arrayOf(
78
+ shape({
79
+ media: string.isRequired,
80
+ style: object.isRequired
81
+ })
82
+ ).isRequired
83
+ };
@@ -0,0 +1,68 @@
1
+ import { useState, useEffect } from 'react';
2
+
3
+ /**
4
+ * Dynamically load an external script and know when its loaded
5
+ * @see https://usehooks.com/useScript/.
6
+ *
7
+ * @param {String} src - the script src.
8
+ *
9
+ * @returns {string} - returns one of the possible status: "idle", "loading", "ready" or "error"
10
+ */
11
+ export default src => {
12
+ // Keep track of script status ("idle", "loading", "ready", "error")
13
+ const [status, setStatus] = useState(src ? 'loading' : 'idle');
14
+ useEffect(
15
+ () => {
16
+ // Allow falsy src value if waiting on other data needed for
17
+ // constructing the script URL passed to this hook.
18
+ if (!src) {
19
+ setStatus('idle');
20
+ return;
21
+ }
22
+ // Fetch existing script element by src
23
+ // It may have been added by another instance of this hook
24
+ let script = document.querySelector(`script[src="${src}"]`);
25
+ if (!script) {
26
+ // Create script
27
+ script = document.createElement('script');
28
+ script.src = src;
29
+ script.async = true;
30
+ script.setAttribute('data-status', 'loading');
31
+ // Add script to document body
32
+ document.body.appendChild(script);
33
+ // Store status in attribute on script
34
+ // This can be read by other instances of this hook
35
+ const setAttributeFromEvent = event => {
36
+ script.setAttribute(
37
+ 'data-status',
38
+ event.type === 'load' ? 'ready' : 'error'
39
+ );
40
+ };
41
+ script.addEventListener('load', setAttributeFromEvent);
42
+ script.addEventListener('error', setAttributeFromEvent);
43
+ } else {
44
+ // Grab existing script status from attribute and set to state.
45
+ setStatus(script.getAttribute('data-status'));
46
+ }
47
+ // Script event handler to update status in state
48
+ // Note: Even if the script already exists we still need to add
49
+ // event handlers to update the state for *this* hook instance.
50
+ const setStateFromEvent = event => {
51
+ setStatus(event.type === 'load' ? 'ready' : 'error');
52
+ };
53
+ // Add event listeners
54
+ script.addEventListener('load', setStateFromEvent);
55
+ script.addEventListener('error', setStateFromEvent);
56
+ // Remove event listeners on cleanup
57
+ return () => {
58
+ if (script) {
59
+ script.removeEventListener('load', setStateFromEvent);
60
+ script.removeEventListener('error', setStateFromEvent);
61
+ }
62
+ };
63
+ },
64
+ [src] // Only re-run effect if script src changes
65
+ );
66
+
67
+ return status;
68
+ };
@@ -8,10 +8,21 @@ const defaultSort = {
8
8
  sortDirection: 'ASC'
9
9
  };
10
10
 
11
+ const searchSort = {
12
+ sortText: 'Best Match',
13
+ sortId: 'sortItem.relevance',
14
+ sortAttribute: 'relevance',
15
+ sortDirection: 'DESC'
16
+ };
17
+
11
18
  /**
12
19
  *
13
20
  * @param props
14
21
  * @returns {[{sortDirection: string, sortAttribute: string, sortText: string}, React.Dispatch<React.SetStateAction<{sortDirection: string, sortAttribute: string, sortText: string}>>]}
15
22
  */
16
- export const useSort = (props = {}) =>
17
- useState(() => Object.assign({}, defaultSort, props));
23
+ export const useSort = (props = {}) => {
24
+ const { sortFromSearch = false } = props;
25
+ return useState(() =>
26
+ Object.assign({}, sortFromSearch ? searchSort : defaultSort, props)
27
+ );
28
+ };
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useMemo, useState } from 'react';
2
2
  import { useMutation, useQuery } from '@apollo/client';
3
3
  import { useUserContext } from '../../context/user';
4
+ import { useGoogleReCaptcha } from '../../hooks/useGoogleReCaptcha';
4
5
 
5
6
  export const useAccountInformationPage = props => {
6
7
  const {
@@ -46,6 +47,15 @@ export const useAccountInformationPage = props => {
46
47
  }
47
48
  ] = useMutation(changeCustomerPasswordMutation);
48
49
 
50
+ const {
51
+ generateReCaptchaData,
52
+ recaptchaLoading,
53
+ recaptchaWidgetProps
54
+ } = useGoogleReCaptcha({
55
+ currentForm: 'CUSTOMER_EDIT',
56
+ formAction: 'editCustomer'
57
+ });
58
+
49
59
  const initialValues = useMemo(() => {
50
60
  if (accountInformationData) {
51
61
  return { customer: accountInformationData.customer };
@@ -97,11 +107,13 @@ export const useAccountInformationPage = props => {
97
107
  });
98
108
  }
99
109
  if (password && newPassword) {
110
+ const recaptchaDataForChangeCustomerPassword = await generateReCaptchaData();
100
111
  await changeCustomerPassword({
101
112
  variables: {
102
113
  currentPassword: password,
103
114
  newPassword: newPassword
104
- }
115
+ },
116
+ ...recaptchaDataForChangeCustomerPassword
105
117
  });
106
118
  }
107
119
  // After submission, close the form if there were no errors.
@@ -117,10 +129,11 @@ export const useAccountInformationPage = props => {
117
129
  }
118
130
  },
119
131
  [
120
- setCustomerInformation,
132
+ initialValues,
121
133
  handleCancel,
122
- changeCustomerPassword,
123
- initialValues
134
+ setCustomerInformation,
135
+ generateReCaptchaData,
136
+ changeCustomerPassword
124
137
  ]
125
138
  );
126
139
 
@@ -134,10 +147,14 @@ export const useAccountInformationPage = props => {
134
147
  handleSubmit,
135
148
  handleChangePassword,
136
149
  initialValues,
137
- isDisabled: isUpdatingCustomerInformation || isChangingCustomerPassword,
150
+ isDisabled:
151
+ isUpdatingCustomerInformation ||
152
+ isChangingCustomerPassword ||
153
+ recaptchaLoading,
138
154
  isUpdateMode,
139
155
  loadDataError,
140
156
  shouldShowNewPassword,
141
- showUpdateMode
157
+ showUpdateMode,
158
+ recaptchaWidgetProps
142
159
  };
143
160
  };
@@ -1,21 +1,46 @@
1
1
  import { gql } from '@apollo/client';
2
2
 
3
- /**
4
- * Local query. GQL support is not available as of today.
5
- *
6
- * Once available, we can change the query to match the schema.
7
- */
3
+ import { CartPageFragment } from '../../cartPageFragments.gql';
4
+ import { GiftOptionsFragment } from './giftOptionsFragments.gql';
5
+
8
6
  const GET_GIFT_OPTIONS = gql`
9
- query getGiftOptions($cartId: String!) {
10
- cart(cart_id: $cartId) @client {
7
+ query GetGiftOptions($cartId: String!) {
8
+ cart(cart_id: $cartId) {
11
9
  id
12
- include_gift_receipt
13
- include_printed_card
14
- local_gift_message
10
+ ...GiftOptionsFragment
11
+ }
12
+ }
13
+ ${GiftOptionsFragment}
14
+ `;
15
+
16
+ // Currently Commerce only
17
+ const SET_GIFT_OPTIONS_ON_CART = gql`
18
+ mutation SetGiftOptionsOnCart(
19
+ $cartId: String!
20
+ $giftMessage: GiftMessageInput
21
+ $giftReceiptIncluded: Boolean!
22
+ $printedCardIncluded: Boolean!
23
+ ) {
24
+ setGiftOptionsOnCart(
25
+ input: {
26
+ cart_id: $cartId
27
+ gift_message: $giftMessage
28
+ gift_receipt_included: $giftReceiptIncluded
29
+ printed_card_included: $printedCardIncluded
30
+ }
31
+ ) {
32
+ cart {
33
+ id
34
+ ...CartPageFragment
35
+ ...GiftOptionsFragment
36
+ }
15
37
  }
16
38
  }
39
+ ${CartPageFragment}
40
+ ${GiftOptionsFragment}
17
41
  `;
18
42
 
19
43
  export default {
20
- getGiftOptionsQuery: GET_GIFT_OPTIONS
44
+ getGiftOptionsQuery: GET_GIFT_OPTIONS,
45
+ setGiftOptionsOnCartMutation: SET_GIFT_OPTIONS_ON_CART
21
46
  };
@@ -0,0 +1,19 @@
1
+ import { gql } from '@apollo/client';
2
+
3
+ import { GiftOptionsSummaryFragment } from '../../PriceSummary/queries/giftOptionsSummary';
4
+
5
+ export const GiftOptionsFragment = gql`
6
+ fragment GiftOptionsFragment on Cart {
7
+ __typename
8
+ id
9
+ gift_message {
10
+ from
11
+ to
12
+ message
13
+ }
14
+ gift_receipt_included
15
+ printed_card_included
16
+ ...GiftOptionsSummaryFragment
17
+ }
18
+ ${GiftOptionsSummaryFragment}
19
+ `;