@shopify/shop-minis-react 0.3.4 → 0.4.1

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 (185) hide show
  1. package/dist/_virtual/index3.js +2 -5
  2. package/dist/_virtual/index3.js.map +1 -1
  3. package/dist/_virtual/index4.js +5 -2
  4. package/dist/_virtual/index4.js.map +1 -1
  5. package/dist/components/MinisContainer.js +11 -10
  6. package/dist/components/MinisContainer.js.map +1 -1
  7. package/dist/hooks/content/useContent.js +12 -18
  8. package/dist/hooks/content/useContent.js.map +1 -1
  9. package/dist/hooks/product/useCuratedProducts.js +9 -11
  10. package/dist/hooks/product/useCuratedProducts.js.map +1 -1
  11. package/dist/hooks/product/usePopularProducts.js +9 -11
  12. package/dist/hooks/product/usePopularProducts.js.map +1 -1
  13. package/dist/hooks/product/useProduct.js +11 -17
  14. package/dist/hooks/product/useProduct.js.map +1 -1
  15. package/dist/hooks/product/useProductList.js +10 -21
  16. package/dist/hooks/product/useProductList.js.map +1 -1
  17. package/dist/hooks/product/useProductLists.js +11 -13
  18. package/dist/hooks/product/useProductLists.js.map +1 -1
  19. package/dist/hooks/product/useProductMedia.js +12 -18
  20. package/dist/hooks/product/useProductMedia.js.map +1 -1
  21. package/dist/hooks/product/useProductSearch.js +34 -27
  22. package/dist/hooks/product/useProductSearch.js.map +1 -1
  23. package/dist/hooks/product/useProductVariants.js +11 -14
  24. package/dist/hooks/product/useProductVariants.js.map +1 -1
  25. package/dist/hooks/product/useProducts.js +12 -11
  26. package/dist/hooks/product/useProducts.js.map +1 -1
  27. package/dist/hooks/product/useRecommendedProducts.js +11 -13
  28. package/dist/hooks/product/useRecommendedProducts.js.map +1 -1
  29. package/dist/hooks/shop/useRecommendedShops.js +11 -13
  30. package/dist/hooks/shop/useRecommendedShops.js.map +1 -1
  31. package/dist/hooks/shop/useShop.js +12 -11
  32. package/dist/hooks/shop/useShop.js.map +1 -1
  33. package/dist/hooks/user/useBuyerAttributes.js +8 -10
  34. package/dist/hooks/user/useBuyerAttributes.js.map +1 -1
  35. package/dist/hooks/user/useCurrentUser.js +7 -9
  36. package/dist/hooks/user/useCurrentUser.js.map +1 -1
  37. package/dist/hooks/user/useFollowedShops.js +11 -14
  38. package/dist/hooks/user/useFollowedShops.js.map +1 -1
  39. package/dist/hooks/user/useOrders.js +7 -9
  40. package/dist/hooks/user/useOrders.js.map +1 -1
  41. package/dist/hooks/user/useRecentProducts.js +11 -13
  42. package/dist/hooks/user/useRecentProducts.js.map +1 -1
  43. package/dist/hooks/user/useRecentShops.js +10 -13
  44. package/dist/hooks/user/useRecentShops.js.map +1 -1
  45. package/dist/hooks/user/useSavedProducts.js +10 -13
  46. package/dist/hooks/user/useSavedProducts.js.map +1 -1
  47. package/dist/hooks/util/useImagePicker.js +13 -6
  48. package/dist/hooks/util/useImagePicker.js.map +1 -1
  49. package/dist/internal/reactQuery/MinisQueryProvider.js +11 -0
  50. package/dist/internal/reactQuery/MinisQueryProvider.js.map +1 -0
  51. package/dist/internal/reactQuery/queryClient.js +33 -0
  52. package/dist/internal/reactQuery/queryClient.js.map +1 -0
  53. package/dist/internal/reactQuery/useShopActionInfiniteQuery.js +52 -0
  54. package/dist/internal/reactQuery/useShopActionInfiniteQuery.js.map +1 -0
  55. package/dist/internal/reactQuery/useShopActionQuery.js +37 -0
  56. package/dist/internal/reactQuery/useShopActionQuery.js.map +1 -0
  57. package/dist/internal/utils/resizeImage.js +61 -0
  58. package/dist/internal/utils/resizeImage.js.map +1 -0
  59. package/dist/providers/ImagePickerProvider.js +123 -102
  60. package/dist/providers/ImagePickerProvider.js.map +1 -1
  61. 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
  62. 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
  63. 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
  64. 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
  65. 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
  66. 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
  67. 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
  68. 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
  69. 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
  70. 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
  71. 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
  72. 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
  73. 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
  74. 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
  75. 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
  76. 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
  77. 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
  78. 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
  79. 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
  80. 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
  81. 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
  82. 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
  83. 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
  84. 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
  85. 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
  86. 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
  87. 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
  88. 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
  89. 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
  90. 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
  91. 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
  92. 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
  93. 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
  94. 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
  95. 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
  96. 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
  97. 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
  98. 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
  99. 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
  100. 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
  101. 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
  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/suspense.js.map +1 -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/useBaseQuery.js +64 -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/useBaseQuery.js.map +1 -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/useInfiniteQuery.js +13 -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/useInfiniteQuery.js.map +1 -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/useQuery.js +9 -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/useQuery.js.map +1 -0
  109. package/dist/shop-minis-react/node_modules/.pnpm/querystringify@2.2.0/node_modules/querystringify/index.js +1 -1
  110. package/dist/shop-minis-react/node_modules/.pnpm/video.js@8.23.3/node_modules/video.js/dist/video.es.js +1 -1
  111. package/package.json +2 -7
  112. package/src/components/MinisContainer.tsx +6 -3
  113. package/src/hooks/content/useContent.ts +6 -17
  114. package/src/hooks/product/useCuratedProducts.ts +4 -6
  115. package/src/hooks/product/usePopularProducts.ts +4 -6
  116. package/src/hooks/product/useProduct.ts +6 -17
  117. package/src/hooks/product/useProductList.ts +4 -19
  118. package/src/hooks/product/useProductLists.ts +4 -6
  119. package/src/hooks/product/useProductMedia.ts +6 -17
  120. package/src/hooks/product/useProductSearch.ts +19 -15
  121. package/src/hooks/product/useProductVariants.ts +5 -13
  122. package/src/hooks/product/useProducts.ts +8 -12
  123. package/src/hooks/product/useRecommendedProducts.ts +4 -6
  124. package/src/hooks/shop/useRecommendedShops.ts +4 -6
  125. package/src/hooks/shop/useShop.ts +8 -12
  126. package/src/hooks/user/useBuyerAttributes.ts +4 -6
  127. package/src/hooks/user/useCurrentUser.ts +4 -6
  128. package/src/hooks/user/useFollowedShops.ts +5 -13
  129. package/src/hooks/user/useOrders.ts +4 -6
  130. package/src/hooks/user/useRecentProducts.ts +4 -6
  131. package/src/hooks/user/useRecentShops.ts +5 -13
  132. package/src/hooks/user/useSavedProducts.ts +5 -13
  133. package/src/hooks/util/useImagePicker.test.tsx +193 -0
  134. package/src/hooks/util/useImagePicker.ts +24 -5
  135. package/src/internal/reactQuery/MinisQueryProvider.test.tsx +38 -0
  136. package/src/internal/reactQuery/MinisQueryProvider.tsx +16 -0
  137. package/src/internal/reactQuery/index.ts +8 -0
  138. package/src/internal/reactQuery/queryClient.test.tsx +91 -0
  139. package/src/internal/reactQuery/queryClient.ts +43 -0
  140. package/src/internal/reactQuery/useShopActionInfiniteQuery.test.tsx +357 -0
  141. package/src/internal/reactQuery/useShopActionInfiniteQuery.ts +129 -0
  142. package/src/internal/reactQuery/useShopActionQuery.test.tsx +184 -0
  143. package/src/internal/reactQuery/useShopActionQuery.ts +74 -0
  144. package/src/internal/utils/resizeImage.test.ts +314 -0
  145. package/src/internal/utils/resizeImage.ts +108 -0
  146. package/src/providers/ImagePickerProvider.test.tsx +32 -1
  147. package/src/providers/ImagePickerProvider.tsx +108 -65
  148. package/dist/internal/useShopActionsDataFetching.js +0 -79
  149. package/dist/internal/useShopActionsDataFetching.js.map +0 -1
  150. package/dist/internal/useShopActionsPaginatedDataFetching.js +0 -96
  151. package/dist/internal/useShopActionsPaginatedDataFetching.js.map +0 -1
  152. package/src/hooks/product/useProductSearch.test.ts +0 -470
  153. package/src/internal/useShopActionsDataFetching.test.ts +0 -465
  154. package/src/internal/useShopActionsDataFetching.ts +0 -150
  155. package/src/internal/useShopActionsPaginatedDataFetching.ts +0 -188
  156. package/src/stories/Accordion.stories.tsx +0 -124
  157. package/src/stories/AddToCart.stories.tsx +0 -251
  158. package/src/stories/Alert.stories.tsx +0 -38
  159. package/src/stories/AlertDialog.stories.tsx +0 -48
  160. package/src/stories/Avatar.stories.tsx +0 -29
  161. package/src/stories/Badge.stories.tsx +0 -46
  162. package/src/stories/Button.stories.tsx +0 -81
  163. package/src/stories/Card.stories.tsx +0 -40
  164. package/src/stories/Checkbox.stories.tsx +0 -44
  165. package/src/stories/FavoriteButton.stories.tsx +0 -58
  166. package/src/stories/IconButton.stories.tsx +0 -68
  167. package/src/stories/ImageContentWrapper.stories.tsx +0 -65
  168. package/src/stories/Input.stories.tsx +0 -44
  169. package/src/stories/Label.stories.tsx +0 -19
  170. package/src/stories/List.stories.tsx +0 -64
  171. package/src/stories/MerchantCard.stories.tsx +0 -127
  172. package/src/stories/ProductCard.stories.tsx +0 -92
  173. package/src/stories/ProductLink.stories.tsx +0 -46
  174. package/src/stories/ProductVariantPrice.stories.tsx +0 -70
  175. package/src/stories/Progress.stories.tsx +0 -30
  176. package/src/stories/PullToRefreshList.stories.tsx +0 -122
  177. package/src/stories/QuantitySelector.stories.tsx +0 -78
  178. package/src/stories/RadioGroup.stories.tsx +0 -51
  179. package/src/stories/Search.stories.tsx +0 -37
  180. package/src/stories/Select.stories.tsx +0 -85
  181. package/src/stories/Skeleton.stories.tsx +0 -19
  182. package/src/stories/TextInput.stories.tsx +0 -26
  183. package/src/stories/Toaster.stories.tsx +0 -46
  184. package/src/stories/Touchable.stories.tsx +0 -40
  185. package/src/stories/VideoPlayer.stories.tsx +0 -129
