@envive-ai/react-hooks 0.3.46 → 0.3.48
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/application/commerce-api.cjs +73 -3
- package/dist/application/commerce-api.d.cts +16 -1
- package/dist/application/commerce-api.d.ts +16 -1
- package/dist/application/commerce-api.js +73 -3
- package/dist/application/models/api/userEvent.d.cts +9 -2
- package/dist/application/models/api/userEvent.d.ts +9 -2
- package/dist/application/models/featureGates.cjs +2 -1
- package/dist/application/models/featureGates.d.cts +2 -1
- package/dist/application/models/featureGates.d.ts +2 -1
- package/dist/application/models/featureGates.js +2 -1
- package/dist/application/models/graphql/queries/getProductOptionsQuery.cjs +33 -0
- package/dist/application/models/graphql/queries/getProductOptionsQuery.d.cts +28 -0
- package/dist/application/models/graphql/queries/getProductOptionsQuery.d.ts +28 -0
- package/dist/application/models/graphql/queries/getProductOptionsQuery.js +32 -0
- package/dist/application/models/validators/validateResponse.cjs +2 -1
- package/dist/application/models/validators/validateResponse.js +2 -1
- package/dist/application/models/validators/validateUserEvent.cjs +3 -3
- package/dist/application/models/validators/validateUserEvent.js +3 -3
- package/dist/application/utils/index.cjs +2 -0
- package/dist/application/utils/index.d.cts +2 -1
- package/dist/application/utils/index.d.ts +2 -1
- package/dist/application/utils/index.js +2 -1
- package/dist/application/utils/messageFromAddToCartEvent.cjs +32 -0
- package/dist/application/utils/messageFromAddToCartEvent.d.cts +19 -0
- package/dist/application/utils/messageFromAddToCartEvent.d.ts +19 -0
- package/dist/application/utils/messageFromAddToCartEvent.js +31 -0
- package/dist/atoms/app/index.cjs +1 -1
- package/dist/atoms/app/index.d.cts +7 -7
- package/dist/atoms/app/index.d.ts +7 -7
- package/dist/atoms/app/index.js +1 -1
- package/dist/atoms/app/variant.d.cts +6 -6
- package/dist/atoms/app/variant.d.ts +6 -6
- package/dist/atoms/chat/chatState.d.cts +19 -19
- package/dist/atoms/chat/chatState.d.ts +19 -19
- package/dist/atoms/chat/form.d.cts +2 -2
- package/dist/atoms/chat/form.d.ts +3 -3
- package/dist/atoms/chat/index.d.cts +2 -2
- package/dist/atoms/chat/index.d.ts +3 -3
- package/dist/atoms/chat/lastMessage.d.cts +2 -2
- package/dist/atoms/chat/lastMessage.d.ts +2 -2
- package/dist/atoms/chat/messageQueue.d.cts +6 -6
- package/dist/atoms/chat/messageQueue.d.ts +7 -7
- package/dist/atoms/chat/performanceMetrics.d.cts +6 -6
- package/dist/atoms/chat/performanceMetrics.d.ts +6 -6
- package/dist/atoms/chat/renderedWidgetRefs.d.cts +2 -2
- package/dist/atoms/chat/renderedWidgetRefs.d.ts +3 -3
- package/dist/atoms/chat/replies.d.cts +1 -1
- package/dist/atoms/chat/replies.d.ts +3 -3
- package/dist/atoms/chat/suggestions.d.cts +2 -2
- package/dist/atoms/chat/suggestions.d.ts +3 -3
- package/dist/atoms/envive/enviveConfig.cjs +7 -4
- package/dist/atoms/envive/enviveConfig.d.cts +16 -14
- package/dist/atoms/envive/enviveConfig.d.ts +16 -14
- package/dist/atoms/envive/enviveConfig.js +6 -4
- package/dist/atoms/globalSearch/globalSearch.d.cts +5 -5
- package/dist/atoms/globalSearch/globalSearch.d.ts +5 -5
- package/dist/atoms/org/customerService.d.cts +6 -6
- package/dist/atoms/org/customerService.d.ts +6 -6
- package/dist/atoms/org/graphqlConfig.cjs +1 -1
- package/dist/atoms/org/graphqlConfig.d.cts +6 -6
- package/dist/atoms/org/graphqlConfig.d.ts +6 -6
- package/dist/atoms/org/graphqlConfig.js +1 -1
- package/dist/atoms/org/newOrgConfigAtom.d.cts +2 -2
- package/dist/atoms/org/newOrgConfigAtom.d.ts +2 -2
- package/dist/atoms/org/orgAnalyticsConfig.d.cts +4 -4
- package/dist/atoms/org/orgAnalyticsConfig.d.ts +4 -4
- package/dist/atoms/search/chatSearch.d.cts +17 -17
- package/dist/atoms/search/chatSearch.d.ts +17 -17
- package/dist/atoms/search/searchAPI.d.cts +13 -13
- package/dist/atoms/search/searchAPI.d.ts +13 -13
- package/dist/atoms/search/types.d.ts +1 -1
- package/dist/atoms/widget/chatPreviewLoading.d.cts +2 -2
- package/dist/atoms/widget/chatPreviewLoading.d.ts +2 -2
- package/dist/contexts/enviveConfigContext/enviveConfigContext.cjs +6 -4
- package/dist/contexts/enviveConfigContext/enviveConfigContext.d.cts +3 -1
- package/dist/contexts/enviveConfigContext/enviveConfigContext.d.ts +3 -1
- package/dist/contexts/enviveConfigContext/enviveConfigContext.js +6 -4
- package/dist/contexts/enviveContext/enviveContext.cjs +7 -3
- package/dist/contexts/enviveContext/enviveContext.d.cts +5 -1
- package/dist/contexts/enviveContext/enviveContext.d.ts +5 -1
- package/dist/contexts/enviveContext/enviveContext.js +7 -3
- package/dist/contexts/featureFlagServiceContext/featureFlagServiceContext.cjs +2 -97
- package/dist/contexts/featureFlagServiceContext/featureFlagServiceContext.d.cts +3 -25
- package/dist/contexts/featureFlagServiceContext/featureFlagServiceContext.d.ts +3 -25
- package/dist/contexts/featureFlagServiceContext/featureFlagServiceContext.js +3 -97
- package/dist/contexts/featureFlagServiceContext/index.cjs +2 -1
- package/dist/contexts/featureFlagServiceContext/index.d.cts +2 -1
- package/dist/contexts/featureFlagServiceContext/index.d.ts +2 -1
- package/dist/contexts/featureFlagServiceContext/index.js +2 -1
- package/dist/contexts/graphqlContext/mockV3Config.cjs +1 -1
- package/dist/contexts/graphqlContext/mockV3Config.js +1 -1
- package/dist/contexts/localStorageContext/index.cjs +2 -1
- package/dist/contexts/localStorageContext/index.d.cts +2 -1
- package/dist/contexts/localStorageContext/index.d.ts +2 -1
- package/dist/contexts/localStorageContext/index.js +2 -1
- package/dist/contexts/localStorageContext/localStorageContext.cjs +5 -11
- package/dist/contexts/localStorageContext/localStorageContext.d.cts +3 -7
- package/dist/contexts/localStorageContext/localStorageContext.d.ts +3 -7
- package/dist/contexts/localStorageContext/localStorageContext.js +3 -8
- package/dist/contexts/localStorageContext/localStorageKeys.cjs +12 -0
- package/dist/contexts/localStorageContext/localStorageKeys.d.cts +9 -0
- package/dist/contexts/localStorageContext/localStorageKeys.d.ts +9 -0
- package/dist/contexts/localStorageContext/localStorageKeys.js +11 -0
- package/dist/contexts/salesAgentContext/chatAPI.cjs +13 -2
- package/dist/contexts/salesAgentContext/chatAPI.d.cts +4 -1
- package/dist/contexts/salesAgentContext/chatAPI.d.ts +4 -1
- package/dist/contexts/salesAgentContext/chatAPI.js +14 -3
- package/dist/contexts/salesAgentContext/salesAgentContext.cjs +12 -4
- package/dist/contexts/salesAgentContext/salesAgentContext.js +12 -4
- package/dist/contexts/systemSettingsContext/systemSettingsContext.d.cts +2 -2
- package/dist/contexts/types.cjs +1 -1
- package/dist/contexts/types.d.cts +3 -1
- package/dist/contexts/types.d.ts +3 -1
- package/dist/contexts/types.js +1 -1
- package/dist/contexts/typesV3.cjs +1 -1
- package/dist/contexts/typesV3.d.cts +11 -2
- package/dist/contexts/typesV3.d.ts +12 -3
- package/dist/contexts/typesV3.js +1 -1
- package/dist/hooks/AddToCart/addToCartErrorMessage.cjs +7 -0
- package/dist/hooks/AddToCart/addToCartErrorMessage.d.cts +5 -0
- package/dist/hooks/AddToCart/addToCartErrorMessage.d.ts +5 -0
- package/dist/hooks/AddToCart/addToCartErrorMessage.js +6 -0
- package/dist/hooks/AddToCart/createAddToCartUserEvent.cjs +20 -0
- package/dist/hooks/AddToCart/createAddToCartUserEvent.d.cts +17 -0
- package/dist/hooks/AddToCart/createAddToCartUserEvent.d.ts +17 -0
- package/dist/hooks/AddToCart/createAddToCartUserEvent.js +19 -0
- package/dist/hooks/AddToCart/index.cjs +9 -0
- package/dist/hooks/AddToCart/index.d.cts +5 -0
- package/dist/hooks/AddToCart/index.d.ts +5 -0
- package/dist/hooks/AddToCart/index.js +6 -0
- package/dist/hooks/AddToCart/parseAddToCartFunction.cjs +27 -0
- package/dist/hooks/AddToCart/parseAddToCartFunction.d.cts +13 -0
- package/dist/hooks/AddToCart/parseAddToCartFunction.d.ts +13 -0
- package/dist/hooks/AddToCart/parseAddToCartFunction.js +27 -0
- package/dist/hooks/AddToCart/useAddToCart.cjs +24 -0
- package/dist/hooks/AddToCart/useAddToCart.d.cts +14 -0
- package/dist/hooks/AddToCart/useAddToCart.d.ts +14 -0
- package/dist/hooks/AddToCart/useAddToCart.js +23 -0
- package/dist/hooks/GrabAndScroll/useGrabAndScroll.d.cts +2 -2
- package/dist/hooks/LocalStorageOperations/useLocalStorageOperations.cjs +6 -5
- package/dist/hooks/LocalStorageOperations/useLocalStorageOperations.js +3 -2
- package/dist/hooks/PageViewedEvent/usePageViewedEvent.cjs +5 -2
- package/dist/hooks/PageViewedEvent/usePageViewedEvent.js +5 -2
- package/dist/hooks/ProductDetails/index.cjs +9 -0
- package/dist/hooks/ProductDetails/index.d.cts +4 -0
- package/dist/hooks/ProductDetails/index.d.ts +4 -0
- package/dist/hooks/ProductDetails/index.js +4 -0
- package/dist/hooks/ProductDetails/useProductDetails.cjs +53 -0
- package/dist/hooks/ProductDetails/useProductDetails.d.cts +20 -0
- package/dist/hooks/ProductDetails/useProductDetails.d.ts +20 -0
- package/dist/hooks/ProductDetails/useProductDetails.js +52 -0
- package/dist/hooks/ProductDetails/variantSelection.cjs +42 -0
- package/dist/hooks/ProductDetails/variantSelection.d.cts +35 -0
- package/dist/hooks/ProductDetails/variantSelection.d.ts +35 -0
- package/dist/hooks/ProductDetails/variantSelection.js +38 -0
- package/dist/hooks/Search/useSearch.d.cts +1 -1
- package/dist/hooks/Search/useSearch.d.ts +1 -1
- package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.cjs +11 -2
- package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.d.cts +1 -10
- package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.d.ts +1 -10
- package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.js +11 -2
- package/dist/hooks/WidgetInteraction/types.cjs +12 -2
- package/dist/hooks/WidgetInteraction/types.d.cts +28 -3
- package/dist/hooks/WidgetInteraction/types.d.ts +28 -3
- package/dist/hooks/WidgetInteraction/types.js +12 -2
- package/dist/hooks/utils.d.cts +1 -1
- package/dist/hooks/utils.d.ts +1 -1
- package/dist/services/amplitudeService/amplitudeService.cjs +16 -10
- package/dist/services/amplitudeService/amplitudeService.d.cts +5 -2
- package/dist/services/amplitudeService/amplitudeService.d.ts +5 -2
- package/dist/services/amplitudeService/amplitudeService.js +14 -7
- package/dist/services/enviveConfigService/enviveConfigService.cjs +3 -4
- package/dist/services/enviveConfigService/enviveConfigService.d.cts +1 -1
- package/dist/services/enviveConfigService/enviveConfigService.d.ts +1 -1
- package/dist/services/enviveConfigService/enviveConfigService.js +2 -3
- package/dist/services/enviveConfigService/fetchGraphQLConfig.cjs +8 -7
- package/dist/services/enviveConfigService/fetchGraphQLConfig.js +8 -6
- package/dist/services/featureFlagService/index.cjs +101 -0
- package/dist/services/featureFlagService/index.d.cts +28 -0
- package/dist/services/featureFlagService/index.d.ts +28 -0
- package/dist/services/featureFlagService/index.js +101 -0
- package/dist/services/ga4ProjectionService/ga4EventSchema.cjs +8 -2
- package/dist/services/ga4ProjectionService/ga4EventSchema.js +8 -2
- package/dist/services/hardcopyService/hardcopyService.d.cts +1 -1
- package/dist/services/hardcopyService/hardcopyService.d.ts +1 -1
- package/dist/services/urlResolverWarmupService/index.cjs +54 -0
- package/dist/services/urlResolverWarmupService/index.d.cts +40 -0
- package/dist/services/urlResolverWarmupService/index.d.ts +40 -0
- package/dist/services/urlResolverWarmupService/index.js +54 -0
- package/dist/types/enviveConfig.d.cts +6 -2
- package/dist/types/enviveConfig.d.ts +6 -2
- package/dist/types/index.d.cts +2 -2
- package/dist/types/index.d.ts +2 -2
- package/package.json +17 -1
- package/src/application/__tests__/commerceApiAddToCartRehydration.test.ts +126 -0
- package/src/application/commerce-api.ts +142 -23
- package/src/application/models/api/userEvent.ts +8 -1
- package/src/application/models/featureGates.ts +5 -0
- package/src/application/models/graphql/queries/getProductOptionsQuery.ts +59 -0
- package/src/application/models/validators/__tests__/validateUserEvent.test.ts +41 -0
- package/src/application/models/validators/validateResponse.ts +1 -0
- package/src/application/models/validators/validateUserEvent.ts +7 -2
- package/src/application/utils/__tests__/messageFromAddToCartEvent.test.ts +109 -0
- package/src/application/utils/index.ts +1 -0
- package/src/application/utils/messageFromAddToCartEvent.ts +32 -0
- package/src/atoms/app/index.ts +1 -1
- package/src/atoms/envive/enviveConfig.ts +6 -1
- package/src/atoms/org/graphqlConfig.ts +1 -1
- package/src/contexts/enviveConfigContext/enviveConfigContext.tsx +5 -0
- package/src/contexts/enviveContext/enviveContext.tsx +10 -0
- package/src/contexts/featureFlagServiceContext/featureFlagServiceContext.tsx +6 -173
- package/src/contexts/localStorageContext/localStorageContext.tsx +2 -5
- package/src/contexts/localStorageContext/localStorageKeys.ts +8 -0
- package/src/contexts/salesAgentContext/__tests__/chatAPI.test.tsx +63 -0
- package/src/contexts/salesAgentContext/chatAPI.ts +20 -1
- package/src/contexts/salesAgentContext/salesAgentContext.tsx +10 -0
- package/src/contexts/types.ts +2 -0
- package/src/contexts/typesV3.ts +14 -0
- package/src/hooks/AddToCart/__tests__/parseAddToCartFunction.test.ts +31 -0
- package/src/hooks/AddToCart/__tests__/useAddToCart.test.tsx +204 -0
- package/src/hooks/AddToCart/addToCartErrorMessage.ts +2 -0
- package/src/hooks/AddToCart/createAddToCartUserEvent.ts +25 -0
- package/src/hooks/AddToCart/index.ts +4 -0
- package/src/hooks/AddToCart/parseAddToCartFunction.ts +31 -0
- package/src/hooks/AddToCart/useAddToCart.ts +35 -0
- package/src/hooks/PageViewedEvent/usePageViewedEvent.ts +4 -0
- package/src/hooks/ProductDetails/__tests__/useProductDetails.test.tsx +129 -0
- package/src/hooks/ProductDetails/__tests__/variantSelection.test.ts +145 -0
- package/src/hooks/ProductDetails/index.ts +2 -0
- package/src/hooks/ProductDetails/useProductDetails.ts +62 -0
- package/src/hooks/ProductDetails/variantSelection.ts +61 -0
- package/src/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.ts +15 -0
- package/src/hooks/WidgetInteraction/types.ts +42 -1
- package/src/services/amplitudeService/__tests__/amplitudeService.test.ts +8 -8
- package/src/services/amplitudeService/amplitudeService.ts +42 -23
- package/src/services/enviveConfigService/enviveConfigService.ts +2 -2
- package/src/services/enviveConfigService/fetchGraphQLConfig.ts +17 -6
- package/src/services/featureFlagService/index.ts +176 -0
- package/src/services/ga4ProjectionService/__tests__/ga4ProjectionService.test.ts +36 -0
- package/src/services/ga4ProjectionService/ga4EventSchema.ts +7 -0
- package/src/services/urlResolverWarmupService/__tests__/urlResolverWarmupService.test.ts +85 -0
- package/src/services/urlResolverWarmupService/index.ts +89 -0
- package/src/types/enviveConfig.ts +3 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { UserEventCategory } from '@spiffy-ai/commerce-api-client';
|
|
2
|
+
import { renderHook } from '@testing-library/react';
|
|
3
|
+
import { Provider, createStore } from 'jotai';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { addToCartCallbackAtom, enviveConfigAtom } from 'src/atoms/envive/enviveConfig';
|
|
6
|
+
import { newOrgConfigAtom } from 'src/atoms/org';
|
|
7
|
+
import { GraphQlConfigValues } from 'src/contexts/graphqlContext';
|
|
8
|
+
import { createAddToCartUserEvent } from '../createAddToCartUserEvent';
|
|
9
|
+
import { useAddToCart } from '../useAddToCart';
|
|
10
|
+
|
|
11
|
+
const enabledUiConfig = {
|
|
12
|
+
frontendConfig: {
|
|
13
|
+
uiConfigs: {
|
|
14
|
+
lookAndFeel: {
|
|
15
|
+
addToCart: {
|
|
16
|
+
enabled: true,
|
|
17
|
+
backgroundColor: 'var(--envive-colors-background-dark)',
|
|
18
|
+
textColor: 'var(--envive-colors-text-light)',
|
|
19
|
+
ctaText: 'Add to Cart',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
} as unknown as GraphQlConfigValues;
|
|
25
|
+
|
|
26
|
+
const disabledUiConfig = {
|
|
27
|
+
frontendConfig: {
|
|
28
|
+
uiConfigs: {
|
|
29
|
+
lookAndFeel: {
|
|
30
|
+
addToCart: { enabled: false },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
} as unknown as GraphQlConfigValues;
|
|
35
|
+
|
|
36
|
+
const configFunctionUiConfig = (addToCartFunction: string) =>
|
|
37
|
+
({
|
|
38
|
+
frontendConfig: {
|
|
39
|
+
uiConfigs: {
|
|
40
|
+
lookAndFeel: {
|
|
41
|
+
addToCart: {
|
|
42
|
+
enabled: true,
|
|
43
|
+
addToCartFunction,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
}) as unknown as GraphQlConfigValues;
|
|
49
|
+
|
|
50
|
+
const renderUseAddToCart = (store: ReturnType<typeof createStore>) => {
|
|
51
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
52
|
+
// The jotai Provider is typed against the root @types/react (18) while this
|
|
53
|
+
// package resolves @types/react 19; the cast bridges the two ReactNode types.
|
|
54
|
+
<Provider store={store}>{children as React.ReactElement<unknown, string>}</Provider>
|
|
55
|
+
);
|
|
56
|
+
return renderHook(() => useAddToCart(), { wrapper });
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
describe('addToCartCallbackAtom', () => {
|
|
60
|
+
it('returns undefined when no config is set', () => {
|
|
61
|
+
const store = createStore();
|
|
62
|
+
expect(store.get(addToCartCallbackAtom)).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('returns the callback from the envive config', () => {
|
|
66
|
+
const store = createStore();
|
|
67
|
+
const addToCart = vi.fn();
|
|
68
|
+
store.set(enviveConfigAtom, { identifyingPrefix: 'test', addToCart });
|
|
69
|
+
expect(store.get(addToCartCallbackAtom)).toBe(addToCart);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('useAddToCart', () => {
|
|
74
|
+
it('is enabled when both the callback and the enabled ui config are present', () => {
|
|
75
|
+
const store = createStore();
|
|
76
|
+
const addToCart = vi.fn();
|
|
77
|
+
store.set(enviveConfigAtom, { identifyingPrefix: 'test', addToCart });
|
|
78
|
+
store.set(newOrgConfigAtom, enabledUiConfig);
|
|
79
|
+
|
|
80
|
+
const { result } = renderUseAddToCart(store);
|
|
81
|
+
|
|
82
|
+
expect(result.current.isAddToCartEnabled).toBe(true);
|
|
83
|
+
expect(result.current.addToCart).toBe(addToCart);
|
|
84
|
+
expect(result.current.config).toEqual({
|
|
85
|
+
enabled: true,
|
|
86
|
+
backgroundColor: 'var(--envive-colors-background-dark)',
|
|
87
|
+
textColor: 'var(--envive-colors-text-light)',
|
|
88
|
+
ctaText: 'Add to Cart',
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('is disabled when only the callback is present', () => {
|
|
93
|
+
const store = createStore();
|
|
94
|
+
store.set(enviveConfigAtom, { identifyingPrefix: 'test', addToCart: vi.fn() });
|
|
95
|
+
|
|
96
|
+
const { result } = renderUseAddToCart(store);
|
|
97
|
+
|
|
98
|
+
expect(result.current.isAddToCartEnabled).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('is disabled when only the ui config is present', () => {
|
|
102
|
+
const store = createStore();
|
|
103
|
+
store.set(newOrgConfigAtom, enabledUiConfig);
|
|
104
|
+
|
|
105
|
+
const { result } = renderUseAddToCart(store);
|
|
106
|
+
|
|
107
|
+
expect(result.current.isAddToCartEnabled).toBe(false);
|
|
108
|
+
expect(result.current.addToCart).toBeUndefined();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('is disabled when the ui config section is disabled', () => {
|
|
112
|
+
const store = createStore();
|
|
113
|
+
store.set(enviveConfigAtom, { identifyingPrefix: 'test', addToCart: vi.fn() });
|
|
114
|
+
store.set(newOrgConfigAtom, disabledUiConfig);
|
|
115
|
+
|
|
116
|
+
const { result } = renderUseAddToCart(store);
|
|
117
|
+
|
|
118
|
+
expect(result.current.isAddToCartEnabled).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('is disabled when neither gate is present', () => {
|
|
122
|
+
const store = createStore();
|
|
123
|
+
|
|
124
|
+
const { result } = renderUseAddToCart(store);
|
|
125
|
+
|
|
126
|
+
expect(result.current.isAddToCartEnabled).toBe(false);
|
|
127
|
+
expect(result.current.addToCart).toBeUndefined();
|
|
128
|
+
expect(result.current.config).toBeUndefined();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('falls back to the parsed config function when no window callback is set', () => {
|
|
132
|
+
const store = createStore();
|
|
133
|
+
store.set(
|
|
134
|
+
newOrgConfigAtom,
|
|
135
|
+
configFunctionUiConfig('function(params) { globalThis.__uatCalls = [params]; }'),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const { result } = renderUseAddToCart(store);
|
|
139
|
+
|
|
140
|
+
expect(result.current.isAddToCartEnabled).toBe(true);
|
|
141
|
+
result.current.addToCart?.({ product_id: 'config-fn' });
|
|
142
|
+
expect((globalThis as Record<string, unknown>).__uatCalls).toEqual([
|
|
143
|
+
{ product_id: 'config-fn' },
|
|
144
|
+
]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('prefers the window callback over the config function when both are present', () => {
|
|
148
|
+
const store = createStore();
|
|
149
|
+
const windowAddToCart = vi.fn();
|
|
150
|
+
store.set(enviveConfigAtom, { identifyingPrefix: 'test', addToCart: windowAddToCart });
|
|
151
|
+
store.set(
|
|
152
|
+
newOrgConfigAtom,
|
|
153
|
+
configFunctionUiConfig('function(params) { globalThis.__uatCalls = [params]; }'),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const { result } = renderUseAddToCart(store);
|
|
157
|
+
|
|
158
|
+
expect(result.current.isAddToCartEnabled).toBe(true);
|
|
159
|
+
expect(result.current.addToCart).toBe(windowAddToCart);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('is disabled when the config function string is invalid and no window callback is set', () => {
|
|
163
|
+
const store = createStore();
|
|
164
|
+
store.set(newOrgConfigAtom, configFunctionUiConfig('function( { broken'));
|
|
165
|
+
|
|
166
|
+
const { result } = renderUseAddToCart(store);
|
|
167
|
+
|
|
168
|
+
expect(result.current.isAddToCartEnabled).toBe(false);
|
|
169
|
+
expect(result.current.addToCart).toBeUndefined();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('createAddToCartUserEvent', () => {
|
|
174
|
+
it('creates an AddToCart user event with the product attributes', () => {
|
|
175
|
+
const event = createAddToCartUserEvent({
|
|
176
|
+
productId: 'product-1',
|
|
177
|
+
parentProductId: 'group-1',
|
|
178
|
+
url: 'https://example.com/product-1',
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(event.category).toBe(UserEventCategory.AddToCart);
|
|
182
|
+
expect(event.eventId).toEqual(expect.any(String));
|
|
183
|
+
expect(event.createdAt).toEqual(expect.any(String));
|
|
184
|
+
expect(event).toMatchObject({
|
|
185
|
+
attributes: {
|
|
186
|
+
productId: 'product-1',
|
|
187
|
+
parentProductId: 'group-1',
|
|
188
|
+
url: 'https://example.com/product-1',
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('defaults optional attributes to empty strings', () => {
|
|
194
|
+
const event = createAddToCartUserEvent({ productId: 'product-1' });
|
|
195
|
+
|
|
196
|
+
expect(event).toMatchObject({
|
|
197
|
+
attributes: {
|
|
198
|
+
productId: 'product-1',
|
|
199
|
+
parentProductId: '',
|
|
200
|
+
url: '',
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { UserEventCategory } from '@spiffy-ai/commerce-api-client';
|
|
2
|
+
import { UserEvent } from 'src/application/models';
|
|
3
|
+
import { v4 as uuid } from 'uuid';
|
|
4
|
+
|
|
5
|
+
export const createAddToCartUserEvent = ({
|
|
6
|
+
productId,
|
|
7
|
+
parentProductId,
|
|
8
|
+
url,
|
|
9
|
+
productTitle,
|
|
10
|
+
}: {
|
|
11
|
+
productId: string;
|
|
12
|
+
parentProductId?: string;
|
|
13
|
+
url?: string;
|
|
14
|
+
productTitle?: string;
|
|
15
|
+
}): UserEvent => ({
|
|
16
|
+
eventId: uuid(),
|
|
17
|
+
createdAt: new Date().toISOString(),
|
|
18
|
+
category: UserEventCategory.AddToCart,
|
|
19
|
+
attributes: {
|
|
20
|
+
productId,
|
|
21
|
+
parentProductId: parentProductId ?? '',
|
|
22
|
+
url: url ?? '',
|
|
23
|
+
productTitle,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import Logger from 'src/application/logging/logger';
|
|
2
|
+
import { AddToCartCallback } from 'src/types/enviveConfig';
|
|
3
|
+
|
|
4
|
+
const logger = new Logger('parseAddToCartFunction');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parses a merchant-supplied `add_to_cart_function` config string into a
|
|
8
|
+
* callable. The string is expected to evaluate to a function matching
|
|
9
|
+
* AddToCartCallback, e.g. "function(params) { window.myCart.add(params.product_id); }".
|
|
10
|
+
*/
|
|
11
|
+
export const parseAddToCartFunction = (
|
|
12
|
+
fnString: string | undefined,
|
|
13
|
+
): AddToCartCallback | undefined => {
|
|
14
|
+
if (!fnString) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const parsed = new Function(`return (${fnString});`)();
|
|
20
|
+
if (typeof parsed !== 'function') {
|
|
21
|
+
logger.logError('add_to_cart_function did not evaluate to a function', undefined, {
|
|
22
|
+
fnString,
|
|
23
|
+
});
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return parsed as AddToCartCallback;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
logger.logError('Failed to parse add_to_cart_function', error, { fnString });
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useAtomValue } from 'jotai';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { addToCartCallbackAtom } from 'src/atoms/envive/enviveConfig';
|
|
4
|
+
import { newOrgConfigAtom } from 'src/atoms/org';
|
|
5
|
+
import { AddToCartConfig, OrgUIConfigV3 } from 'src/contexts/typesV3';
|
|
6
|
+
import { AddToCartCallback } from 'src/types/enviveConfig';
|
|
7
|
+
import { parseAddToCartFunction } from './parseAddToCartFunction';
|
|
8
|
+
|
|
9
|
+
export interface AddToCartHookResult {
|
|
10
|
+
/** True only when the org UI config enables add-to-cart AND a callback is available. */
|
|
11
|
+
isAddToCartEnabled: boolean;
|
|
12
|
+
addToCart?: AddToCartCallback;
|
|
13
|
+
config?: AddToCartConfig;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const useAddToCart = (): AddToCartHookResult => {
|
|
17
|
+
const windowAddToCart = useAtomValue(addToCartCallbackAtom);
|
|
18
|
+
const newOrgConfig = useAtomValue(newOrgConfigAtom);
|
|
19
|
+
const uiConfigs = newOrgConfig?.frontendConfig?.uiConfigs as OrgUIConfigV3 | undefined;
|
|
20
|
+
const config = uiConfigs?.lookAndFeel?.addToCart;
|
|
21
|
+
|
|
22
|
+
const configAddToCart = useMemo(
|
|
23
|
+
() => parseAddToCartFunction(config?.addToCartFunction),
|
|
24
|
+
[config?.addToCartFunction],
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// window._spiffy/_envive.addToCart always overrides the config-provided function.
|
|
28
|
+
const addToCart = windowAddToCart ?? configAddToCart;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
isAddToCartEnabled: Boolean(config?.enabled && addToCart),
|
|
32
|
+
addToCart,
|
|
33
|
+
config,
|
|
34
|
+
};
|
|
35
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useAtomValue } from 'jotai';
|
|
2
2
|
import { useEffect, useRef } from 'react';
|
|
3
3
|
import { VariantTypeEnum } from 'src/application/models';
|
|
4
|
+
import { getQueryParam } from 'src/application/utils/urlsParser';
|
|
4
5
|
import { hasParsedVariantInfoAtom, variantInfoAtom } from 'src/atoms/app/variant';
|
|
5
6
|
import { EnviveMetricsEventName, useAmplitude } from 'src/contexts/amplitudeContext';
|
|
6
7
|
import { usePage } from 'src/contexts/pageContext';
|
|
@@ -87,6 +88,8 @@ export const usePageViewedEvent = () => {
|
|
|
87
88
|
|
|
88
89
|
hasFired.current = true;
|
|
89
90
|
|
|
91
|
+
const envivePlacement = getQueryParam('envive_placement');
|
|
92
|
+
|
|
90
93
|
trackEvent({
|
|
91
94
|
eventName: EnviveMetricsEventName.PageViewed,
|
|
92
95
|
eventProps: {
|
|
@@ -97,6 +100,7 @@ export const usePageViewedEvent = () => {
|
|
|
97
100
|
'context.supported': isSupported,
|
|
98
101
|
'context.ready': isSupported,
|
|
99
102
|
'context.page_variant_id': variantInfo.variantId,
|
|
103
|
+
...(envivePlacement ? { 'context.envive_placement': envivePlacement } : {}),
|
|
100
104
|
},
|
|
101
105
|
});
|
|
102
106
|
}, [isLoading, isSupported, hasParsedVariantInfo, pageVariantInfo, variantInfo, trackEvent]);
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
2
|
+
import CommerceApiClient from 'src/application/commerce-api';
|
|
3
|
+
import { ProductDetails } from 'src/application/models/graphql/queries/getProductOptionsQuery';
|
|
4
|
+
import { useProductDetails } from '../useProductDetails';
|
|
5
|
+
|
|
6
|
+
const productDetails: ProductDetails = {
|
|
7
|
+
productId: 'small-time-teller-gold-black-gold',
|
|
8
|
+
virtualParentId: 'small-time-teller',
|
|
9
|
+
title: 'Small Time Teller',
|
|
10
|
+
options: [{ name: 'Color', values: ['Gold/Black/Gold', 'Silver'] }],
|
|
11
|
+
variants: [
|
|
12
|
+
{
|
|
13
|
+
variantId: 'variant-1',
|
|
14
|
+
available: true,
|
|
15
|
+
originalPrice: 125,
|
|
16
|
+
salePrice: 100,
|
|
17
|
+
imageUrl: 'https://example.com/variant-1.jpg',
|
|
18
|
+
url: 'https://example.com/products/small-time-teller?variant=1',
|
|
19
|
+
options: [{ name: 'Color', value: 'Gold/Black/Gold' }],
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe('useProductDetails', () => {
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
vi.restoreAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('stays idle and does not fetch when productId is undefined', () => {
|
|
30
|
+
const retrieveSpy = vi.spyOn(CommerceApiClient, 'retrieveProductDetails');
|
|
31
|
+
|
|
32
|
+
const { result } = renderHook(() => useProductDetails(undefined));
|
|
33
|
+
|
|
34
|
+
expect(result.current).toEqual({ data: undefined, isLoading: false, error: undefined });
|
|
35
|
+
expect(retrieveSpy).not.toHaveBeenCalled();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns the product details after a successful fetch', async () => {
|
|
39
|
+
const retrieveSpy = vi
|
|
40
|
+
.spyOn(CommerceApiClient, 'retrieveProductDetails')
|
|
41
|
+
.mockResolvedValue(productDetails);
|
|
42
|
+
|
|
43
|
+
const { result } = renderHook(() => useProductDetails('small-time-teller-gold-black-gold'));
|
|
44
|
+
|
|
45
|
+
expect(result.current.isLoading).toBe(true);
|
|
46
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
47
|
+
|
|
48
|
+
expect(retrieveSpy).toHaveBeenCalledWith('small-time-teller-gold-black-gold');
|
|
49
|
+
expect(result.current.data).toEqual(productDetails);
|
|
50
|
+
expect(result.current.error).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('surfaces an error when the fetch fails', async () => {
|
|
54
|
+
vi.spyOn(CommerceApiClient, 'retrieveProductDetails').mockRejectedValue(
|
|
55
|
+
new Error('GraphQL request failed: Internal Server Error'),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const { result } = renderHook(() => useProductDetails('missing-product'));
|
|
59
|
+
|
|
60
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
61
|
+
|
|
62
|
+
expect(result.current.data).toBeUndefined();
|
|
63
|
+
expect(result.current.error).toEqual(
|
|
64
|
+
new Error('GraphQL request failed: Internal Server Error'),
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('wraps non-Error rejections in an Error', async () => {
|
|
69
|
+
vi.spyOn(CommerceApiClient, 'retrieveProductDetails').mockRejectedValue('boom');
|
|
70
|
+
|
|
71
|
+
const { result } = renderHook(() => useProductDetails('missing-product'));
|
|
72
|
+
|
|
73
|
+
await waitFor(() => expect(result.current.error).toEqual(new Error('boom')));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('resets to idle when productId becomes undefined', async () => {
|
|
77
|
+
vi.spyOn(CommerceApiClient, 'retrieveProductDetails').mockResolvedValue(productDetails);
|
|
78
|
+
|
|
79
|
+
const { result, rerender } = renderHook(({ productId }) => useProductDetails(productId), {
|
|
80
|
+
initialProps: { productId: 'small-time-teller-gold-black-gold' as string | undefined },
|
|
81
|
+
});
|
|
82
|
+
await waitFor(() => expect(result.current.data).toEqual(productDetails));
|
|
83
|
+
|
|
84
|
+
rerender({ productId: undefined });
|
|
85
|
+
|
|
86
|
+
expect(result.current).toEqual({ data: undefined, isLoading: false, error: undefined });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('ignores a stale response when productId changes mid-flight', async () => {
|
|
90
|
+
let resolveFirst: (details: ProductDetails) => void = () => {};
|
|
91
|
+
const staleDetails = { ...productDetails, productId: 'stale-product' };
|
|
92
|
+
vi.spyOn(CommerceApiClient, 'retrieveProductDetails')
|
|
93
|
+
.mockImplementationOnce(
|
|
94
|
+
() =>
|
|
95
|
+
new Promise(resolve => {
|
|
96
|
+
resolveFirst = resolve;
|
|
97
|
+
}),
|
|
98
|
+
)
|
|
99
|
+
.mockResolvedValueOnce(productDetails);
|
|
100
|
+
|
|
101
|
+
const { result, rerender } = renderHook(({ productId }) => useProductDetails(productId), {
|
|
102
|
+
initialProps: { productId: 'stale-product' },
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
rerender({ productId: 'small-time-teller-gold-black-gold' });
|
|
106
|
+
resolveFirst(staleDetails);
|
|
107
|
+
|
|
108
|
+
await waitFor(() => expect(result.current.data).toEqual(productDetails));
|
|
109
|
+
expect(result.current.data?.productId).toBe('small-time-teller-gold-black-gold');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('does not update state after unmount', async () => {
|
|
113
|
+
let resolveFetch: (details: ProductDetails) => void = () => {};
|
|
114
|
+
vi.spyOn(CommerceApiClient, 'retrieveProductDetails').mockImplementation(
|
|
115
|
+
() =>
|
|
116
|
+
new Promise(resolve => {
|
|
117
|
+
resolveFetch = resolve;
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
const consoleErrorSpy = vi.spyOn(console, 'error');
|
|
121
|
+
|
|
122
|
+
const { unmount } = renderHook(() => useProductDetails('small-time-teller-gold-black-gold'));
|
|
123
|
+
unmount();
|
|
124
|
+
resolveFetch(productDetails);
|
|
125
|
+
await Promise.resolve();
|
|
126
|
+
|
|
127
|
+
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ProductDetails,
|
|
3
|
+
ProductVariant,
|
|
4
|
+
} from 'src/application/models/graphql/queries/getProductOptionsQuery';
|
|
5
|
+
import {
|
|
6
|
+
findVariantForSelection,
|
|
7
|
+
getDefaultVariantSelection,
|
|
8
|
+
getSingleVariantId,
|
|
9
|
+
needsVariantSelection,
|
|
10
|
+
} from '../variantSelection';
|
|
11
|
+
|
|
12
|
+
const makeVariant = (overrides: Partial<ProductVariant>): ProductVariant => ({
|
|
13
|
+
variantId: 'variant-1',
|
|
14
|
+
available: true,
|
|
15
|
+
options: [],
|
|
16
|
+
...overrides,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const multiVariantDetails: ProductDetails = {
|
|
20
|
+
productId: 'frypan-set',
|
|
21
|
+
title: 'Ceramic Nonstick Frypan',
|
|
22
|
+
options: [
|
|
23
|
+
{ name: 'Size', values: ['8"', '10"'] },
|
|
24
|
+
{ name: 'Color', values: ['Grey', 'Black'] },
|
|
25
|
+
],
|
|
26
|
+
variants: [
|
|
27
|
+
makeVariant({
|
|
28
|
+
variantId: 'v-8-grey',
|
|
29
|
+
available: false,
|
|
30
|
+
options: [
|
|
31
|
+
{ name: 'Size', value: '8"' },
|
|
32
|
+
{ name: 'Color', value: 'Grey' },
|
|
33
|
+
],
|
|
34
|
+
}),
|
|
35
|
+
makeVariant({
|
|
36
|
+
variantId: 'v-8-black',
|
|
37
|
+
available: true,
|
|
38
|
+
options: [
|
|
39
|
+
{ name: 'Size', value: '8"' },
|
|
40
|
+
{ name: 'Color', value: 'Black' },
|
|
41
|
+
],
|
|
42
|
+
}),
|
|
43
|
+
makeVariant({
|
|
44
|
+
variantId: 'v-10-grey',
|
|
45
|
+
available: true,
|
|
46
|
+
options: [
|
|
47
|
+
{ name: 'Size', value: '10"' },
|
|
48
|
+
{ name: 'Color', value: 'Grey' },
|
|
49
|
+
],
|
|
50
|
+
}),
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
describe('needsVariantSelection', () => {
|
|
55
|
+
it('is true for multiple variants with a real choice', () => {
|
|
56
|
+
expect(needsVariantSelection(multiVariantDetails)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('is false for undefined details', () => {
|
|
60
|
+
expect(needsVariantSelection(undefined)).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('is false for a single variant', () => {
|
|
64
|
+
expect(
|
|
65
|
+
needsVariantSelection({
|
|
66
|
+
...multiVariantDetails,
|
|
67
|
+
variants: [multiVariantDetails.variants[0]],
|
|
68
|
+
}),
|
|
69
|
+
).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('is false when no option offers more than one value', () => {
|
|
73
|
+
expect(
|
|
74
|
+
needsVariantSelection({
|
|
75
|
+
...multiVariantDetails,
|
|
76
|
+
options: [
|
|
77
|
+
{ name: 'Size', values: ['8"'] },
|
|
78
|
+
{ name: 'Color', values: ['Grey'] },
|
|
79
|
+
],
|
|
80
|
+
}),
|
|
81
|
+
).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('is false when there are no options at all', () => {
|
|
85
|
+
expect(needsVariantSelection({ ...multiVariantDetails, options: [] })).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('getSingleVariantId', () => {
|
|
90
|
+
it('returns the variantId when exactly one variant exists', () => {
|
|
91
|
+
expect(
|
|
92
|
+
getSingleVariantId({ ...multiVariantDetails, variants: [multiVariantDetails.variants[1]] }),
|
|
93
|
+
).toBe('v-8-black');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('returns undefined for multiple variants', () => {
|
|
97
|
+
expect(getSingleVariantId(multiVariantDetails)).toBeUndefined();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('returns undefined for undefined details', () => {
|
|
101
|
+
expect(getSingleVariantId(undefined)).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('getDefaultVariantSelection', () => {
|
|
106
|
+
it('uses the first available variant', () => {
|
|
107
|
+
expect(getDefaultVariantSelection(multiVariantDetails)).toEqual({
|
|
108
|
+
Size: '8"',
|
|
109
|
+
Color: 'Black',
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('falls back to the first variant when none are available', () => {
|
|
114
|
+
const details = {
|
|
115
|
+
...multiVariantDetails,
|
|
116
|
+
variants: multiVariantDetails.variants.map(v => ({ ...v, available: false })),
|
|
117
|
+
};
|
|
118
|
+
expect(getDefaultVariantSelection(details)).toEqual({ Size: '8"', Color: 'Grey' });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('falls back to first option values when there are no variants', () => {
|
|
122
|
+
expect(getDefaultVariantSelection({ ...multiVariantDetails, variants: [] })).toEqual({
|
|
123
|
+
Size: '8"',
|
|
124
|
+
Color: 'Grey',
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('findVariantForSelection', () => {
|
|
130
|
+
it('finds the variant matching the selection exactly', () => {
|
|
131
|
+
expect(
|
|
132
|
+
findVariantForSelection(multiVariantDetails, { Size: '10"', Color: 'Grey' })?.variantId,
|
|
133
|
+
).toBe('v-10-grey');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('returns undefined for a combination that does not exist', () => {
|
|
137
|
+
expect(
|
|
138
|
+
findVariantForSelection(multiVariantDetails, { Size: '10"', Color: 'Black' }),
|
|
139
|
+
).toBeUndefined();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('returns undefined for a partial selection', () => {
|
|
143
|
+
expect(findVariantForSelection(multiVariantDetails, { Size: '8"' })).toBeUndefined();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import CommerceApiClient from 'src/application/commerce-api';
|
|
3
|
+
import { ProductDetails } from 'src/application/models/graphql/queries/getProductOptionsQuery';
|
|
4
|
+
|
|
5
|
+
export type {
|
|
6
|
+
ProductDetails,
|
|
7
|
+
ProductOption,
|
|
8
|
+
ProductVariant,
|
|
9
|
+
ProductVariantOption,
|
|
10
|
+
} from 'src/application/models/graphql/queries/getProductOptionsQuery';
|
|
11
|
+
|
|
12
|
+
export interface UseProductDetailsResult {
|
|
13
|
+
data: ProductDetails | undefined;
|
|
14
|
+
isLoading: boolean;
|
|
15
|
+
error: Error | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fetches variant-level product details (options, availability, pricing, images)
|
|
20
|
+
* for the given product ID via CommerceApiClient.retrieveProductDetails (GraphQL
|
|
21
|
+
* `me.productOptions`). Only meaningful for products that offer variations.
|
|
22
|
+
*
|
|
23
|
+
* Returns an idle result ({ data: undefined, isLoading: false, error: undefined })
|
|
24
|
+
* when productId is not provided.
|
|
25
|
+
*/
|
|
26
|
+
export const useProductDetails = (productId: string | undefined): UseProductDetailsResult => {
|
|
27
|
+
const [data, setData] = useState<ProductDetails | undefined>(undefined);
|
|
28
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
29
|
+
const [error, setError] = useState<Error | undefined>(undefined);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!productId) {
|
|
33
|
+
setData(undefined);
|
|
34
|
+
setIsLoading(false);
|
|
35
|
+
setError(undefined);
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
let cancelled = false;
|
|
39
|
+
setData(undefined);
|
|
40
|
+
setError(undefined);
|
|
41
|
+
setIsLoading(true);
|
|
42
|
+
CommerceApiClient.retrieveProductDetails(productId).then(
|
|
43
|
+
result => {
|
|
44
|
+
if (!cancelled) {
|
|
45
|
+
setData(result);
|
|
46
|
+
setIsLoading(false);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
err => {
|
|
50
|
+
if (!cancelled) {
|
|
51
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
52
|
+
setIsLoading(false);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
return () => {
|
|
57
|
+
cancelled = true;
|
|
58
|
+
};
|
|
59
|
+
}, [productId]);
|
|
60
|
+
|
|
61
|
+
return { data, isLoading, error };
|
|
62
|
+
};
|