@shopify/shop-minis-react 0.3.2 → 0.4.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 (205) hide show
  1. package/dist/components/MinisContainer.js +11 -10
  2. package/dist/components/MinisContainer.js.map +1 -1
  3. package/dist/components/atoms/content-wrapper.js.map +1 -1
  4. package/dist/components/atoms/video-player.js +28 -26
  5. package/dist/components/atoms/video-player.js.map +1 -1
  6. package/dist/components/commerce/product-card.js +106 -79
  7. package/dist/components/commerce/product-card.js.map +1 -1
  8. package/dist/components/commerce/product-link.js +124 -137
  9. package/dist/components/commerce/product-link.js.map +1 -1
  10. package/dist/components/commerce/search.js +20 -20
  11. package/dist/components/commerce/search.js.map +1 -1
  12. package/dist/components/ui/sonner.js +3 -1
  13. package/dist/components/ui/sonner.js.map +1 -1
  14. package/dist/hooks/content/useContent.js +12 -18
  15. package/dist/hooks/content/useContent.js.map +1 -1
  16. package/dist/hooks/navigation/useNavigateWithTransition.js +10 -11
  17. package/dist/hooks/navigation/useNavigateWithTransition.js.map +1 -1
  18. package/dist/hooks/product/useCuratedProducts.js +9 -11
  19. package/dist/hooks/product/useCuratedProducts.js.map +1 -1
  20. package/dist/hooks/product/usePopularProducts.js +9 -11
  21. package/dist/hooks/product/usePopularProducts.js.map +1 -1
  22. package/dist/hooks/product/useProduct.js +11 -17
  23. package/dist/hooks/product/useProduct.js.map +1 -1
  24. package/dist/hooks/product/useProductList.js +10 -21
  25. package/dist/hooks/product/useProductList.js.map +1 -1
  26. package/dist/hooks/product/useProductLists.js +11 -13
  27. package/dist/hooks/product/useProductLists.js.map +1 -1
  28. package/dist/hooks/product/useProductMedia.js +12 -18
  29. package/dist/hooks/product/useProductMedia.js.map +1 -1
  30. package/dist/hooks/product/useProductSearch.js +34 -27
  31. package/dist/hooks/product/useProductSearch.js.map +1 -1
  32. package/dist/hooks/product/useProductVariants.js +11 -14
  33. package/dist/hooks/product/useProductVariants.js.map +1 -1
  34. package/dist/hooks/product/useProducts.js +12 -11
  35. package/dist/hooks/product/useProducts.js.map +1 -1
  36. package/dist/hooks/product/useRecommendedProducts.js +11 -13
  37. package/dist/hooks/product/useRecommendedProducts.js.map +1 -1
  38. package/dist/hooks/shop/useRecommendedShops.js +11 -13
  39. package/dist/hooks/shop/useRecommendedShops.js.map +1 -1
  40. package/dist/hooks/shop/useShop.js +12 -11
  41. package/dist/hooks/shop/useShop.js.map +1 -1
  42. package/dist/hooks/user/useBuyerAttributes.js +8 -10
  43. package/dist/hooks/user/useBuyerAttributes.js.map +1 -1
  44. package/dist/hooks/user/useCurrentUser.js +7 -9
  45. package/dist/hooks/user/useCurrentUser.js.map +1 -1
  46. package/dist/hooks/user/useFollowedShops.js +11 -14
  47. package/dist/hooks/user/useFollowedShops.js.map +1 -1
  48. package/dist/hooks/user/useOrders.js +7 -9
  49. package/dist/hooks/user/useOrders.js.map +1 -1
  50. package/dist/hooks/user/useRecentProducts.js +11 -13
  51. package/dist/hooks/user/useRecentProducts.js.map +1 -1
  52. package/dist/hooks/user/useRecentShops.js +10 -13
  53. package/dist/hooks/user/useRecentShops.js.map +1 -1
  54. package/dist/hooks/user/useSavedProducts.js +10 -13
  55. package/dist/hooks/user/useSavedProducts.js.map +1 -1
  56. package/dist/index.js +269 -264
  57. package/dist/index.js.map +1 -1
  58. package/dist/internal/components/product-review-stars.js +78 -0
  59. package/dist/internal/components/product-review-stars.js.map +1 -0
  60. package/dist/internal/reactQuery/MinisQueryProvider.js +11 -0
  61. package/dist/internal/reactQuery/MinisQueryProvider.js.map +1 -0
  62. package/dist/internal/reactQuery/queryClient.js +33 -0
  63. package/dist/internal/reactQuery/queryClient.js.map +1 -0
  64. package/dist/internal/reactQuery/useShopActionInfiniteQuery.js +52 -0
  65. package/dist/internal/reactQuery/useShopActionInfiniteQuery.js.map +1 -0
  66. package/dist/internal/reactQuery/useShopActionQuery.js +37 -0
  67. package/dist/internal/reactQuery/useShopActionQuery.js.map +1 -0
  68. package/dist/mocks.js +178 -107
  69. package/dist/mocks.js.map +1 -1
  70. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/focusManager.js +45 -0
  71. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/focusManager.js.map +1 -0
  72. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/infiniteQueryBehavior.js +89 -0
  73. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/infiniteQueryBehavior.js.map +1 -0
  74. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/infiniteQueryObserver.js +55 -0
  75. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/infiniteQueryObserver.js.map +1 -0
  76. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/mutation.js +198 -0
  77. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/mutation.js.map +1 -0
  78. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/mutationCache.js +99 -0
  79. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/mutationCache.js.map +1 -0
  80. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/notifyManager.js +67 -0
  81. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/notifyManager.js.map +1 -0
  82. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/onlineManager.js +39 -0
  83. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/onlineManager.js.map +1 -0
  84. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/query.js +299 -0
  85. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/query.js.map +1 -0
  86. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/queryCache.js +80 -0
  87. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/queryCache.js.map +1 -0
  88. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/queryClient.js +215 -0
  89. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/queryClient.js.map +1 -0
  90. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/queryObserver.js +300 -0
  91. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/queryObserver.js.map +1 -0
  92. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/removable.js +25 -0
  93. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/removable.js.map +1 -0
  94. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/retryer.js +76 -0
  95. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/retryer.js.map +1 -0
  96. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/subscribable.js +21 -0
  97. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/subscribable.js.map +1 -0
  98. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/thenable.js +26 -0
  99. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/thenable.js.map +1 -0
  100. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/utils.js +176 -0
  101. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/utils.js.map +1 -0
  102. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/IsRestoringProvider.js +7 -0
  103. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/IsRestoringProvider.js.map +1 -0
  104. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/QueryClientProvider.js +17 -0
  105. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/QueryClientProvider.js.map +1 -0
  106. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/QueryErrorResetBoundary.js +19 -0
  107. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/QueryErrorResetBoundary.js.map +1 -0
  108. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/errorBoundaryUtils.js +21 -0
  109. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/errorBoundaryUtils.js.map +1 -0
  110. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/suspense.js +18 -0
  111. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/suspense.js.map +1 -0
  112. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/useBaseQuery.js +64 -0
  113. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/useBaseQuery.js.map +1 -0
  114. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/useInfiniteQuery.js +13 -0
  115. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/useInfiniteQuery.js.map +1 -0
  116. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/useQuery.js +9 -0
  117. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/useQuery.js.map +1 -0
  118. package/dist/shop-minis-react/node_modules/.pnpm/lucide-react@0.513.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/star-half.js +21 -0
  119. package/dist/shop-minis-react/node_modules/.pnpm/lucide-react@0.513.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/star-half.js.map +1 -0
  120. package/dist/shop-minis-react/node_modules/.pnpm/sonner@2.0.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/sonner/dist/index.js +4 -4
  121. package/dist/shop-minis-react/node_modules/.pnpm/sonner@2.0.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/sonner/dist/index.js.map +1 -1
  122. package/package.json +2 -7
  123. package/src/components/MinisContainer.tsx +6 -3
  124. package/src/components/atoms/content-wrapper.tsx +1 -1
  125. package/src/components/atoms/video-player.tsx +7 -0
  126. package/src/components/commerce/product-card.test.tsx +135 -0
  127. package/src/components/commerce/product-card.tsx +39 -5
  128. package/src/components/commerce/product-link.test.tsx +15 -3
  129. package/src/components/commerce/product-link.tsx +9 -25
  130. package/src/components/commerce/search.tsx +2 -2
  131. package/src/components/index.ts +1 -0
  132. package/src/components/ui/sonner.tsx +2 -2
  133. package/src/hooks/content/useContent.ts +6 -17
  134. package/src/hooks/navigation/useNavigateWithTransition.test.ts +46 -7
  135. package/src/hooks/navigation/useNavigateWithTransition.ts +4 -1
  136. package/src/hooks/product/useCuratedProducts.ts +4 -6
  137. package/src/hooks/product/usePopularProducts.ts +4 -6
  138. package/src/hooks/product/useProduct.ts +6 -17
  139. package/src/hooks/product/useProductList.ts +4 -19
  140. package/src/hooks/product/useProductLists.ts +4 -6
  141. package/src/hooks/product/useProductMedia.ts +6 -17
  142. package/src/hooks/product/useProductSearch.ts +19 -15
  143. package/src/hooks/product/useProductVariants.ts +5 -13
  144. package/src/hooks/product/useProducts.ts +8 -12
  145. package/src/hooks/product/useRecommendedProducts.ts +4 -6
  146. package/src/hooks/shop/useRecommendedShops.ts +4 -6
  147. package/src/hooks/shop/useShop.ts +8 -12
  148. package/src/hooks/user/useBuyerAttributes.ts +4 -6
  149. package/src/hooks/user/useCurrentUser.ts +4 -6
  150. package/src/hooks/user/useFollowedShops.ts +5 -13
  151. package/src/hooks/user/useOrders.ts +4 -6
  152. package/src/hooks/user/useRecentProducts.ts +4 -6
  153. package/src/hooks/user/useRecentShops.ts +5 -13
  154. package/src/hooks/user/useSavedProducts.ts +5 -13
  155. package/src/internal/components/product-review-stars.test.tsx +90 -0
  156. package/src/internal/components/product-review-stars.tsx +113 -0
  157. package/src/internal/reactQuery/MinisQueryProvider.test.tsx +38 -0
  158. package/src/internal/reactQuery/MinisQueryProvider.tsx +16 -0
  159. package/src/internal/reactQuery/index.ts +8 -0
  160. package/src/internal/reactQuery/queryClient.test.tsx +91 -0
  161. package/src/internal/reactQuery/queryClient.ts +43 -0
  162. package/src/internal/reactQuery/useShopActionInfiniteQuery.test.tsx +357 -0
  163. package/src/internal/reactQuery/useShopActionInfiniteQuery.ts +129 -0
  164. package/src/internal/reactQuery/useShopActionQuery.test.tsx +184 -0
  165. package/src/internal/reactQuery/useShopActionQuery.ts +74 -0
  166. package/src/mocks.ts +10 -2
  167. package/src/providers/ImagePickerProvider.test.tsx +3 -9
  168. package/dist/internal/useShopActionsDataFetching.js +0 -79
  169. package/dist/internal/useShopActionsDataFetching.js.map +0 -1
  170. package/dist/internal/useShopActionsPaginatedDataFetching.js +0 -96
  171. package/dist/internal/useShopActionsPaginatedDataFetching.js.map +0 -1
  172. package/src/hooks/product/useProductSearch.test.ts +0 -470
  173. package/src/internal/useShopActionsDataFetching.test.ts +0 -465
  174. package/src/internal/useShopActionsDataFetching.ts +0 -150
  175. package/src/internal/useShopActionsPaginatedDataFetching.ts +0 -188
  176. package/src/stories/Accordion.stories.tsx +0 -124
  177. package/src/stories/AddToCart.stories.tsx +0 -251
  178. package/src/stories/Alert.stories.tsx +0 -38
  179. package/src/stories/AlertDialog.stories.tsx +0 -48
  180. package/src/stories/Avatar.stories.tsx +0 -29
  181. package/src/stories/Badge.stories.tsx +0 -46
  182. package/src/stories/Button.stories.tsx +0 -81
  183. package/src/stories/Card.stories.tsx +0 -40
  184. package/src/stories/Checkbox.stories.tsx +0 -44
  185. package/src/stories/FavoriteButton.stories.tsx +0 -58
  186. package/src/stories/IconButton.stories.tsx +0 -68
  187. package/src/stories/ImageContentWrapper.stories.tsx +0 -65
  188. package/src/stories/Input.stories.tsx +0 -44
  189. package/src/stories/Label.stories.tsx +0 -19
  190. package/src/stories/List.stories.tsx +0 -64
  191. package/src/stories/MerchantCard.stories.tsx +0 -127
  192. package/src/stories/ProductCard.stories.tsx +0 -92
  193. package/src/stories/ProductLink.stories.tsx +0 -46
  194. package/src/stories/ProductVariantPrice.stories.tsx +0 -70
  195. package/src/stories/Progress.stories.tsx +0 -30
  196. package/src/stories/PullToRefreshList.stories.tsx +0 -122
  197. package/src/stories/QuantitySelector.stories.tsx +0 -78
  198. package/src/stories/RadioGroup.stories.tsx +0 -51
  199. package/src/stories/Search.stories.tsx +0 -37
  200. package/src/stories/Select.stories.tsx +0 -85
  201. package/src/stories/Skeleton.stories.tsx +0 -19
  202. package/src/stories/TextInput.stories.tsx +0 -26
  203. package/src/stories/Toaster.stories.tsx +0 -46
  204. package/src/stories/Touchable.stories.tsx +0 -40
  205. package/src/stories/VideoPlayer.stories.tsx +0 -129
