@envive-ai/react-hooks 0.3.45 → 0.3.47
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/api/widgetText.d.cts +2 -1
- package/dist/application/models/api/widgetText.d.ts +2 -1
- 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/index.cjs +1 -1
- package/dist/application/models/index.js +1 -1
- package/dist/application/models/validators/validateGraphQLFrontendConfig.cjs +1 -1
- package/dist/application/models/validators/validateGraphQLFrontendConfig.js +1 -1
- 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/elementObserver.d.cts +2 -2
- package/dist/application/utils/elementObserver.d.ts +2 -2
- 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/application/utils/widgetTextFromApiWidgetTextResponse.cjs +3 -2
- package/dist/application/utils/widgetTextFromApiWidgetTextResponse.js +3 -2
- 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.cjs +2 -2
- package/dist/atoms/app/variant.d.cts +7 -7
- package/dist/atoms/app/variant.d.ts +7 -7
- package/dist/atoms/app/variant.js +2 -2
- 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 +2 -2
- package/dist/atoms/chat/index.cjs +1 -1
- package/dist/atoms/chat/index.d.cts +2 -2
- package/dist/atoms/chat/index.d.ts +2 -2
- package/dist/atoms/chat/index.js +1 -1
- 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 +6 -6
- 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 +2 -2
- package/dist/atoms/chat/replies.d.cts +3 -3
- package/dist/atoms/chat/replies.d.ts +2 -2
- package/dist/atoms/chat/suggestions.d.cts +2 -2
- package/dist/atoms/chat/suggestions.d.ts +2 -2
- 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/index.cjs +1 -1
- package/dist/atoms/org/index.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.cjs +1 -1
- package/dist/atoms/search/chatSearch.d.cts +17 -17
- package/dist/atoms/search/chatSearch.d.ts +17 -17
- package/dist/atoms/search/chatSearch.js +1 -1
- package/dist/atoms/search/searchAPI.cjs +1 -1
- package/dist/atoms/search/searchAPI.d.cts +13 -13
- package/dist/atoms/search/searchAPI.d.ts +13 -13
- package/dist/atoms/search/searchAPI.js +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/enviveContext/types.d.cts +1 -1
- package/dist/contexts/enviveContext/types.d.ts +1 -1
- 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/pageContext/pageContext.cjs +2 -2
- package/dist/contexts/pageContext/pageContext.js +2 -2
- package/dist/contexts/salesAgentContext/chatAPI.cjs +15 -4
- package/dist/contexts/salesAgentContext/chatAPI.d.cts +4 -1
- package/dist/contexts/salesAgentContext/chatAPI.d.ts +4 -1
- package/dist/contexts/salesAgentContext/chatAPI.js +16 -5
- package/dist/contexts/salesAgentContext/salesAgentContext.cjs +15 -4
- package/dist/contexts/salesAgentContext/salesAgentContext.d.cts +2 -1
- package/dist/contexts/salesAgentContext/salesAgentContext.d.ts +2 -1
- package/dist/contexts/salesAgentContext/salesAgentContext.js +16 -5
- package/dist/contexts/salesAgentContext/salesAgentService.cjs +12 -4
- package/dist/contexts/salesAgentContext/salesAgentService.js +12 -4
- package/dist/contexts/searchContext/searchContext.cjs +1 -1
- package/dist/contexts/searchContext/searchContext.js +1 -1
- package/dist/contexts/systemSettingsContext/systemSettingsContext.d.cts +2 -2
- package/dist/contexts/systemSettingsContext/systemSettingsContext.d.ts +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/contexts/widgetConfigContext/widgetConfigContext.cjs +1 -1
- package/dist/contexts/widgetConfigContext/widgetConfigContext.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/ChatToggle/useChatToggle.cjs +1 -1
- package/dist/hooks/ChatToggle/useChatToggle.js +1 -1
- package/dist/hooks/LocalStorageOperations/useLocalStorageOperations.cjs +6 -5
- package/dist/hooks/LocalStorageOperations/useLocalStorageOperations.js +3 -2
- package/dist/hooks/PageViewedEvent/usePageViewedEvent.cjs +6 -3
- package/dist/hooks/PageViewedEvent/usePageViewedEvent.js +6 -3
- 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.cjs +3 -3
- package/dist/hooks/Search/useSearch.d.cts +1 -1
- package/dist/hooks/Search/useSearch.d.ts +1 -1
- package/dist/hooks/Search/useSearch.js +3 -3
- package/dist/hooks/SystemSettingsContext/useSystemSettingsContext.d.cts +2 -2
- 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 +13 -3
- package/dist/hooks/WidgetInteraction/types.d.cts +29 -4
- package/dist/hooks/WidgetInteraction/types.d.ts +29 -4
- package/dist/hooks/WidgetInteraction/types.js +13 -3
- package/dist/hooks/WidgetInteraction/utils.cjs +2 -2
- package/dist/hooks/WidgetInteraction/utils.js +2 -2
- package/dist/hooks/utils.d.cts +1 -1
- package/dist/hooks/utils.d.ts +1 -1
- package/dist/services/amplitudeService/amplitudeService.cjs +18 -11
- package/dist/services/amplitudeService/amplitudeService.d.cts +5 -2
- package/dist/services/amplitudeService/amplitudeService.d.ts +5 -2
- package/dist/services/amplitudeService/amplitudeService.js +16 -8
- package/dist/services/enviveConfigService/enviveConfigService.cjs +36 -4
- package/dist/services/enviveConfigService/enviveConfigService.d.cts +4 -1
- package/dist/services/enviveConfigService/enviveConfigService.d.ts +4 -1
- package/dist/services/enviveConfigService/enviveConfigService.js +35 -3
- package/dist/services/enviveConfigService/fetchGraphQLConfig.cjs +9 -8
- package/dist/services/enviveConfigService/fetchGraphQLConfig.js +9 -7
- 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.cjs +4 -3
- package/dist/services/hardcopyService/hardcopyService.d.cts +1 -1
- package/dist/services/hardcopyService/hardcopyService.d.ts +1 -1
- package/dist/services/hardcopyService/hardcopyService.js +4 -3
- 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 +18 -2
- 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/api/widgetText.ts +3 -0
- 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/application/utils/widgetTextFromApiWidgetTextResponse.ts +1 -0
- package/src/atoms/app/index.ts +1 -1
- package/src/atoms/app/variant.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/pageContext/pageContext.tsx +1 -1
- package/src/contexts/salesAgentContext/__tests__/chatAPI.test.tsx +63 -0
- package/src/contexts/salesAgentContext/chatAPI.ts +20 -1
- package/src/contexts/salesAgentContext/salesAgentContext.tsx +17 -0
- package/src/contexts/salesAgentContext/salesAgentService.ts +12 -3
- 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/__tests__/usePageViewedEvent.test.ts +4 -4
- package/src/hooks/PageViewedEvent/usePageViewedEvent.ts +5 -1
- 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 +43 -2
- package/src/hooks/WidgetInteraction/utils.ts +1 -1
- package/src/services/amplitudeService/__tests__/amplitudeService.test.ts +8 -8
- package/src/services/amplitudeService/amplitudeService.ts +43 -23
- package/src/services/enviveConfigService/__tests__/enviveConfigService.test.ts +208 -0
- package/src/services/enviveConfigService/enviveConfigService.ts +62 -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/hardcopyService/__tests__/hardcopyService.test.ts +47 -1
- package/src/services/hardcopyService/hardcopyService.ts +5 -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,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
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import CommerceApiClient from 'src/application/commerce-api';
|
|
2
|
+
import {
|
|
3
|
+
ProductDetails,
|
|
4
|
+
ProductVariant,
|
|
5
|
+
} from 'src/application/models/graphql/queries/getProductOptionsQuery';
|
|
6
|
+
|
|
7
|
+
/** A dropdown selection state: option name → chosen value. */
|
|
8
|
+
export type VariantSelection = Record<string, string>;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Imperative variant-details fetch for click-driven flows (the mount-driven
|
|
12
|
+
* useProductDetails hook is a poor fit when the fetch should ride a button's
|
|
13
|
+
* pending state). Resolves undefined when the product has no options data.
|
|
14
|
+
*
|
|
15
|
+
* @throws on network/HTTP failure or GraphQL errors
|
|
16
|
+
*/
|
|
17
|
+
export const fetchProductDetails = (productId: string): Promise<ProductDetails | undefined> =>
|
|
18
|
+
CommerceApiClient.retrieveProductDetails(productId);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A product needs the variant selector when there is more than one variant to
|
|
22
|
+
* choose between and at least one option actually offers a choice.
|
|
23
|
+
*/
|
|
24
|
+
export const needsVariantSelection = (
|
|
25
|
+
details: ProductDetails | undefined,
|
|
26
|
+
): details is ProductDetails =>
|
|
27
|
+
!!details && details.variants.length > 1 && details.options.some(o => o.values.length > 1);
|
|
28
|
+
|
|
29
|
+
/** The variantId to add directly when the product has exactly one variant. */
|
|
30
|
+
export const getSingleVariantId = (details: ProductDetails | undefined): string | undefined =>
|
|
31
|
+
details && details.variants.length === 1 ? details.variants[0].variantId : undefined;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Initial dropdown state: the options of the first available variant, falling
|
|
35
|
+
* back to the first variant, falling back to the first value of each option —
|
|
36
|
+
* so the selector opens in an addable state whenever possible.
|
|
37
|
+
*/
|
|
38
|
+
export const getDefaultVariantSelection = (details: ProductDetails): VariantSelection => {
|
|
39
|
+
const defaultVariant = details.variants.find(v => v.available) ?? details.variants[0];
|
|
40
|
+
if (defaultVariant) {
|
|
41
|
+
return Object.fromEntries(defaultVariant.options.map(({ name, value }) => [name, value]));
|
|
42
|
+
}
|
|
43
|
+
return Object.fromEntries(
|
|
44
|
+
details.options.filter(o => o.values.length > 0).map(o => [o.name, o.values[0]]),
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The variant whose options exactly match the selection (every option name in
|
|
50
|
+
* the variant agrees with the selected value, and counts match), or undefined
|
|
51
|
+
* when the combination does not exist.
|
|
52
|
+
*/
|
|
53
|
+
export const findVariantForSelection = (
|
|
54
|
+
details: ProductDetails,
|
|
55
|
+
selection: VariantSelection,
|
|
56
|
+
): ProductVariant | undefined =>
|
|
57
|
+
details.variants.find(
|
|
58
|
+
variant =>
|
|
59
|
+
variant.options.length === Object.keys(selection).length &&
|
|
60
|
+
variant.options.every(({ name, value }) => selection[name] === value),
|
|
61
|
+
);
|
|
@@ -18,6 +18,14 @@ import {
|
|
|
18
18
|
* @param rootMargin - Root margin for the intersection observer (defaults to 0px).
|
|
19
19
|
* @param enabled - Whether tracking is enabled (defaults to true).
|
|
20
20
|
*/
|
|
21
|
+
const safeNow = (): number | undefined => {
|
|
22
|
+
try {
|
|
23
|
+
return typeof window !== 'undefined' ? window.performance?.now?.() : undefined;
|
|
24
|
+
} catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
21
29
|
export const useTrackComponentVisibleEvent = (
|
|
22
30
|
element: RefObject<HTMLElement>,
|
|
23
31
|
eventProps?: Record<string, unknown>,
|
|
@@ -28,6 +36,12 @@ export const useTrackComponentVisibleEvent = (
|
|
|
28
36
|
const hasTrackedEvent = useRef(false);
|
|
29
37
|
const { trackEvent } = useAmplitude();
|
|
30
38
|
const variantInfo = useAtomValue(pageVariantInfoAtom);
|
|
39
|
+
// Captured on first render, not when the event fires: Widget Rendered is
|
|
40
|
+
// emitted at visibility, which for below-the-fold widgets can be long
|
|
41
|
+
// after the widget actually rendered. performance.now() is relative to
|
|
42
|
+
// navigation start, so this reads as "ms from initial page load to
|
|
43
|
+
// widget render".
|
|
44
|
+
const renderOffsetMs = useRef(safeNow()).current;
|
|
31
45
|
|
|
32
46
|
useEffect(() => {
|
|
33
47
|
console.debug('[useTrackComponentVisibleEvent] effect ran', {
|
|
@@ -63,6 +77,7 @@ export const useTrackComponentVisibleEvent = (
|
|
|
63
77
|
widget: eventProps?.widget_type,
|
|
64
78
|
},
|
|
65
79
|
context: variantInfo ? extractPageContext(variantInfo as PageVariantInfo) : null,
|
|
80
|
+
'performance.render_offset_ms': renderOffsetMs,
|
|
66
81
|
},
|
|
67
82
|
});
|
|
68
83
|
hasTrackedEvent.current = true;
|
|
@@ -30,7 +30,7 @@ export enum WidgetInteractionPageType {
|
|
|
30
30
|
HOMEPAGE = 'homepage',
|
|
31
31
|
SEARCH = 'search',
|
|
32
32
|
OTHER = 'other',
|
|
33
|
-
|
|
33
|
+
ADAPTIVE_STOREFRONT = 'adaptive_storefront',
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export enum WidgetInteractionComponent {
|
|
@@ -61,6 +61,8 @@ export enum WidgetInteractionType {
|
|
|
61
61
|
SUGGESTION_SCROLLED = 'suggestion_scrolled',
|
|
62
62
|
LINK_CLICKED = 'link_clicked',
|
|
63
63
|
PRODUCT_CARD_CLICKED = 'product_card_clicked',
|
|
64
|
+
ADD_TO_CART_CLICKED = 'add_to_cart_clicked',
|
|
65
|
+
ADD_TO_CART_FAILED = 'add_to_cart_failed',
|
|
64
66
|
TEXT_LINK_CLICKED = 'text_link_clicked',
|
|
65
67
|
ARTICLE_LINK_CLICKED = 'article_link_clicked',
|
|
66
68
|
REVIEW_CARD_CLICKED = 'review_card_clicked',
|
|
@@ -69,6 +71,9 @@ export enum WidgetInteractionType {
|
|
|
69
71
|
VOICE_TRANSCRIPTION_STARTED = 'voice_transcription_started',
|
|
70
72
|
VOICE_TRANSCRIPTION_COMPLETED = 'voice_transcription_completed',
|
|
71
73
|
OVERLAY_DISMISSED = 'overlay_dismissed',
|
|
74
|
+
VARIANT_SELECTOR_OPENED = 'variant_selector_opened',
|
|
75
|
+
VARIANT_OPTION_SELECTED = 'variant_option_selected',
|
|
76
|
+
VARIANT_SELECTOR_DISMISSED = 'variant_selector_dismissed',
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
export enum InteractionClass {
|
|
@@ -87,6 +92,8 @@ export const INTERACTION_TYPE_CLASS: Record<WidgetInteractionType, InteractionCl
|
|
|
87
92
|
[WidgetInteractionType.SUGGESTION_SCROLLED]: InteractionClass.PASSIVE,
|
|
88
93
|
[WidgetInteractionType.LINK_CLICKED]: InteractionClass.INTENTIONAL,
|
|
89
94
|
[WidgetInteractionType.PRODUCT_CARD_CLICKED]: InteractionClass.INTENTIONAL,
|
|
95
|
+
[WidgetInteractionType.ADD_TO_CART_CLICKED]: InteractionClass.INTENTIONAL,
|
|
96
|
+
[WidgetInteractionType.ADD_TO_CART_FAILED]: InteractionClass.PASSIVE,
|
|
90
97
|
[WidgetInteractionType.TEXT_LINK_CLICKED]: InteractionClass.INTENTIONAL,
|
|
91
98
|
[WidgetInteractionType.ARTICLE_LINK_CLICKED]: InteractionClass.INTENTIONAL,
|
|
92
99
|
[WidgetInteractionType.REVIEW_CARD_CLICKED]: InteractionClass.INTENTIONAL,
|
|
@@ -95,6 +102,9 @@ export const INTERACTION_TYPE_CLASS: Record<WidgetInteractionType, InteractionCl
|
|
|
95
102
|
[WidgetInteractionType.VOICE_TRANSCRIPTION_STARTED]: InteractionClass.INTENTIONAL,
|
|
96
103
|
[WidgetInteractionType.VOICE_TRANSCRIPTION_COMPLETED]: InteractionClass.INTENTIONAL,
|
|
97
104
|
[WidgetInteractionType.OVERLAY_DISMISSED]: InteractionClass.NAVIGATIONAL,
|
|
105
|
+
[WidgetInteractionType.VARIANT_SELECTOR_OPENED]: InteractionClass.NAVIGATIONAL,
|
|
106
|
+
[WidgetInteractionType.VARIANT_OPTION_SELECTED]: InteractionClass.INTENTIONAL,
|
|
107
|
+
[WidgetInteractionType.VARIANT_SELECTOR_DISMISSED]: InteractionClass.NAVIGATIONAL,
|
|
98
108
|
};
|
|
99
109
|
|
|
100
110
|
export type URL = {
|
|
@@ -153,6 +163,14 @@ export type ProductCardClicked = {
|
|
|
153
163
|
product_card_clicked: Product;
|
|
154
164
|
};
|
|
155
165
|
|
|
166
|
+
export type AddToCartClicked = {
|
|
167
|
+
add_to_cart_clicked: Product;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export type AddToCartFailed = {
|
|
171
|
+
add_to_cart_failed: Product;
|
|
172
|
+
};
|
|
173
|
+
|
|
156
174
|
export type ReviewCardClicked = {
|
|
157
175
|
review_card_clicked: Review;
|
|
158
176
|
};
|
|
@@ -165,6 +183,24 @@ export type VoiceTranscription = {
|
|
|
165
183
|
transcription: string;
|
|
166
184
|
};
|
|
167
185
|
|
|
186
|
+
export type SelectedVariantOption = {
|
|
187
|
+
product_id: string;
|
|
188
|
+
option_name: string;
|
|
189
|
+
option_value: string;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export type VariantSelectorOpened = {
|
|
193
|
+
variant_selector_opened: Product;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export type VariantOptionSelected = {
|
|
197
|
+
variant_option_selected: SelectedVariantOption;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export type VariantSelectorDismissed = {
|
|
201
|
+
variant_selector_dismissed: Product;
|
|
202
|
+
};
|
|
203
|
+
|
|
168
204
|
export type WidgetInteractionData =
|
|
169
205
|
| SuggestionScrolled
|
|
170
206
|
| SuggestionClicked
|
|
@@ -174,6 +210,11 @@ export type WidgetInteractionData =
|
|
|
174
210
|
| TextLinkClicked
|
|
175
211
|
| ArticleLinkClicked
|
|
176
212
|
| ProductCardClicked
|
|
213
|
+
| AddToCartClicked
|
|
214
|
+
| AddToCartFailed
|
|
177
215
|
| ReviewCardClicked
|
|
178
216
|
| MessageSubmitted
|
|
179
|
-
| VoiceTranscription
|
|
217
|
+
| VoiceTranscription
|
|
218
|
+
| VariantSelectorOpened
|
|
219
|
+
| VariantOptionSelected
|
|
220
|
+
| VariantSelectorDismissed;
|
|
@@ -36,7 +36,7 @@ export const extractPageContext = (
|
|
|
36
36
|
case VariantTypeEnum.FullPageSalesAgent:
|
|
37
37
|
return {
|
|
38
38
|
page_id: (pageVariant as FullPageSalesAgentVariantInfo).url,
|
|
39
|
-
page_type: WidgetInteractionPageType.
|
|
39
|
+
page_type: WidgetInteractionPageType.ADAPTIVE_STOREFRONT,
|
|
40
40
|
};
|
|
41
41
|
default:
|
|
42
42
|
return null;
|
|
@@ -105,8 +105,8 @@ describe('AmplitudeService', () => {
|
|
|
105
105
|
};
|
|
106
106
|
|
|
107
107
|
describe('Initialization', () => {
|
|
108
|
-
it('should initialize Amplitude client when all required config is provided', () => {
|
|
109
|
-
createService();
|
|
108
|
+
it('should initialize Amplitude client when all required config is provided', async () => {
|
|
109
|
+
await createService().initialized();
|
|
110
110
|
|
|
111
111
|
expect(mockInit).toHaveBeenCalledWith(
|
|
112
112
|
validApiKey,
|
|
@@ -516,9 +516,9 @@ describe('AmplitudeService', () => {
|
|
|
516
516
|
configurable: true,
|
|
517
517
|
});
|
|
518
518
|
|
|
519
|
-
createService({
|
|
519
|
+
await createService({
|
|
520
520
|
show: true,
|
|
521
|
-
});
|
|
521
|
+
}).initialized();
|
|
522
522
|
|
|
523
523
|
// Simulate a Page Viewed event by calling the enrichment plugin
|
|
524
524
|
// The enrichment is added during initialization
|
|
@@ -564,7 +564,7 @@ describe('AmplitudeService', () => {
|
|
|
564
564
|
configurable: true,
|
|
565
565
|
});
|
|
566
566
|
|
|
567
|
-
createService();
|
|
567
|
+
await createService().initialized();
|
|
568
568
|
|
|
569
569
|
const enrichmentCall = mockAdd.mock.calls[0];
|
|
570
570
|
const enrichment = enrichmentCall[0];
|
|
@@ -580,7 +580,7 @@ describe('AmplitudeService', () => {
|
|
|
580
580
|
});
|
|
581
581
|
|
|
582
582
|
it('should not enrich non-specified events', async () => {
|
|
583
|
-
createService();
|
|
583
|
+
await createService().initialized();
|
|
584
584
|
|
|
585
585
|
const enrichmentCall = mockAdd.mock.calls[0];
|
|
586
586
|
const enrichment = enrichmentCall[0];
|
|
@@ -659,8 +659,8 @@ describe('AmplitudeService', () => {
|
|
|
659
659
|
});
|
|
660
660
|
|
|
661
661
|
describe('Session replay initialization', () => {
|
|
662
|
-
it('should disable session replay by default', () => {
|
|
663
|
-
createService();
|
|
662
|
+
it('should disable session replay by default', async () => {
|
|
663
|
+
await createService().initialized();
|
|
664
664
|
|
|
665
665
|
// Session replay is always disabled in the current implementation
|
|
666
666
|
expect(mockInit).toHaveBeenCalledWith(
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createInstance } from '@amplitude/analytics-browser';
|
|
2
1
|
import { EventsDispatcher, SpiffyEvent } from 'src/events';
|
|
3
2
|
import { LocalStorageKeys } from 'src/contexts/localStorageContext';
|
|
4
3
|
import Logger from 'src/application/logging/logger';
|
|
@@ -8,7 +7,7 @@ import type {
|
|
|
8
7
|
Event,
|
|
9
8
|
ServerZoneType,
|
|
10
9
|
} from '@amplitude/analytics-types';
|
|
11
|
-
import { FeatureFlagService } from 'src/
|
|
10
|
+
import { FeatureFlagService } from 'src/services/featureFlagService';
|
|
12
11
|
import { WidgetTypeV3 } from 'src/contexts/typesV3';
|
|
13
12
|
import { ChatElementDisplayLocationV3 } from 'src/application/models/chatElementDisplayLocationV3';
|
|
14
13
|
import { ExperimentNamespace, OrgConfigExperimentAssignment } from 'src/application/models';
|
|
@@ -80,24 +79,36 @@ export class AmplitudeService {
|
|
|
80
79
|
|
|
81
80
|
private config: AmplitudeServiceConfig;
|
|
82
81
|
|
|
82
|
+
private initPromise: Promise<void>;
|
|
83
|
+
|
|
83
84
|
constructor(config: AmplitudeServiceConfig) {
|
|
84
85
|
this.config = config;
|
|
85
|
-
this.initialize();
|
|
86
|
+
this.initPromise = this.initialize();
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
get isMockApiKey(): boolean {
|
|
89
90
|
return this.config.amplitudeApiKey === 'mock-amplitude-key';
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
// Must not depend on `amplitudeClient`: the SDK now loads via dynamic
|
|
94
|
+
// import, and AmplitudeProvider unmounts its entire subtree while
|
|
95
|
+
// isReady is false — gating on the client would block widget rendering
|
|
96
|
+
// on the analytics chunk download. Events awaited in trackEvent queue
|
|
97
|
+
// behind `initPromise` instead.
|
|
92
98
|
get isReady(): boolean {
|
|
93
99
|
return Boolean(
|
|
94
100
|
this.config.userId &&
|
|
95
101
|
this.config.featureFlagService &&
|
|
96
102
|
this.config.amplitudeApiKey &&
|
|
97
|
-
this.
|
|
103
|
+
!this.isMockApiKey,
|
|
98
104
|
);
|
|
99
105
|
}
|
|
100
106
|
|
|
107
|
+
/** Resolves once SDK initialization has settled (never rejects). */
|
|
108
|
+
initialized(): Promise<void> {
|
|
109
|
+
return this.initPromise;
|
|
110
|
+
}
|
|
111
|
+
|
|
101
112
|
private getLocalStorageItem(key: string): string | null {
|
|
102
113
|
if (this.config.getLocalStorageItem) {
|
|
103
114
|
return this.config.getLocalStorageItem(key);
|
|
@@ -246,7 +257,7 @@ export class AmplitudeService {
|
|
|
246
257
|
return enrichment;
|
|
247
258
|
}
|
|
248
259
|
|
|
249
|
-
private initialize(): void {
|
|
260
|
+
private async initialize(): Promise<void> {
|
|
250
261
|
const isReady = Boolean(
|
|
251
262
|
this.config.userId && this.config.featureFlagService && this.config.amplitudeApiKey,
|
|
252
263
|
);
|
|
@@ -270,25 +281,32 @@ export class AmplitudeService {
|
|
|
270
281
|
}
|
|
271
282
|
|
|
272
283
|
if (!this.amplitudeClient) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
trackHistoryChanges: 'pathOnly',
|
|
284
|
+
try {
|
|
285
|
+
// Dynamic import keeps the Amplitude SDK out of the eager
|
|
286
|
+
// injection-bundle chunk; trackEvent queues behind initPromise.
|
|
287
|
+
const { createInstance } = await import('@amplitude/analytics-browser');
|
|
288
|
+
const currentAmplitudeInstance: BrowserClient = createInstance();
|
|
289
|
+
|
|
290
|
+
currentAmplitudeInstance.add(this.getEventTrackingEnrichment());
|
|
291
|
+
currentAmplitudeInstance.init(this.config.amplitudeApiKey, this.config.userId, {
|
|
292
|
+
serverZone: this.config.dataResidency as ServerZoneType,
|
|
293
|
+
trackingOptions: {
|
|
294
|
+
ipAddress: true,
|
|
285
295
|
},
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
296
|
+
autocapture: {
|
|
297
|
+
attribution: true,
|
|
298
|
+
pageViews: {
|
|
299
|
+
trackHistoryChanges: 'pathOnly',
|
|
300
|
+
},
|
|
301
|
+
sessions: false,
|
|
302
|
+
formInteractions: false,
|
|
303
|
+
fileDownloads: false,
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
this.amplitudeClient = currentAmplitudeInstance;
|
|
307
|
+
} catch (err) {
|
|
308
|
+
logger.logError('Failed to load or initialize the Amplitude SDK', err);
|
|
309
|
+
}
|
|
292
310
|
}
|
|
293
311
|
}
|
|
294
312
|
|
|
@@ -296,6 +314,7 @@ export class AmplitudeService {
|
|
|
296
314
|
switch (widgetType) {
|
|
297
315
|
case WidgetTypeV3.FloatingButtonV3:
|
|
298
316
|
return 'floating_button';
|
|
317
|
+
case WidgetTypeV3.TypingAnimationV3:
|
|
299
318
|
case WidgetTypeV3.ChatPreviewV3:
|
|
300
319
|
case WidgetTypeV3.SocialProofV3:
|
|
301
320
|
return 'embedded_widget';
|
|
@@ -395,6 +414,7 @@ export class AmplitudeService {
|
|
|
395
414
|
alsoSendToGoogleAnalytics = false,
|
|
396
415
|
}: TrackEventParams): Promise<void> {
|
|
397
416
|
if (this.isMockApiKey) return;
|
|
417
|
+
await this.initPromise;
|
|
398
418
|
logger.logDebug('Submitting event', eventName);
|
|
399
419
|
try {
|
|
400
420
|
const decoratedEventName = AmplitudeService.decorateEventName(eventName);
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { EnviveConfigService } from '../enviveConfigService';
|
|
2
|
+
|
|
3
|
+
const mockFetch = vi.fn();
|
|
4
|
+
globalThis.fetch = mockFetch;
|
|
5
|
+
|
|
6
|
+
const ORG_CONFIG_RESPONSE = {
|
|
7
|
+
org: {
|
|
8
|
+
org: {
|
|
9
|
+
id: 'org-1',
|
|
10
|
+
short_name: 'test-org',
|
|
11
|
+
display_name: 'Test Org',
|
|
12
|
+
domain: 'test.com',
|
|
13
|
+
status: 'active',
|
|
14
|
+
created_at: '2024-01-01T00:00:00.000Z',
|
|
15
|
+
updated_at: '2024-01-01T00:00:00.000Z',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
gates: [],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function makeOkOrgConfigFetch(overrides: Record<string, unknown> = {}) {
|
|
22
|
+
mockFetch.mockResolvedValueOnce({
|
|
23
|
+
ok: true,
|
|
24
|
+
json: () => Promise.resolve({ ...ORG_CONFIG_RESPONSE, ...overrides }),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// productsConfig has no colors, so fetchGraphQLConfig falls back to its mock
|
|
29
|
+
// config (see fetchGraphQLConfig.test.ts "no colors fallback") - fine here
|
|
30
|
+
// since these tests only care about caching/fetch-count behaviour.
|
|
31
|
+
function makeOkGraphqlFetch() {
|
|
32
|
+
mockFetch.mockResolvedValueOnce({
|
|
33
|
+
ok: true,
|
|
34
|
+
json: () => Promise.resolve({ data: { me: { getProductsConfigByVersion: {} } } }),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeService(
|
|
39
|
+
overrides: Partial<ConstructorParameters<typeof EnviveConfigService>[0]> = {},
|
|
40
|
+
) {
|
|
41
|
+
return new EnviveConfigService({
|
|
42
|
+
baseUrl: 'https://api.example.com',
|
|
43
|
+
apiKey: 'key',
|
|
44
|
+
userId: 'user-1',
|
|
45
|
+
namespace: 'spiffy-react-components',
|
|
46
|
+
source: 'app',
|
|
47
|
+
...overrides,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
vi.clearAllMocks();
|
|
53
|
+
sessionStorage.clear();
|
|
54
|
+
window.history.replaceState({}, '', '/');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
vi.useRealTimers();
|
|
59
|
+
vi.restoreAllMocks();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('EnviveConfigService caching', () => {
|
|
63
|
+
it('fetches org config + GraphQL config on a cold cache', async () => {
|
|
64
|
+
makeOkOrgConfigFetch();
|
|
65
|
+
makeOkGraphqlFetch();
|
|
66
|
+
|
|
67
|
+
const result = await makeService().getEnviveConfig();
|
|
68
|
+
|
|
69
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
70
|
+
expect(result.org.org.id).toBe('org-1');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('serves a warm cache to a new instance with identical params without hitting the network', async () => {
|
|
74
|
+
makeOkOrgConfigFetch();
|
|
75
|
+
makeOkGraphqlFetch();
|
|
76
|
+
await makeService().getEnviveConfig();
|
|
77
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
78
|
+
|
|
79
|
+
// A fresh instance (as happens on the next page navigation) should read
|
|
80
|
+
// from the sessionStorage cache instead of refetching.
|
|
81
|
+
const result = await makeService().getEnviveConfig();
|
|
82
|
+
|
|
83
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
84
|
+
expect(result.org.org.id).toBe('org-1');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('refetches once the cache entry has expired past the 5-minute TTL', async () => {
|
|
88
|
+
vi.useFakeTimers();
|
|
89
|
+
vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'));
|
|
90
|
+
|
|
91
|
+
makeOkOrgConfigFetch();
|
|
92
|
+
makeOkGraphqlFetch();
|
|
93
|
+
await makeService().getEnviveConfig();
|
|
94
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
95
|
+
|
|
96
|
+
vi.setSystemTime(new Date('2026-01-01T00:05:01.000Z'));
|
|
97
|
+
|
|
98
|
+
makeOkOrgConfigFetch();
|
|
99
|
+
makeOkGraphqlFetch();
|
|
100
|
+
await makeService().getEnviveConfig();
|
|
101
|
+
|
|
102
|
+
expect(mockFetch).toHaveBeenCalledTimes(4);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it.each(['envive_config_version', 'spiffy_config_version'])(
|
|
106
|
+
'always bypasses the cache (read and write) when %s is present',
|
|
107
|
+
async param => {
|
|
108
|
+
window.history.replaceState({}, '', `/?${param}=1.2.3`);
|
|
109
|
+
|
|
110
|
+
makeOkOrgConfigFetch();
|
|
111
|
+
makeOkGraphqlFetch();
|
|
112
|
+
await makeService().getEnviveConfig();
|
|
113
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
114
|
+
|
|
115
|
+
makeOkOrgConfigFetch();
|
|
116
|
+
makeOkGraphqlFetch();
|
|
117
|
+
await makeService().getEnviveConfig();
|
|
118
|
+
|
|
119
|
+
// No cache read/write happened, so a second instance refetches instead
|
|
120
|
+
// of reusing anything the first instance stored.
|
|
121
|
+
expect(mockFetch).toHaveBeenCalledTimes(4);
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
it('does not read a version-pinned cache entry after the override is removed', async () => {
|
|
126
|
+
window.history.replaceState({}, '', '/?envive_config_version=1.2.3');
|
|
127
|
+
makeOkOrgConfigFetch();
|
|
128
|
+
makeOkGraphqlFetch();
|
|
129
|
+
await makeService().getEnviveConfig();
|
|
130
|
+
|
|
131
|
+
window.history.replaceState({}, '', '/');
|
|
132
|
+
makeOkOrgConfigFetch();
|
|
133
|
+
makeOkGraphqlFetch();
|
|
134
|
+
await makeService().getEnviveConfig();
|
|
135
|
+
|
|
136
|
+
expect(mockFetch).toHaveBeenCalledTimes(4);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('keeps separate cache entries per userId', async () => {
|
|
140
|
+
makeOkOrgConfigFetch();
|
|
141
|
+
makeOkGraphqlFetch();
|
|
142
|
+
await makeService({ userId: 'user-1' }).getEnviveConfig();
|
|
143
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
144
|
+
|
|
145
|
+
makeOkOrgConfigFetch();
|
|
146
|
+
makeOkGraphqlFetch();
|
|
147
|
+
await makeService({ userId: 'user-2' }).getEnviveConfig();
|
|
148
|
+
|
|
149
|
+
expect(mockFetch).toHaveBeenCalledTimes(4);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('keeps separate cache entries per namespace/source/baseUrl/apiKey', async () => {
|
|
153
|
+
makeOkOrgConfigFetch();
|
|
154
|
+
makeOkGraphqlFetch();
|
|
155
|
+
await makeService({ namespace: 'ns-a' }).getEnviveConfig();
|
|
156
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
157
|
+
|
|
158
|
+
makeOkOrgConfigFetch();
|
|
159
|
+
makeOkGraphqlFetch();
|
|
160
|
+
await makeService({ namespace: 'ns-b' }).getEnviveConfig();
|
|
161
|
+
|
|
162
|
+
expect(mockFetch).toHaveBeenCalledTimes(4);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('fails open and still returns config when sessionStorage.setItem throws', async () => {
|
|
166
|
+
vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
|
|
167
|
+
throw new Error('quota exceeded');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
makeOkOrgConfigFetch();
|
|
171
|
+
makeOkGraphqlFetch();
|
|
172
|
+
|
|
173
|
+
const result = await makeService().getEnviveConfig();
|
|
174
|
+
|
|
175
|
+
expect(result.org.org.id).toBe('org-1');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('fails open and refetches when sessionStorage.getItem throws', async () => {
|
|
179
|
+
vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
|
|
180
|
+
throw new Error('storage disabled');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
makeOkOrgConfigFetch();
|
|
184
|
+
makeOkGraphqlFetch();
|
|
185
|
+
|
|
186
|
+
const result = await makeService().getEnviveConfig();
|
|
187
|
+
|
|
188
|
+
expect(result.org.org.id).toBe('org-1');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('fails open and refetches when a cached entry is corrupted JSON', async () => {
|
|
192
|
+
const service = makeService();
|
|
193
|
+
// Seed a corrupted entry under whatever key the service reads from by
|
|
194
|
+
// populating every session storage key with garbage after a real write,
|
|
195
|
+
// then corrupting it directly.
|
|
196
|
+
makeOkOrgConfigFetch();
|
|
197
|
+
makeOkGraphqlFetch();
|
|
198
|
+
await service.getEnviveConfig();
|
|
199
|
+
const [key] = Object.keys(sessionStorage);
|
|
200
|
+
sessionStorage.setItem(key, 'not json');
|
|
201
|
+
|
|
202
|
+
makeOkOrgConfigFetch();
|
|
203
|
+
makeOkGraphqlFetch();
|
|
204
|
+
const result = await makeService().getEnviveConfig();
|
|
205
|
+
|
|
206
|
+
expect(result.org.org.id).toBe('org-1');
|
|
207
|
+
});
|
|
208
|
+
});
|