@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
@@ -20,6 +20,7 @@ import {
20
20
  ProductCardFavoriteButton,
21
21
  ProductCardInfo,
22
22
  ProductCardTitle,
23
+ ProductCardReviewStars,
23
24
  ProductCardPrice,
24
25
  } from './product-card'
25
26
 
@@ -174,6 +175,108 @@ describe('ProductCard', () => {
174
175
  const favoriteButton = screen.getByRole('button')
175
176
  expect(favoriteButton).toBeInTheDocument()
176
177
  })
178
+
179
+ it('renders review stars when review data is available', () => {
180
+ const product = mockProduct({
181
+ reviewAnalytics: {
182
+ averageRating: 4.5,
183
+ reviewCount: 123,
184
+ },
185
+ })
186
+
187
+ render(<ProductCard product={product} />)
188
+
189
+ // Should show the review stars and count
190
+ expect(screen.getByText('(123)')).toBeInTheDocument()
191
+
192
+ // Check for star elements - ProductReviewStars uses overlay technique with 10 stars total
193
+ const container = screen.getByText('(123)').parentElement
194
+ const stars = container?.querySelectorAll('svg')
195
+ // 5 empty stars + filled stars overlay (4 full + 1 half for 4.5 rating)
196
+ expect(stars).toHaveLength(10)
197
+
198
+ // Verify review count is displayed
199
+ expect(screen.getByText('(123)')).toBeInTheDocument()
200
+ })
201
+
202
+ it('does not render review stars when review data is missing', () => {
203
+ const product = mockProduct({
204
+ reviewAnalytics: undefined,
205
+ })
206
+
207
+ render(<ProductCard product={product} />)
208
+
209
+ // Should not show review count
210
+ expect(screen.queryByText(/^\(\d+\)$/)).not.toBeInTheDocument()
211
+ })
212
+
213
+ it('does not render review stars when averageRating is null', () => {
214
+ const product = mockProduct({
215
+ reviewAnalytics: {
216
+ averageRating: null,
217
+ reviewCount: 100,
218
+ },
219
+ })
220
+
221
+ render(<ProductCard product={product} />)
222
+
223
+ // Should not show review count
224
+ expect(screen.queryByText('(100)')).not.toBeInTheDocument()
225
+ })
226
+
227
+ it('does not render review stars when reviewCount is 0', () => {
228
+ const product = mockProduct({
229
+ reviewAnalytics: {
230
+ averageRating: 4.5,
231
+ reviewCount: 0,
232
+ },
233
+ })
234
+
235
+ render(<ProductCard product={product} />)
236
+
237
+ // Should not show review count
238
+ expect(screen.queryByText('(0)')).not.toBeInTheDocument()
239
+ })
240
+
241
+ it('formats large review counts correctly', () => {
242
+ const product = mockProduct({
243
+ reviewAnalytics: {
244
+ averageRating: 4.0,
245
+ reviewCount: 1500,
246
+ },
247
+ })
248
+
249
+ render(<ProductCard product={product} />)
250
+
251
+ // Should format as 1K
252
+ expect(screen.getByText('(1K)')).toBeInTheDocument()
253
+ })
254
+
255
+ it('does not show review stars in non-default variants', () => {
256
+ const product = mockProduct({
257
+ reviewAnalytics: {
258
+ averageRating: 4.5,
259
+ reviewCount: 123,
260
+ },
261
+ })
262
+
263
+ const {rerender} = render(
264
+ <ProductCard product={product} variant="priceOverlay" />
265
+ )
266
+
267
+ // Should not show review stars in priceOverlay variant
268
+ expect(screen.queryByText('(123)')).not.toBeInTheDocument()
269
+
270
+ rerender(<ProductCard product={product} variant="compact" />)
271
+
272
+ // Should not show review stars in compact variant
273
+ expect(screen.queryByText('(123)')).not.toBeInTheDocument()
274
+
275
+ rerender(<ProductCard product={product} variant="default" />)
276
+
277
+ // Should show review stars in default variant
278
+ expect(screen.getByText('(123)')).toBeInTheDocument()
279
+ })
177
280
  })