@@ -0,0 +1,113 @@
1
+ import * as React from 'react'
2
+
3
+ import {Star, StarHalf} from 'lucide-react'
4
+
5
+ import {cn} from '../../lib/utils'
6
+ import {formatReviewCount, normalizeRating} from '../../utils/merchant-card'
7
+
8
+ const starSizeClasses = {
9
+ sm: 'h-3 w-3',
10
+ md: 'h-4 w-4',
11
+ lg: 'h-5 w-5',
12
+ } as const
13
+
14
+ const textSizeClasses = {
15
+ sm: 'text-xs',
16
+ md: 'text-sm',
17
+ lg: 'text-base',
18
+ } as const
19
+
20
+ export interface ProductReviewStarsProps
21
+ extends React.HTMLAttributes<HTMLDivElement> {
22
+ /**
23
+ * Average rating value (0-5)
24
+ */
25
+ averageRating: number
26
+ /**
27
+ * Total number of reviews
28
+ */
29
+ reviewCount?: number | null
30
+ /**
31
+ * Custom color for filled star
32
+ * @default 'var(--grayscale-d100)' (black)
33
+ */
34
+ filledStarColor?: string
35
+ /**
36
+ * Custom color for empty star
37
+ * @default 'var(--grayscale-l20)' (light gray)
38
+ */
39
+ emptyStarColor?: string
40
+ /**
41
+ * Size of the stars
42
+ * @default 'sm'
43
+ */
44
+ size?: 'sm' | 'md' | 'lg'
45
+ }
46
+
47
+ export const ProductReviewStars = React.forwardRef<
48
+ HTMLDivElement,
49
+ ProductReviewStarsProps
50
+ >(
51
+ (
52
+ {
53
+ averageRating,
54
+ reviewCount,
55
+ size = 'sm',
56
+ filledStarColor = 'var(--grayscale-d100)',
57
+ emptyStarColor = 'var(--grayscale-l20)',
58
+ className,
59
+ ...props
60
+ },
61
+ ref
62
+ ) => {
63
+ const normalizedRating = Math.min(5, normalizeRating(averageRating))
64
+ const filledStars = Math.floor(normalizedRating)
65
+ const remainder = normalizedRating % 1
66
+
67
+ return (
68
+ <div
69
+ ref={ref}
70
+ className={cn('flex items-center', className)}
71
+ data-slot="product-review-stars"
72
+ {...props}
73
+ >
74
+ <div className="relative">
75
+ <div className="flex">
76
+ {Array.from({length: 5}, (_, i) => (
77
+ <Star
78
+ key={i}
79
+ className={cn(starSizeClasses[size])}
80
+ style={{fill: emptyStarColor}}
81
+ strokeWidth={0}
82
+ />
83
+ ))}
84
+ </div>
85
+ <div className="flex absolute top-0 left-0">
86
+ {Array.from({length: filledStars}, (_, i) => (
87
+ <Star
88
+ key={i}
89
+ className={cn(starSizeClasses[size])}
90
+ style={{fill: filledStarColor}}
91
+ strokeWidth={0}
92
+ />
93
+ ))}
94
+ {remainder >= 0.5 && (
95
+ <StarHalf
96
+ className={cn(starSizeClasses[size])}
97
+ style={{fill: filledStarColor}}
98
+ strokeWidth={0}
99
+ />
100
+ )}
101
+ </div>
102
+ </div>
103
+ {reviewCount && (
104
+ <span className={cn(textSizeClasses[size], 'ml-1')}>
105
+ ({formatReviewCount(reviewCount)})
106
+ </span>
107
+ )}
108
+ </div>
109
+ )
110
+ }
111
+ )
112
+
113
+ ProductReviewStars.displayName = 'ProductReviewStars'
@@ -0,0 +1,38 @@
1
+ import {QueryClient} from '@tanstack/react-query'
2
+ import {renderHook} from '@testing-library/react'
3
+ import {describe, expect, it} from 'vitest'
4
+
5
+ import {MinisQueryProvider} from './MinisQueryProvider'
6
+ import {useShopMinisQueryClient} from './queryClient'
7
+
8
+ describe('MinisQueryProvider', () => {
9
+ it('provides QueryClient to children', () => {
10
+ const {result} = renderHook(() => useShopMinisQueryClient(), {
11
+ wrapper: MinisQueryProvider,
12
+ })
13
+
14
+ expect(result.current).toBeInstanceOf(QueryClient)
15
+ })
16
+
17
+ it('allows hooks to work within provider', () => {
18
+ const {result} = renderHook(() => useShopMinisQueryClient(), {
19
+ wrapper: MinisQueryProvider,
20
+ })
21
+
22
+ // Should not throw
23
+ expect(() => result.current).not.toThrow()
24
+ })
25
+
26
+ it('provides consistent QueryClient across renders', () => {
27
+ const {result, rerender} = renderHook(() => useShopMinisQueryClient(), {
28
+ wrapper: MinisQueryProvider,
29
+ })
30
+
31
+ const firstClient = result.current
32
+ rerender()
33
+ const secondClient = result.current
34
+
35
+ // Same instance across re-renders
36
+ expect(firstClient).toBe(secondClient)
37
+ })
38
+ })
@@ -0,0 +1,16 @@
1
+ import React, {useMemo} from 'react'
2
+
3
+ import {
4
+ createShopMinisQueryClient,
5
+ ShopMinisQueryClientContext,
6
+ } from './queryClient'
7
+
8
+ export function MinisQueryProvider({children}: {children: React.ReactNode}) {
9
+ const queryClient = useMemo(() => createShopMinisQueryClient(), [])
10
+
11
+ return (
12
+ <ShopMinisQueryClientContext.Provider value={queryClient}>
13
+ {children}
14
+ </ShopMinisQueryClientContext.Provider>
15
+ )
16
+ }
@@ -0,0 +1,8 @@
1
+ export {
2
+ ShopMinisQueryClientContext,
3
+ useShopMinisQueryClient,
4
+ createShopMinisQueryClient,
5
+ } from './queryClient'
6
+ export {MinisQueryProvider} from './MinisQueryProvider'
7
+ export {useShopActionQuery} from './useShopActionQuery'
8
+ export {useShopActionInfiniteQuery} from './useShopActionInfiniteQuery'
@@ -0,0 +1,91 @@
1
+ import React from 'react'
2
+
3
+ import {QueryClient} from '@tanstack/react-query'
4
+ import {renderHook} from '@testing-library/react'
5
+ import {describe, expect, it} from 'vitest'
6
+
7
+ import {
8
+ createShopMinisQueryClient,
9
+ ShopMinisQueryClientContext,
10
+ useShopMinisQueryClient,
11
+ } from './queryClient'
12
+
13
+ describe('queryClient', () => {
14
+ describe('createShopMinisQueryClient', () => {
15
+ it('creates a QueryClient instance', () => {
16
+ const client = createShopMinisQueryClient()
17
+
18
+ expect(client).toBeInstanceOf(QueryClient)
19
+ })
20
+
21
+ it('creates isolated QueryClient instances', () => {
22
+ const client1 = createShopMinisQueryClient()
23
+ const client2 = createShopMinisQueryClient()
24
+
25
+ // Each call creates a new instance
26
+ expect(client1).not.toBe(client2)
27
+ })
28
+
29
+ it('has caching disabled by default', () => {
30
+ const client = createShopMinisQueryClient()
31
+
32
+ const defaults = client.getDefaultOptions()
33
+
34
+ expect(defaults.queries?.staleTime).toBe(0)
35
+ expect(defaults.queries?.gcTime).toBe(0)
36
+ })
37
+
38
+ it('has retry and refetchOnWindowFocus configured', () => {
39
+ const client = createShopMinisQueryClient()
40
+
41
+ const defaults = client.getDefaultOptions()
42
+
43
+ expect(defaults.queries?.retry).toBe(1)
44
+ expect(defaults.queries?.refetchOnWindowFocus).toBe(false)
45
+ })
46
+ })
47
+
48
+ describe('useShopMinisQueryClient', () => {
49
+ it('throws error when used outside provider', () => {
50
+ expect(() => {
51
+ renderHook(() => useShopMinisQueryClient())
52
+ }).toThrow(
53
+ 'Shop Minis hooks must be used within <MinisContainer> or <MinisQueryProvider>. ' +
54
+ 'Wrap your component tree with one of these providers.'
55
+ )
56
+ })
57
+
58
+ it('returns QueryClient when used within provider', () => {
59
+ const client = createShopMinisQueryClient()
60
+ const wrapper = ({children}: {children: React.ReactNode}) => (
61
+ <ShopMinisQueryClientContext.Provider value={client}>
62
+ {children}
63
+ </ShopMinisQueryClientContext.Provider>
64
+ )
65
+
66
+ const {result} = renderHook(() => useShopMinisQueryClient(), {wrapper})
67
+
68
+ expect(result.current).toBe(client)
69
+ expect(result.current).toBeInstanceOf(QueryClient)
70
+ })
71
+
72
+ it('returns the same client instance across re-renders', () => {
73
+ const client = createShopMinisQueryClient()
74
+ const wrapper = ({children}: {children: React.ReactNode}) => (
75
+ <ShopMinisQueryClientContext.Provider value={client}>
76
+ {children}
77
+ </ShopMinisQueryClientContext.Provider>
78
+ )
79
+
80
+ const {result, rerender} = renderHook(() => useShopMinisQueryClient(), {
81
+ wrapper,
82
+ })
83
+
84
+ const firstClient = result.current
85
+ rerender()
86
+ const secondClient = result.current
87
+
88
+ expect(firstClient).toBe(secondClient)
89
+ })
90
+ })
91
+ })
@@ -0,0 +1,43 @@
1
+ import {createContext, useContext} from 'react'
2
+
3
+ import {QueryClient} from '@tanstack/react-query'
4
+
5
+ /**
6
+ * Custom QueryClient context for Shop Minis SDK
7
+ * This ensures our SDK always uses its own QueryClient,
8
+ * even if the parent app has their own QueryClientProvider
9
+ */
10
+ export const ShopMinisQueryClientContext = createContext<QueryClient | null>(
11
+ null
12
+ )
13
+
14
+ export function useShopMinisQueryClient(): QueryClient {
15
+ const client = useContext(ShopMinisQueryClientContext)
16
+ if (!client) {
17
+ throw new Error(
18
+ 'Shop Minis hooks must be used within <MinisContainer> or <MinisQueryProvider>. ' +
19
+ 'Wrap your component tree with one of these providers.'
20
+ )
21
+ }
22
+ return client
23
+ }
24
+
25
+ /**
26
+ * Create a QueryClient instance for Shop Minis SDK
27
+ * Isolated from any parent app's QueryClient
28
+ *
29
+ * Caching is disabled by default since Apollo provides the cache layer.
30
+ * React Query only provides request deduplication and state management.
31
+ */
32
+ export function createShopMinisQueryClient() {
33
+ return new QueryClient({
34
+ defaultOptions: {
35
+ queries: {
36
+ staleTime: 0, // Data is immediately stale
37
+ gcTime: 0, // Don't keep in cache after component unmounts
38
+ retry: 1,
39
+ refetchOnWindowFocus: false,
40
+ },
41
+ },
42
+ })
43
+ }
@@ -0,0 +1,357 @@
1
+ import React from 'react'
2
+
3
+ import {renderHook, waitFor, act} from '@testing-library/react'
4
+ import {describe, expect, it, vi} from 'vitest'
5
+
6
+ import {MinisQueryProvider} from './MinisQueryProvider'
7
+ import {useShopActionInfiniteQuery} from './useShopActionInfiniteQuery'
8
+
9
+ describe('useShopActionInfiniteQuery', () => {
10
+ const wrapper = ({children}: {children: React.ReactNode}) => (
11
+ <MinisQueryProvider>{children}</MinisQueryProvider>
12
+ )
13
+
14
+ describe('Data Fetching', () => {
15
+ it('fetches and returns flattened array data', async () => {
16
+ const mockAction = vi.fn().mockResolvedValue({
17
+ ok: true,
18
+ data: {
19
+ data: [{id: '1'}, {id: '2'}],
20
+ pageInfo: {hasNextPage: false, endCursor: null},
21
+ },
22
+ })
23
+
24
+ const {result} = renderHook(
25
+ () => useShopActionInfiniteQuery(['test-array'], mockAction, {}),
26
+ {wrapper}
27
+ )
28
+
29
+ await waitFor(() => {
30
+ expect(result.current.loading).toBe(false)
31
+ })
32
+
33
+ expect(result.current.data).toEqual([{id: '1'}, {id: '2'}])
34
+ expect(result.current.hasNextPage).toBe(false)
35
+ expect(mockAction).toHaveBeenCalledWith({})
36
+ })
37
+
38
+ it('handles non-array data by returning it directly', async () => {
39
+ const mockAction = vi.fn().mockResolvedValue({
40
+ ok: true,
41
+ data: {
42
+ data: {items: [{id: '1'}]},
43
+ pageInfo: {hasNextPage: false, endCursor: null},
44
+ },
45
+ })
46
+
47
+ const {result} = renderHook(
48
+ () => useShopActionInfiniteQuery(['test-nonarray'], mockAction, {}),
49
+ {wrapper}
50
+ )
51
+
52
+ await waitFor(() => {
53
+ expect(result.current.loading).toBe(false)
54
+ })
55
+
56
+ // Non-array data is returned as-is from the first page
57
+ expect(result.current.data).toEqual({items: [{id: '1'}]})
58
+ })
59
+
60
+ it('returns null when data is null', async () => {
61
+ const mockAction = vi.fn().mockResolvedValue({
62
+ ok: true,
63
+ data: {
64
+ data: null,
65
+ pageInfo: {hasNextPage: false, endCursor: null},
66
+ },
67
+ })
68
+
69
+ const {result} = renderHook(
70
+ () => useShopActionInfiniteQuery(['test-null-data'], mockAction, {}),
71
+ {wrapper}
72
+ )
73
+
74
+ await waitFor(() => {
75
+ expect(result.current.loading).toBe(false)
76
+ })
77
+
78
+ expect(result.current.data).toBeNull()
79
+ })
80
+ })
81
+
82
+ describe('Pagination Logic', () => {
83
+ it('appends data when fetching more pages', async () => {
84
+ const page1 = [{id: '1'}]
85
+ const page2 = [{id: '2'}]
86
+
87
+ const mockAction = vi
88
+ .fn()
89
+ .mockResolvedValueOnce({
90
+ ok: true,
91
+ data: {
92
+ data: page1,
93
+ pageInfo: {hasNextPage: true, endCursor: 'cursor1'},
94
+ },
95
+ })
96
+ .mockResolvedValueOnce({
97
+ ok: true,
98
+ data: {
99
+ data: page2,
100
+ pageInfo: {hasNextPage: false, endCursor: null},
101
+ },
102
+ })
103
+
104
+ const {result} = renderHook(
105
+ () => useShopActionInfiniteQuery(['test-pagination'], mockAction, {}),
106
+ {wrapper}
107
+ )
108
+
109
+ await waitFor(() => {
110
+ expect(result.current.data).toEqual(page1)
111
+ expect(result.current.hasNextPage).toBe(true)
112
+ })
113
+
114
+ // Fetch more
115
+ await act(async () => {
116
+ await result.current.fetchMore()
117
+ })
118
+
119
+ // Data should be flattened and appended
120
+ await waitFor(() => {
121
+ expect(result.current.data).toEqual([...page1, ...page2])
122
+ })
123
+
124
+ expect(result.current.hasNextPage).toBe(false)
125
+ expect(mockAction).toHaveBeenCalledTimes(2)
126
+ })
127
+
128
+ it('passes cursor to subsequent pages', async () => {
129
+ const mockAction = vi
130
+ .fn()
131
+ .mockResolvedValueOnce({
132
+ ok: true,
133
+ data: {
134
+ data: [{id: '1'}],
135
+ pageInfo: {hasNextPage: true, endCursor: 'cursor1'},
136
+ },
137
+ })
138
+ .mockResolvedValueOnce({
139
+ ok: true,
140
+ data: {
141
+ data: [{id: '2'}],
142
+ pageInfo: {hasNextPage: false, endCursor: null},
143
+ },
144
+ })
145
+
146
+ const {result} = renderHook(
147
+ () => useShopActionInfiniteQuery(['test-cursor'], mockAction, {}),
148
+ {wrapper}
149
+ )
150
+
151
+ await waitFor(() => {
152
+ expect(result.current.data).toEqual([{id: '1'}])
153
+ expect(result.current.hasNextPage).toBe(true)
154
+ })
155
+
156
+ await act(async () => {
157
+ await result.current.fetchMore()
158
+ })
159
+
160
+ // Second call should include the cursor
161
+ expect(mockAction).toHaveBeenNthCalledWith(2, {after: 'cursor1'})
162
+ })
163
+
164
+ it('preserves other params when fetching more', async () => {
165
+ const mockAction = vi
166
+ .fn()
167
+ .mockResolvedValueOnce({
168
+ ok: true,
169
+ data: {
170
+ data: [{id: '1'}],
171
+ pageInfo: {hasNextPage: true, endCursor: 'cursor1'},
172
+ },
173
+ })
174
+ .mockResolvedValueOnce({
175
+ ok: true,
176
+ data: {
177
+ data: [{id: '2'}],
178
+ pageInfo: {hasNextPage: false, endCursor: null},
179
+ },
180
+ })
181
+
182
+ const {result} = renderHook(
183
+ () =>
184
+ useShopActionInfiniteQuery(['test-params'], mockAction, {
185
+ query: 'shoes',
186
+ fetchPolicy: 'cache-first',
187
+ }),
188
+ {wrapper}
189
+ )
190
+
191
+ await waitFor(() => {
192
+ expect(result.current.data).toEqual([{id: '1'}])
193
+ expect(result.current.hasNextPage).toBe(true)
194
+ })
195
+
196
+ await act(async () => {
197
+ await result.current.fetchMore()
198
+ })
199
+
200
+ // Should preserve original params and add cursor
201
+ expect(mockAction).toHaveBeenNthCalledWith(2, {
202
+ query: 'shoes',
203
+ fetchPolicy: 'cache-first',
204
+ after: 'cursor1',
205
+ })
206
+ })
207
+ })
208
+
209
+ describe('Error Handling', () => {
210
+ it('handles action errors', async () => {
211
+ const mockAction = vi.fn().mockRejectedValue(new Error('API Error'))
212
+
213
+ const {result} = renderHook(
214
+ () => useShopActionInfiniteQuery(['test-api-error'], mockAction, {}),
215
+ {wrapper}
216
+ )
217
+
218
+ // Wait for loading to complete (after retries)
219
+ await waitFor(
220
+ () => {
221
+ expect(result.current.loading).toBe(false)
222
+ },
223
+ {timeout: 3000}
224
+ )
225
+
226
+ expect(result.current.data).toBeNull()
227
+ expect(result.current.error).toBeInstanceOf(Error)
228
+ expect(result.current.error?.message).toBe('API Error')
229
+ })
230
+ })
231
+
232
+ describe('Skip Parameter', () => {
233
+ it('does not fetch when skip is true', async () => {
234
+ const mockAction = vi.fn()
235
+
236
+ renderHook(
237
+ () =>
238
+ useShopActionInfiniteQuery(
239
+ ['test-skip-inf'],
240
+ mockAction,
241
+ {},
242
+ {skip: true}
243
+ ),
244
+ {wrapper}
245
+ )
246
+
247
+ await new Promise(resolve => setTimeout(resolve, 100))
248
+
249
+ expect(mockAction).not.toHaveBeenCalled()
250
+ })
251
+
252
+ it('fetches when skip is false', async () => {
253
+ const mockAction = vi.fn().mockResolvedValue({
254
+ ok: true,
255
+ data: {
256
+ data: [{id: 'noskip'}],
257
+ pageInfo: {hasNextPage: false, endCursor: null},
258
+ },
259
+ })
260
+
261
+ const {result} = renderHook(
262
+ () =>
263
+ useShopActionInfiniteQuery(
264
+ ['test-noskip-fetch'],
265
+ mockAction,
266
+ {},
267
+ {skip: false}
268
+ ),
269
+ {wrapper}
270
+ )
271
+
272
+ await waitFor(() => {
273
+ expect(result.current.data).toEqual([{id: 'noskip'}])
274
+ })
275
+
276
+ expect(mockAction).toHaveBeenCalled()
277
+ })
278
+ })
279
+
280
+ describe('API Contract', () => {
281
+ it('returns expected shape', async () => {
282
+ const mockAction = vi.fn().mockResolvedValue({
283
+ ok: true,
284
+ data: {
285
+ data: [{id: 'contract'}],
286
+ pageInfo: {hasNextPage: false, endCursor: null},
287
+ },
288
+ })
289
+
290
+ const {result} = renderHook(
291
+ () => useShopActionInfiniteQuery(['test-contract-inf'], mockAction, {}),
292
+ {wrapper}
293
+ )
294
+
295
+ await waitFor(() => {
296
+ expect(result.current.loading).toBe(false)
297
+ })
298
+
299
+ // Verify all expected properties exist
300
+ expect(result.current).toHaveProperty('data')
301
+ expect(result.current).toHaveProperty('loading')
302
+ expect(result.current).toHaveProperty('error')
303
+ expect(result.current).toHaveProperty('hasNextPage')
304
+ expect(result.current).toHaveProperty('fetchMore')
305
+ expect(result.current).toHaveProperty('refetch')
306
+
307
+ // Verify types
308
+ expect(typeof result.current.loading).toBe('boolean')
309
+ expect(typeof result.current.hasNextPage).toBe('boolean')
310
+ expect(typeof result.current.fetchMore).toBe('function')
311
+ expect(typeof result.current.refetch).toBe('function')
312
+ })
313
+ })
314
+
315
+ describe('hasNextPage Logic', () => {
316
+ it('sets hasNextPage based on pageInfo', async () => {
317
+ const mockAction = vi.fn().mockResolvedValue({
318
+ ok: true,
319
+ data: {
320
+ data: [{id: '1'}],
321
+ pageInfo: {hasNextPage: true, endCursor: 'cursor1'},
322
+ },
323
+ })
324
+
325
+ const {result} = renderHook(
326
+ () => useShopActionInfiniteQuery(['test-hasnext'], mockAction, {}),
327
+ {wrapper}
328
+ )
329
+
330
+ await waitFor(() => {
331
+ expect(result.current.data).toEqual([{id: '1'}])
332
+ expect(result.current.hasNextPage).toBe(true)
333
+ })
334
+ })
335
+
336
+ it('returns false when no more pages', async () => {
337
+ const mockAction = vi.fn().mockResolvedValue({
338
+ ok: true,
339
+ data: {
340
+ data: [{id: '1'}],
341
+ pageInfo: {hasNextPage: false, endCursor: null},
342
+ },
343
+ })
344
+
345
+ const {result} = renderHook(
346
+ () => useShopActionInfiniteQuery(['test-nonext'], mockAction, {}),
347
+ {wrapper}
348
+ )
349
+
350
+ await waitFor(() => {
351
+ expect(result.current.loading).toBe(false)
352
+ })
353
+
354
+ expect(result.current.hasNextPage).toBe(false)
355
+ })
356
+ })
357
+ })