@@ -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
+ })
@@ -0,0 +1,129 @@
1
+ import {useCallback, useMemo} from 'react'
2
+
3
+ import {ShopActionResult} from '@shopify/shop-minis-platform/actions'
4
+ import {useInfiniteQuery} from '@tanstack/react-query'
5
+
6
+ import {DataHookFetchPolicy} from '../../types'
7
+
8
+ import {useShopMinisQueryClient} from './queryClient'
9
+
10
+ /**
11
+ * Helper to use React Query with Shop Actions (paginated)
12
+ * Replaces useShopActionsPaginatedDataFetching
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * // Example: Fetching saved products
17
+ * const { getSavedProducts } = useShopActions()
18
+ * const { data, loading, error, hasNextPage, fetchMore, refetch } =
19
+ * useShopActionInfiniteQuery(
20
+ * ['savedProducts', { includeSensitive }], // Query key
21
+ * getSavedProducts, // Shop Action
22
+ * { includeSensitive }, // Params (excludes 'after')
23
+ * { skip: false } // Options
24
+ * )
25
+ * // data will be flattened array of products from all pages
26
+ * ```
27
+ */
28
+ export function useShopActionInfiniteQuery<
29
+ TData,
30
+ TParams extends {after?: string; fetchPolicy?: DataHookFetchPolicy},
31
+ >(
32
+ queryKey: unknown[],
33
+ action: (params: TParams) => Promise<
34
+ ShopActionResult<{
35
+ data: TData
36
+ pageInfo: {hasNextPage: boolean; endCursor: string | null}
37
+ }>
38
+ >,
39
+ params: Omit<TParams, 'after'>,
40
+ options?: {
41
+ skip?: boolean
42
+ }
43
+ ) {
44
+ const {skip = false} = options ?? {}
45
+
46
+ // Always use our SDK's QueryClient for isolation
47
+ const queryClient = useShopMinisQueryClient()
48
+
49
+ interface PageData {
50
+ data: TData
51
+ pageInfo: {hasNextPage: boolean; endCursor: string | null}
52
+ }
53
+
54
+ const {
55
+ data,
56
+ fetchNextPage,
57
+ hasNextPage,
58
+ isLoading,
59
+ error,
60
+ refetch: reactQueryRefetch,
61
+ } = useInfiniteQuery<
62
+ PageData,
63
+ Error,
64
+ {pages: PageData[]},
65
+ unknown[],
66
+ string | undefined
67
+ >(
68
+ {
69
+ queryKey,
70
+ queryFn: async ({pageParam}: {pageParam: string | undefined}) => {
71
+ const result = await action({
72
+ ...params,
73
+ after: pageParam,
74
+ } as TParams)
75
+
76
+ if (!result.ok) {
77
+ throw result.error
78
+ }
79
+
80
+ return result.data
81
+ },
82
+ getNextPageParam: (lastPage: PageData) =>
83
+ lastPage.pageInfo.hasNextPage ? lastPage.pageInfo.endCursor : undefined,
84
+ initialPageParam: undefined as string | undefined,
85
+ enabled: !skip,
86
+ // Caching disabled by default (handled by Apollo)
87
+ // fetchPolicy param is passed through to the action (Apollo layer)
88
+ },
89
+ queryClient
90
+ )
91
+
92
+ // Flatten paginated data
93
+ // For paginated queries, we expect TData to be an array type
94
+ // Each page.data is an array that we concatenate together
95
+ const flattenedData = useMemo(() => {
96
+ if (!data?.pages || data.pages.length === 0) return null
97
+
98
+ // If first page data is null/undefined, return null
99
+ const firstPageData = data.pages[0].data
100
+ if (firstPageData === null || firstPageData === undefined) return null
101
+
102
+ // If data is array type, flatten all pages
103
+ if (Array.isArray(firstPageData)) {
104
+ return data.pages.flatMap((page: PageData) => page.data as any) as TData
105
+ }
106
+
107
+ // If data is not an array, just return the first page's data
108
+ // (Though in practice, all Shop Minis paginated queries return arrays)
109
+ return firstPageData as TData
110
+ }, [data?.pages])
111
+
112
+ // Wrap React Query functions to match expected API
113
+ const fetchMore = useCallback(async () => {
114
+ await fetchNextPage()
115
+ }, [fetchNextPage])
116
+
117
+ const refetch = useCallback(async () => {
118
+ await reactQueryRefetch()
119
+ }, [reactQueryRefetch])
120
+
121
+ return {
122
+ data: flattenedData,
123
+ loading: isLoading,
124
+ error: error as Error | null,
125
+ hasNextPage: hasNextPage ?? false,
126
+ fetchMore,
127
+ refetch,
128
+ }
129
+ }
@@ -0,0 +1,184 @@
1
+ import React from 'react'
2
+
3
+ import {renderHook, waitFor} from '@testing-library/react'
4
+ import {describe, expect, it, vi} from 'vitest'
5
+
6
+ import {MinisQueryProvider} from './MinisQueryProvider'
7
+ import {useShopActionQuery} from './useShopActionQuery'
8
+
9
+ describe('useShopActionQuery', () => {
10
+ const wrapper = ({children}: {children: React.ReactNode}) => (
11
+ <MinisQueryProvider>{children}</MinisQueryProvider>
12
+ )
13
+
14
+ describe('Successful Data Fetching', () => {
15
+ it('fetches and returns data', async () => {
16
+ const mockAction = vi.fn().mockResolvedValue({
17
+ ok: true,
18
+ data: {data: {id: '1', name: 'Test Product'}},
19
+ })
20
+
21
+ const {result} = renderHook(
22
+ () => useShopActionQuery(['test-1'], mockAction, {}),
23
+ {wrapper}
24
+ )
25
+
26
+ await waitFor(() => {
27
+ expect(result.current.loading).toBe(false)
28
+ })
29
+
30
+ expect(result.current.data).toEqual({id: '1', name: 'Test Product'})
31
+ expect(result.current.error).toBeNull()
32
+ expect(mockAction).toHaveBeenCalledWith({})
33
+ })
34
+
35
+ it('returns null when data is undefined', async () => {
36
+ const mockAction = vi.fn().mockResolvedValue({
37
+ ok: true,
38
+ data: {data: undefined},
39
+ })
40
+
41
+ const {result} = renderHook(
42
+ () => useShopActionQuery(['test-undefined'], mockAction, {}),
43
+ {wrapper}
44
+ )
45
+
46
+ await waitFor(() => {
47
+ expect(result.current.loading).toBe(false)
48
+ })
49
+
50
+ expect(result.current.data).toBeNull()
51
+ })
52
+ })
53
+
54
+ describe('Error Handling', () => {
55
+ it('handles action errors (ok: false)', async () => {
56
+ const mockAction = vi.fn().mockRejectedValue(new Error('API Error'))
57
+
58
+ const {result} = renderHook(
59
+ () => useShopActionQuery(['test-action-error'], mockAction, {}),
60
+ {wrapper}
61
+ )
62
+
63
+ // Wait for loading to complete (after retries)
64
+ await waitFor(
65
+ () => {
66
+ expect(result.current.loading).toBe(false)
67
+ },
68
+ {timeout: 3000}
69
+ )
70
+
71
+ expect(result.current.data).toBeNull()
72
+ expect(result.current.error).toBeInstanceOf(Error)
73
+ expect(result.current.error?.message).toBe('API Error')
74
+ })
75
+ })
76
+
77
+ describe('Skip Parameter', () => {
78
+ it('does not fetch when skip is true', async () => {
79
+ const mockAction = vi.fn()
80
+
81
+ renderHook(
82
+ () => useShopActionQuery(['test-skip'], mockAction, {}, {skip: true}),
83
+ {wrapper}
84
+ )
85
+
86
+ // Wait a bit to ensure no fetch happens
87
+ await new Promise(resolve => setTimeout(resolve, 100))
88
+
89
+ expect(mockAction).not.toHaveBeenCalled()
90
+ })
91
+
92
+ it('fetches when skip is false', async () => {
93
+ const mockAction = vi.fn().mockResolvedValue({
94
+ ok: true,
95
+ data: {data: 'skip-false-data'},
96
+ })
97
+
98
+ const {result} = renderHook(
99
+ () =>
100
+ useShopActionQuery(
101
+ ['test-noskip-query'],
102
+ mockAction,
103
+ {},
104
+ {skip: false}
105
+ ),
106
+ {wrapper}
107
+ )
108
+
109
+ await waitFor(() => {
110
+ expect(result.current.data).toBe('skip-false-data')
111
+ })
112
+
113
+ expect(mockAction).toHaveBeenCalled()
114
+ })
115
+ })
116
+
117
+ describe('API Contract', () => {
118
+ it('returns expected shape', async () => {
119
+ const mockAction = vi.fn().mockResolvedValue({
120
+ ok: true,
121
+ data: {data: 'contract-test'},
122
+ })
123
+
124
+ const {result} = renderHook(
125
+ () => useShopActionQuery(['test-contract'], mockAction, {}),
126
+ {wrapper}
127
+ )
128
+
129
+ await waitFor(() => {
130
+ expect(result.current.loading).toBe(false)
131
+ })
132
+
133
+ // Verify all expected properties exist
134
+ expect(result.current).toHaveProperty('data')
135
+ expect(result.current).toHaveProperty('loading')
136
+ expect(result.current).toHaveProperty('error')
137
+ expect(result.current).toHaveProperty('refetch')
138
+
139
+ // Verify types
140
+ expect(typeof result.current.loading).toBe('boolean')
141
+ expect(typeof result.current.refetch).toBe('function')
142
+ })
143
+ })
144
+
145
+ describe('fetchPolicy Parameter', () => {
146
+ it('passes fetchPolicy to action', async () => {
147
+ const mockAction = vi.fn().mockResolvedValue({
148
+ ok: true,
149
+ data: {data: 'test'},
150
+ })
151
+
152
+ renderHook(
153
+ () =>
154
+ useShopActionQuery(['test-network-only'], mockAction, {
155
+ fetchPolicy: 'network-only',
156
+ }),
157
+ {wrapper}
158
+ )
159
+
160
+ await waitFor(() => {
161
+ expect(mockAction).toHaveBeenCalledWith({fetchPolicy: 'network-only'})
162
+ })
163
+ })
164
+
165
+ it('passes cache-first fetchPolicy to action', async () => {
166
+ const mockAction = vi.fn().mockResolvedValue({
167
+ ok: true,
168
+ data: {data: 'test'},
169
+ })
170
+
171
+ renderHook(
172
+ () =>
173
+ useShopActionQuery(['test-cache-first'], mockAction, {
174
+ fetchPolicy: 'cache-first',
175
+ }),
176
+ {wrapper}
177
+ )
178
+
179
+ await waitFor(() => {
180
+ expect(mockAction).toHaveBeenCalledWith({fetchPolicy: 'cache-first'})
181
+ })
182
+ })
183
+ })
184
+ })