178
281
 
179
282
  describe('Interactions', () => {
@@ -357,6 +460,38 @@ describe('ProductCard', () => {
357
460
  expect(screen.getByText('$99.99')).toBeInTheDocument()
358
461
  })
359
462
 
463
+ it('allows ProductCardReviewStars in custom composition', () => {
464
+ const product = mockProduct({
465
+ reviewAnalytics: {
466
+ averageRating: 4.2,
467
+ reviewCount: 50,
468
+ },
469
+ })
470
+
471
+ render(
472
+ <ProductCard product={product}>
473
+ <ProductCardContainer>
474
+ <ProductCardImageContainer>
475
+ <ProductCardImage />
476
+ </ProductCardImageContainer>
477
+ <ProductCardInfo>
478
+ <ProductCardTitle />
479
+ <ProductCardReviewStars />
480
+ <ProductCardPrice />
481
+ </ProductCardInfo>
482
+ </ProductCardContainer>
483
+ </ProductCard>
484
+ )
485
+
486
+ // Review stars should be rendered
487
+ expect(screen.getByText('(50)')).toBeInTheDocument()
488
+
489
+ // Check for star elements - ProductReviewStars uses overlay technique
490
+ const container = screen.getByText('(50)').parentElement
491
+ const stars = container?.querySelectorAll('svg')
492
+ expect(stars).toHaveLength(9) // 5 empty + 3 full + 1 half for 3.5 rating
493
+ })
494
+
360
495
  it('respects favoriteButtonDisabled in custom composition', () => {
361
496
  const product = mockProduct()
362
497
 
@@ -5,6 +5,7 @@ import {type Product, type ProductVariant} from '@shopify/shop-minis-platform'
5
5
 
6
6
  import {useShopNavigation} from '../../hooks/navigation/useShopNavigation'
7
7
  import {useSavedProductsActions} from '../../hooks/user/useSavedProductsActions'
8
+ import {ProductReviewStars} from '../../internal/components/product-review-stars'
8
9
  import {formatMoney} from '../../lib/formatMoney'
9
10
  import {cn} from '../../lib/utils'
10
11
  import {Image} from '../atoms/image'
@@ -25,10 +26,11 @@ interface ProductCardContextValue {
25
26
  touchable: boolean
26
27
  badgeText?: string
27
28
  badgeVariant?: 'primary' | 'secondary' | 'destructive' | 'outline' | 'none'
29
+ favoriteButtonDisabled: boolean
30
+ reviewsDisabled: boolean
28
31
 
29
32
  // State
30
33
  isFavorited: boolean
31
- isFavoriteButtonDisabled: boolean
32
34
 
33
35
  // Actions
34
36
  onClick: () => void
@@ -194,9 +196,9 @@ function ProductCardFavoriteButton({
194
196
  className,
195
197
  ...props
196
198
  }: React.ComponentProps<'div'>) {
197
- const {isFavorited, isFavoriteButtonDisabled, onFavoriteToggle} =
199
+ const {isFavorited, favoriteButtonDisabled, onFavoriteToggle} =
198
200
  useProductCardContext()
199
- if (isFavoriteButtonDisabled) return null
201
+ if (favoriteButtonDisabled) return null
200
202
 
201
203
  return (
202
204
  <div className={cn('absolute bottom-3 right-3 z-10', className)} {...props}>
@@ -213,7 +215,7 @@ function ProductCardInfo({className, ...props}: React.ComponentProps<'div'>) {
213
215
 
214
216
  return (
215
217
  <div
216
- data-slot="product-card-info"
218
+ data-testid="product-card-info"
217
219
  className={cn('px-1 pt-2 pb-0 space-y-1', className)}
218
220
  {...props}
219
221
  />
@@ -241,6 +243,31 @@ function ProductCardTitle({
241
243
  )
242
244
  }
243
245
 
246
+ function ProductCardReviewStars({
247
+ className,
248
+ ...props
249
+ }: React.ComponentProps<'div'>) {
250
+ const {product, reviewsDisabled} = useProductCardContext()
251
+ const reviewAnalytics = product.reviewAnalytics
252
+
253
+ if (reviewsDisabled || !reviewAnalytics?.averageRating) {
254
+ return null
255
+ }
256
+
257
+ return (
258
+ <div
259
+ data-slot="product-card-review-stars"
260
+ className={cn('', className)}
261
+ {...props}
262
+ >
263
+ <ProductReviewStars
264
+ averageRating={reviewAnalytics.averageRating}
265
+ reviewCount={reviewAnalytics.reviewCount}
266
+ />
267
+ </div>
268
+ )
269
+ }
270
+
244
271
  function ProductCardPrice({className}: {className?: string}) {
245
272
  const {product, selectedProductVariant} = useProductCardContext()
246
273
 
@@ -297,6 +324,8 @@ export interface ProductCardProps {
297
324
  children?: React.ReactNode
298
325
  /** Whether the favorite button is disabled */
299
326
  favoriteButtonDisabled?: boolean
327
+ /** Whether review stars are disabled */
328
+ reviewsDisabled?: boolean
300
329
  }
301
330
 
302
331
  function ProductCard({
@@ -310,6 +339,7 @@ function ProductCard({
310
339
  onFavoriteToggled,
311
340
  children,
312
341
  favoriteButtonDisabled = false,
342
+ reviewsDisabled = false,
313
343
  }: ProductCardProps) {
314
344
  const {navigateToProduct} = useShopNavigation()
315
345
  const {saveProduct, unsaveProduct} = useSavedProductsActions()
@@ -377,10 +407,11 @@ function ProductCard({
377
407
  touchable,
378
408
  badgeText,
379
409
  badgeVariant,
410
+ favoriteButtonDisabled,
411
+ reviewsDisabled,
380
412
 
381
413
  // State
382
414
  isFavorited: isFavoritedLocal,
383
- isFavoriteButtonDisabled: favoriteButtonDisabled,
384
415
  // Actions
385
416
  onClick: handleClick,
386
417
  onFavoriteToggle: handleFavoriteClick,
@@ -396,6 +427,7 @@ function ProductCard({
396
427
  handleClick,
397
428
  handleFavoriteClick,
398
429
  favoriteButtonDisabled,
430
+ reviewsDisabled,
399
431
  ]
400
432
  )
401
433
 
@@ -412,6 +444,7 @@ function ProductCard({
412
444
  {variant === 'default' && (
413
445
  <ProductCardInfo>
414
446
  <ProductCardTitle />
447
+ <ProductCardReviewStars />
415
448
  <ProductCardPrice />
416
449
  </ProductCardInfo>
417
450
  )}
@@ -430,5 +463,6 @@ export {
430
463
  ProductCardFavoriteButton,
431
464
  ProductCardInfo,
432
465
  ProductCardTitle,
466
+ ProductCardReviewStars,
433
467
  ProductCardPrice,
434
468
  }
@@ -442,10 +442,22 @@ describe('ProductLink', () => {
442
442
 
443
443
  render(<ProductLink product={product} />)
444
444
 
445
- const stars = document.querySelectorAll('svg')
446
- const filledStars = Array.from(stars).filter(
447
- star => star.getAttribute('fill') === 'currentColor'
445
+ // ProductReviewStars uses overlay technique with specific fill colors
446
+ // Find the star container first to only count star SVGs
447
+ const starContainer = document.querySelector(
448
+ '[data-slot="product-review-stars"]'
448
449
  )
450
+ expect(starContainer).toBeInTheDocument()
451
+
452
+ const stars = starContainer?.querySelectorAll('svg') || []
453
+ // For 5 star rating: 5 empty stars + 5 filled stars = 10 total
454
+ expect(stars).toHaveLength(10)
455
+
456
+ // Check that stars are rendered (5 filled stars using var(--grayscale-d100) style)
457
+ const filledStars = Array.from(stars).filter(star => {
458
+ const style = star.getAttribute('style')
459
+ return style?.includes('var(--grayscale-d100)')
460
+ })
449
461
  expect(filledStars).toHaveLength(5)
450
462
  })
451
463
  })
@@ -2,11 +2,11 @@ import * as React from 'react'
2
2
 
3
3
  import {type Product} from '@shopify/shop-minis-platform'
4
4
  import {cva, type VariantProps} from 'class-variance-authority'
5
- import {Star} from 'lucide-react'
6
5
  import {Slot as SlotPrimitive} from 'radix-ui'
7
6
 
8
7
  import {useShopNavigation} from '../../hooks/navigation/useShopNavigation'
9
8
  import {useSavedProductsActions} from '../../hooks/user/useSavedProductsActions'
9
+ import {ProductReviewStars} from '../../internal/components/product-review-stars'
10
10
  import {formatMoney} from '../../lib/formatMoney'
11
11
  import {cn} from '../../lib/utils'
12
12
  import {Touchable} from '../atoms/touchable'
@@ -183,10 +183,7 @@ function ProductLinkRating({className, ...props}: React.ComponentProps<'div'>) {
183
183
  return (
184
184
  <div
185
185
  data-slot="product-link-rating"
186
- className={cn(
187
- 'flex items-center gap-1 text-xs text-muted-foreground',
188
- className
189
- )}
186
+ className={cn('', className)}
190
187
  {...props}
191
188
  />
192
189
  )
@@ -233,6 +230,7 @@ export type ProductLinkProps = {
233
230
  product: Product
234
231
  hideFavoriteAction?: boolean
235
232
  onClick?: (product: Product) => void
233
+ reviewsDisabled?: boolean
236
234
  } & (
237
235
  | {
238
236
  customAction?: never
@@ -251,6 +249,7 @@ function ProductLink({
251
249
  onClick,
252
250
  customAction,
253
251
  onCustomActionClick,
252
+ reviewsDisabled = false,
254
253
  }: ProductLinkProps) {
255
254
  const {navigateToProduct} = useShopNavigation()
256
255
  const {saveProduct, unsaveProduct} = useSavedProductsActions()
@@ -354,27 +353,12 @@ function ProductLink({
354
353
  <ProductLinkInfo layout="horizontal">
355
354
  <ProductLinkTitle>{title}</ProductLinkTitle>
356
355
 
357
- {reviewCount && averageRating ? (
356
+ {averageRating && !reviewsDisabled ? (
358
357
  <ProductLinkRating>
359
- <div className="flex items-center gap-1">
360
- {Array.from({length: 5}, (_, i) => (
361
- <Star
362
- key={i}
363
- fill={
364
- i < Math.floor(averageRating!) ? 'currentColor' : 'none'
365
- }
366
- className={cn(
367
- 'h-3 w-3',
368
- i < Math.floor(averageRating!)
369
- ? 'text-primary'
370
- : 'text-gray-300'
371
- )}
372
- />
373
- ))}
374
- <span className="text-xs text-gray-600 ml-1">
375
- ({reviewCount})
376
- </span>
377
- </div>
358
+ <ProductReviewStars
359
+ averageRating={averageRating}
360
+ reviewCount={reviewCount}
361
+ />
378
362
  </ProductLinkRating>
379
363
  ) : null}
380
364
 
@@ -246,8 +246,8 @@ function Search({
246
246
 
247
247
  return (
248
248
  <SearchProvider initialQuery={initialQuery}>
249
- <div className={cn('flex flex-col ', className)}>
250
- <div className="fixed top-0 left-0 right-0 p-4 w-full z-20 bg-background">
249
+ <div className={cn('flex flex-col relative', className)}>
250
+ <div className="absolute top-0 left-0 right-0 p-4 w-full z-20 bg-background">
251
251
  <SearchInput placeholder={placeholder} inputProps={inputProps} />
252
252
  </div>
253
253
  <div className="h-14" />
@@ -26,6 +26,7 @@ export * from './atoms/list'
26
26
  export * from './atoms/video-player'
27
27
  export * from './atoms/text-input'
28
28
  export * from './atoms/content-wrapper'
29
+ export * from './atoms/product-variant-price'
29
30
 
30
31
  export * from './ui/accordion'
31
32
  export * from './ui/alert'
@@ -1,5 +1,5 @@
1
1
  import {useTheme} from 'next-themes'
2
- import {Toaster as Sonner, ToasterProps} from 'sonner'
2
+ import {Toaster as Sonner, ToasterProps, toast} from 'sonner'
3
3
 
4
4
  const Toaster = ({...props}: ToasterProps) => {
5
5
  const {theme = 'system'} = useTheme()
@@ -20,4 +20,4 @@ const Toaster = ({...props}: ToasterProps) => {
20
20
  )
21
21
  }
22
22
 
23
- export {Toaster}
23
+ export {Toaster, toast}
@@ -1,7 +1,5 @@
1
- import {useMemo} from 'react'
2
-
1
+ import {useShopActionQuery} from '../../internal/reactQuery'
3
2
  import {useShopActions} from '../../internal/useShopActions'
4
- import {useShopActionsDataFetching} from '../../internal/useShopActionsDataFetching'
5
3
  import {
6
4
  Content,
7
5
  ContentIdentifierInput,
@@ -27,24 +25,15 @@ export const useContent = (params: UseContentParams): UseContentReturns => {
27
25
  const {getContent} = useShopActions()
28
26
  const {identifiers, skip = false, ...restParams} = params
29
27
 
30
- const {data, ...rest} = useShopActionsDataFetching(
28
+ const {data, ...rest} = useShopActionQuery(
29
+ ['content', identifiers, restParams],
31
30
  getContent,
32
- {
33
- identifiers,
34
- ...restParams,
35
- },
36
- {
37
- skip,
38
- hook: 'useContent',
39
- }
31
+ {identifiers, ...restParams},
32
+ {skip}
40
33
  )
41
34
 
42
- const content = useMemo(() => {
43
- return data ?? null
44
- }, [data])
45
-
46
35
  return {
47
36
  ...rest,
48
- content,
37
+ content: data,
49
38
  }
50
39
  }
@@ -86,47 +86,59 @@ describe('useNavigateWithTransition', () => {
86
86
  })
87
87
 
88
88
  it('uses view transition for path navigation', async () => {
89
+ const scrollToSpy = vi.spyOn(window, 'scrollTo')
89
90
  const {result} = renderHook(() => useNavigateWithTransition())
90
91
 
91
92
  await act(async () => {
92
93
  result.current('/new-route')
94
+ await mockTransition.finished
93
95
  })
94
96
 
95
97
  expect(document.startViewTransition).toHaveBeenCalled()
96
98
  expect(mockNavigate).toHaveBeenCalledWith('/new-route', {
97
- preventScrollReset: true,
98
99
  replace: false,
99
100
  })
101
+ expect(scrollToSpy).toHaveBeenCalledWith(0, 0)
102
+
103
+ scrollToSpy.mockRestore()
100
104
  })
101
105
 
102
106
  it('uses replace navigation for same route', async () => {
107
+ const scrollToSpy = vi.spyOn(window, 'scrollTo')
103
108
  const {result} = renderHook(() => useNavigateWithTransition())
104
109
 
105
110
  await act(async () => {
106
111
  result.current('/current')
112
+ await mockTransition.finished
107
113
  })
108
114
 
109
115
  expect(document.startViewTransition).toHaveBeenCalled()
110
116
  expect(mockNavigate).toHaveBeenCalledWith('/current', {
111
- preventScrollReset: true,
112
117
  replace: true,
113
118
  })
119
+ expect(scrollToSpy).toHaveBeenCalledWith(0, 0)
120
+
121
+ scrollToSpy.mockRestore()
114
122
  })
115
123
 
116
124
  it('merges custom options with defaults', async () => {
125
+ const scrollToSpy = vi.spyOn(window, 'scrollTo')
117
126
  const {result} = renderHook(() => useNavigateWithTransition())
118
127
 
119
128
  const customOptions = {state: {from: 'test'}, replace: true}
120
129
 
121
130
  await act(async () => {
122
131
  result.current('/new-route', customOptions)
132
+ await mockTransition.finished
123
133
  })
124
134
 
125
135
  expect(mockNavigate).toHaveBeenCalledWith('/new-route', {
126
- preventScrollReset: true,
127
136
  replace: true,
128
137
  state: {from: 'test'},
129
138
  })
139
+ expect(scrollToSpy).toHaveBeenCalledWith(0, 0)
140
+
141
+ scrollToSpy.mockRestore()
130
142
  })
131
143
 
132
144
  it('removes navigation type attribute after transition completes', async () => {
@@ -183,14 +195,20 @@ describe('useNavigateWithTransition', () => {
183
195
  })
184
196
 
185
197
  it('uses view transition for delta navigation', async () => {
198
+ const scrollToSpy = vi.spyOn(window, 'scrollTo')
186
199
  const {result} = renderHook(() => useNavigateWithTransition())
187
200
 
188
201
  await act(async () => {
189
202
  result.current(-1)
203
+ await mockTransition.finished
190
204
  })
191
205
 
192
206
  expect(document.startViewTransition).toHaveBeenCalled()
193
207
  expect(mockNavigate).toHaveBeenCalledWith(-1)
208
+ // Delta navigation should NOT reset scroll
209
+ expect(scrollToSpy).not.toHaveBeenCalled()
210
+
211
+ scrollToSpy.mockRestore()
194
212
  })
195
213
 
196
214
  it('removes attribute after delta navigation transition', async () => {
@@ -210,14 +228,39 @@ describe('useNavigateWithTransition', () => {
210
228
  })
211
229
 
212
230
  it('handles positive delta navigation', async () => {
231
+ const scrollToSpy = vi.spyOn(window, 'scrollTo')
213
232
  const {result} = renderHook(() => useNavigateWithTransition())
214
233
 
215
234
  await act(async () => {
216
235
  result.current(1)
236
+ await mockTransition.finished
217
237
  })
218
238
 
219
239
  expect(document.startViewTransition).toHaveBeenCalled()
220
240
  expect(mockNavigate).toHaveBeenCalledWith(1)
241
+ // Delta navigation should NOT reset scroll
242
+ expect(scrollToSpy).not.toHaveBeenCalled()
243
+
244
+ scrollToSpy.mockRestore()
245
+ })
246
+
247
+ it('does not reset scroll when preventScrollReset is true', async () => {
248
+ const scrollToSpy = vi.spyOn(window, 'scrollTo')
249
+ const {result} = renderHook(() => useNavigateWithTransition())
250
+
251
+ await act(async () => {
252
+ result.current('/new-route', {preventScrollReset: true})
253
+ await mockTransition.finished
254
+ })
255
+
256
+ expect(document.startViewTransition).toHaveBeenCalled()
257
+ expect(mockNavigate).toHaveBeenCalledWith('/new-route', {
258
+ replace: false,
259
+ preventScrollReset: true,
260
+ })
261
+ expect(scrollToSpy).not.toHaveBeenCalled()
262
+
263
+ scrollToSpy.mockRestore()
221
264
  })
222
265
  })
223
266
 
@@ -243,7 +286,6 @@ describe('useNavigateWithTransition', () => {
243
286
  })
244
287
 
245
288
  expect(mockNavigate).toHaveBeenCalledWith('', {
246
- preventScrollReset: true,
247
289
  replace: false,
248
290
  })
249
291
  })
@@ -256,7 +298,6 @@ describe('useNavigateWithTransition', () => {
256
298
  })
257
299
 
258
300
  expect(mockNavigate).toHaveBeenCalledWith('/', {
259
- preventScrollReset: true,
260
301
  replace: false,
261
302
  })
262
303
  })
@@ -269,7 +310,6 @@ describe('useNavigateWithTransition', () => {
269
310
  })
270
311
 
271
312
  expect(mockNavigate).toHaveBeenCalledWith('/search?q=test&page=2', {
272
- preventScrollReset: true,
273
313
  replace: false,
274
314
  })
275
315
  })
@@ -282,7 +322,6 @@ describe('useNavigateWithTransition', () => {
282
322
  })
283
323
 
284
324
  expect(mockNavigate).toHaveBeenCalledWith('/page#section', {
285
- preventScrollReset: true,
286
325
  replace: false,
287
326
  })
288
327
  })
@@ -43,10 +43,13 @@ export function useNavigateWithTransition(): UseNavigateWithTransitionReturns {
43
43
  if (document.startViewTransition) {
44
44
  const transition = document.startViewTransition(() => {
45
45
  navigate(to, {
46
- preventScrollReset: true,
47
46
  replace: isSameRoute,
48
47
  ...options,
49
48
  })
49
+
50
+ if (options?.preventScrollReset !== true) {
51
+ window.scrollTo(0, 0)
52
+ }
50
53
  })
51
54
 
52
55
  transition.finished
@@ -1,5 +1,5 @@
1
+ import {useShopActionInfiniteQuery} from '../../internal/reactQuery'
1
2
  import {useShopActions} from '../../internal/useShopActions'
2
- import {useShopActionsPaginatedDataFetching} from '../../internal/useShopActionsPaginatedDataFetching'
3
3
  import {
4
4
  Product,
5
5
  PaginatedDataHookOptionsBase,
@@ -25,13 +25,11 @@ export const useCuratedProducts = (
25
25
  const {getCuratedProducts} = useShopActions()
26
26
  const {skip, ...shopActionParams} = params ?? {}
27
27
 
28
- const {data, ...rest} = useShopActionsPaginatedDataFetching(
28
+ const {data, ...rest} = useShopActionInfiniteQuery(
29
+ ['curatedProducts', shopActionParams],
29
30
  getCuratedProducts,
30
31
  shopActionParams,
31
- {
32
- skip,
33
- hook: 'useCuratedProducts',
34
- }
32
+ {skip}
35
33
  )
36
34
 
37
35
  return {
@@ -1,5 +1,5 @@
1
+ import {useShopActionInfiniteQuery} from '../../internal/reactQuery'
1
2
  import {useShopActions} from '../../internal/useShopActions'
2
- import {useShopActionsPaginatedDataFetching} from '../../internal/useShopActionsPaginatedDataFetching'
3
3
  import {
4
4
  Product,
5
5
  PaginatedDataHookOptionsBase,
@@ -21,13 +21,11 @@ export const usePopularProducts = (
21
21
  const {getPopularProducts} = useShopActions()
22
22
  const {skip, ...shopActionParams} = params ?? {}
23
23
 
24
- const {data, ...rest} = useShopActionsPaginatedDataFetching(
24
+ const {data, ...rest} = useShopActionInfiniteQuery(
25
+ ['popularProducts', shopActionParams],
25
26
  getPopularProducts,
26
27
  shopActionParams,
27
- {
28
- skip,
29
- hook: 'usePopularProducts',
30
- }
28
+ {skip}
31
29
  )
32
30
 
33
31
  return {