@kasuvia/sdk 0.0.8 → 0.0.9

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 (176) hide show
  1. package/README.md +42 -15
  2. package/dist/AddressAutocomplete-CjcqPElD.d.mts +46 -0
  3. package/dist/AddressAutocomplete-CvJHuRkf.d.ts +46 -0
  4. package/dist/components/index.d.mts +497 -0
  5. package/dist/components/index.d.ts +497 -0
  6. package/dist/components/index.js +4217 -0
  7. package/dist/components/index.js.map +1 -0
  8. package/dist/components/index.mjs +4194 -0
  9. package/dist/components/index.mjs.map +1 -0
  10. package/dist/config/server.d.mts +26 -0
  11. package/dist/config/server.d.ts +26 -0
  12. package/dist/config/server.js +38 -0
  13. package/dist/config/server.js.map +1 -0
  14. package/dist/config/server.mjs +13 -0
  15. package/dist/config/server.mjs.map +1 -0
  16. package/dist/hooks/index.d.mts +393 -160
  17. package/dist/hooks/index.d.ts +393 -160
  18. package/dist/hooks/index.js +2792 -2137
  19. package/dist/hooks/index.js.map +1 -1
  20. package/dist/hooks/index.mjs +2734 -2121
  21. package/dist/hooks/index.mjs.map +1 -1
  22. package/dist/index-CNW_LINy.d.mts +578 -0
  23. package/dist/index-CNW_LINy.d.ts +578 -0
  24. package/dist/{index-C1chUA3P.d.mts → index-F7N6UItS.d.mts} +1 -1
  25. package/dist/{index-C1chUA3P.d.ts → index-F7N6UItS.d.ts} +1 -1
  26. package/dist/index-NkI10J_E.d.mts +40 -0
  27. package/dist/index-NkI10J_E.d.ts +40 -0
  28. package/dist/index.d.mts +20437 -103
  29. package/dist/index.d.ts +20437 -103
  30. package/dist/index.js +2111 -1802
  31. package/dist/index.js.map +1 -1
  32. package/dist/index.mjs +2006 -1786
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/media.service-Cx7dQjzX.d.mts +1393 -0
  35. package/dist/media.service-mrBFJ2UQ.d.ts +1393 -0
  36. package/dist/services/index.d.mts +908 -2
  37. package/dist/services/index.d.ts +908 -2
  38. package/dist/services/index.js +1492 -1686
  39. package/dist/services/index.js.map +1 -1
  40. package/dist/services/index.mjs +1431 -1670
  41. package/dist/services/index.mjs.map +1 -1
  42. package/package.json +46 -6
  43. package/src/components/bespoke/BespokeAvailabilityBadge.tsx +32 -0
  44. package/src/components/bespoke/BespokeProgressTracker.tsx +58 -0
  45. package/src/components/bespoke/index.ts +3 -0
  46. package/src/components/booking/SlotPicker.tsx +97 -0
  47. package/src/components/booking/index.ts +1 -0
  48. package/src/components/cart/CartItem.tsx +81 -0
  49. package/src/components/cart/CartSidebar.tsx +115 -0
  50. package/src/components/cart/CartTypeMismatchBanner.tsx +55 -0
  51. package/src/components/cart/index.ts +3 -0
  52. package/src/components/index.ts +8 -0
  53. package/src/components/orders/DeliverableReviewer.tsx +139 -0
  54. package/src/components/orders/index.ts +1 -0
  55. package/src/components/product/ProductCard.tsx +350 -0
  56. package/src/components/product/ProductGrid.tsx +84 -0
  57. package/src/components/product/index.ts +2 -0
  58. package/src/components/shared/AddressAutocomplete.tsx +212 -0
  59. package/src/components/shared/AnalyticsScripts.tsx +56 -0
  60. package/src/components/shared/CookieConsentBanner.tsx +98 -0
  61. package/src/components/shared/ImageEditorModal.tsx +1066 -0
  62. package/src/components/shared/ImageViewerModal.tsx +598 -0
  63. package/src/components/shared/LucideReactIcon.tsx +52 -0
  64. package/src/components/shared/MapPinDrop.tsx +101 -0
  65. package/src/components/shared/MultiSelectPicker.tsx +193 -0
  66. package/src/components/shared/OfflineBanner.tsx +46 -0
  67. package/src/components/shared/SearchableInput.tsx +153 -0
  68. package/src/components/shared/SearchablePopover.tsx +166 -0
  69. package/src/components/shared/SmartImage.tsx +311 -0
  70. package/src/components/shared/TruncatedValue.tsx +27 -0
  71. package/src/components/shared/UnsavedChangesGuard.tsx +138 -0
  72. package/src/components/shared/index.ts +15 -0
  73. package/src/components/skeletons/CardSkeleton.tsx +26 -0
  74. package/src/components/skeletons/CartItemSkeleton.tsx +26 -0
  75. package/src/components/skeletons/OrderItemSkeleton.tsx +26 -0
  76. package/src/components/skeletons/ProductCardSkeleton.tsx +26 -0
  77. package/src/components/skeletons/TableSkeleton.tsx +42 -0
  78. package/src/components/skeletons/index.ts +5 -0
  79. package/src/config/index.ts +22 -4
  80. package/src/config/server.ts +45 -0
  81. package/src/context/sdk-context.tsx +5 -2
  82. package/src/generated/auth-openapi.ts +2935 -0
  83. package/src/generated/cart-openapi.ts +652 -0
  84. package/src/generated/cms-openapi.ts +1668 -0
  85. package/src/generated/crm-openapi.ts +986 -0
  86. package/src/generated/graphql.ts +126 -0
  87. package/src/generated/index.ts +11 -0
  88. package/src/generated/media-openapi.ts +613 -0
  89. package/src/generated/notification-openapi.ts +862 -0
  90. package/src/generated/payment-openapi.ts +2713 -0
  91. package/src/generated/store-openapi.ts +9510 -0
  92. package/src/graphql/operations/collections.graphql +32 -0
  93. package/src/graphql/operations/products.graphql +197 -0
  94. package/src/graphql/operations/wishlist.graphql +25 -0
  95. package/src/hooks/index.ts +9 -2
  96. package/src/hooks/use-auth.ts +163 -69
  97. package/src/hooks/use-bespoke-availability.ts +39 -22
  98. package/src/hooks/use-business-config.ts +51 -0
  99. package/src/hooks/use-cart.ts +310 -238
  100. package/src/hooks/use-cms.ts +107 -127
  101. package/src/hooks/use-collections.ts +35 -55
  102. package/src/hooks/use-consultations.ts +9 -136
  103. package/src/hooks/use-countries.ts +19 -28
  104. package/src/hooks/use-currencies.ts +8 -20
  105. package/src/hooks/use-customer-address.ts +64 -0
  106. package/src/hooks/use-feedback.ts +34 -0
  107. package/src/hooks/use-languages.ts +26 -39
  108. package/src/hooks/use-media-upload.ts +23 -0
  109. package/src/hooks/use-notification-stream.ts +88 -0
  110. package/src/hooks/use-notifications.ts +163 -0
  111. package/src/hooks/use-order-stream.ts +29 -29
  112. package/src/hooks/use-orders.ts +91 -73
  113. package/src/hooks/use-payment.ts +116 -84
  114. package/src/hooks/use-products.ts +65 -269
  115. package/src/hooks/use-push-notifications.ts +122 -0
  116. package/src/hooks/use-reviews.ts +175 -0
  117. package/src/hooks/use-service-booking.ts +109 -132
  118. package/src/hooks/use-states.ts +16 -30
  119. package/src/hooks/use-wishlist.ts +44 -69
  120. package/src/index.ts +15 -11
  121. package/src/services/auth.service.ts +422 -132
  122. package/src/services/bespoke.service.ts +69 -10
  123. package/src/services/cart.service.ts +155 -186
  124. package/src/services/cms.service.ts +41 -50
  125. package/src/services/collection.service.ts +25 -63
  126. package/src/services/config.service.ts +10 -21
  127. package/src/services/consultation.service.ts +36 -93
  128. package/src/services/feedback.service.ts +76 -0
  129. package/src/services/index.ts +7 -1
  130. package/src/services/legal.service.ts +62 -0
  131. package/src/services/logistics.service.ts +235 -53
  132. package/src/services/media.service.ts +115 -0
  133. package/src/services/notifications.service.ts +204 -20
  134. package/src/services/order.service.ts +498 -127
  135. package/src/services/payment.service.ts +149 -100
  136. package/src/services/product.service.ts +157 -814
  137. package/src/services/review.service.ts +211 -0
  138. package/src/services/service-booking.service.ts +198 -99
  139. package/src/services/wishlist.service.ts +27 -74
  140. package/src/types/auth/index.ts +161 -18
  141. package/src/types/business/index.ts +9 -1
  142. package/src/types/cms/index.ts +91 -6
  143. package/src/types/cms/legal.ts +18 -0
  144. package/src/types/consultations/index.ts +60 -68
  145. package/src/types/index.ts +5 -3
  146. package/src/types/media/index.ts +35 -3
  147. package/src/types/payments/index.ts +80 -16
  148. package/src/types/reviews/index.ts +116 -0
  149. package/src/types/services/index.ts +113 -38
  150. package/src/types/store/checkout.ts +53 -0
  151. package/src/types/store/index.ts +273 -52
  152. package/src/types/store/invoice.ts +31 -0
  153. package/src/types/store/shipping.ts +84 -3
  154. package/src/types/ui/index.ts +1 -1
  155. package/src/utils/checkout.ts +19 -3
  156. package/src/utils/cookieConsent.ts +34 -0
  157. package/src/utils/format.ts +408 -0
  158. package/src/utils/geolocation.ts +134 -0
  159. package/src/utils/graphql-url.ts +47 -0
  160. package/src/utils/guest-session.ts +59 -0
  161. package/src/utils/http-client.ts +147 -0
  162. package/src/utils/index.ts +11 -4
  163. package/src/utils/status-colors.ts +79 -0
  164. package/src/utils/token-refresh.ts +115 -0
  165. package/src/utils/validators.ts +13 -0
  166. package/src/utils/youtube.ts +18 -0
  167. package/dist/index-C5YO9gTl.d.mts +0 -507
  168. package/dist/index-C5YO9gTl.d.ts +0 -507
  169. package/dist/index-CefG7_xq.d.mts +0 -793
  170. package/dist/index-DeHCf3zI.d.ts +0 -793
  171. package/src/graphql/collections.graphql.ts +0 -48
  172. package/src/graphql/index.ts +0 -7
  173. package/src/graphql/products.graphql.ts +0 -860
  174. package/src/graphql/wishlist.graphql.ts +0 -34
  175. package/src/hooks/use-consultation-stream.ts +0 -142
  176. package/src/types/bespoke/index.ts +0 -39
@@ -1,311 +1,107 @@
1
1
  /**
2
2
  * Products Hooks
3
- *
4
- * React Query hooks for fetching products via Next.js API routes.
5
- * Client-side hooks fetch from API routes to avoid exposing backend URLs to browser.
3
+ *
4
+ * React Query hooks for fetching products via the real GraphQL storefront API.
5
+ *
6
+ * REAL BUG FIXED (2026-09-21), live-reproduced: every hook here used to fetch
7
+ * from a Next.js API route (`/api/products/popular`, `/api/products/featured`,
8
+ * etc.) — but those routes were deleted earlier this session as confirmed
9
+ * orphaned (an incomplete grep missed this file as a real consumer, the same
10
+ * mistake class already caught once for `/api/products/[slug]`). This broke
11
+ * every home-page rail (Featured/New/Popular/On Sale) and the "You may also
12
+ * crave" recommended section — all 404s. Fixed by calling the SDK's real
13
+ * GraphQL service functions directly (product.service.ts), resolving config
14
+ * from SdkContext the same way every other SDK hook does (see use-orders.ts).
15
+ * `useLatestProducts`/`useLowStockProducts`/`useStorefrontProducts`/
16
+ * `useProduct` are NOT restored — confirmed zero real UI consumers anywhere
17
+ * in kasuvia-storefront (ProductsListing.tsx and the product detail page
18
+ * both already call the SDK service functions directly, not through a hook).
6
19
  */
7
20
  import { useQuery } from '@tanstack/react-query'
8
21
  import type { ListParams } from '../types/store'
9
22
  import { getCacheTier } from '../utils/cache'
10
- import { logger } from '../utils'
23
+ import { useSdkContext, resolveHookArgs } from '../context/sdk-context'
24
+ import type { SdkConfig } from '../config'
25
+ import {
26
+ getPopularProducts,
27
+ getFeaturedProducts,
28
+ getNewProducts,
29
+ getOnSaleProducts,
30
+ getRecommendedProducts,
31
+ } from '../services/product.service'
11
32
 
12
33
  export const productKeys = {
13
34
  all: ["products"] as const,
14
35
  list: (params?: ListParams) => [...productKeys.all, "list", params] as const,
15
36
  detail: (slug: string) => [...productKeys.all, "detail", slug] as const,
16
37
  recommended: (params?: ListParams) => [...productKeys.all, "recommended", params] as const,
17
- latest: (params?: ListParams) => [...productKeys.all, "latest", params] as const,
18
38
  popular: (params?: ListParams) => [...productKeys.all, "popular", params] as const,
19
39
  featured: (params?: ListParams) => [...productKeys.all, "featured", params] as const,
20
40
  new: (params?: ListParams) => [...productKeys.all, "new", params] as const,
21
41
  onSale: (params?: ListParams) => [...productKeys.all, "onSale", params] as const,
22
- lowStock: (params?: ListParams) => [...productKeys.all, "lowStock", params] as const,
23
- }
24
-
25
- /**
26
- * Hook for fetching latest products.
27
- * Fetches from Next.js API route to avoid exposing backend URLs.
28
- */
29
- export function useLatestProducts(params?: ListParams) {
30
- try {
31
- const queryParams = new URLSearchParams()
32
- if (params?.page) queryParams.set('page', params.page.toString())
33
- if (params?.pageSize) queryParams.set('pageSize', params.pageSize.toString())
34
-
35
- return useQuery({
36
- queryKey: productKeys.latest(params),
37
- queryFn: async () => {
38
- const response = await fetch(`/api/products/latest?${queryParams.toString()}`)
39
- if (!response.ok) throw new Error('Failed to fetch latest products')
40
- return response.json()
41
- },
42
- // medium — latest products change infrequently
43
- ...getCacheTier('medium'),
44
- })
45
- } catch (error) {
46
- logger.warn('[useLatestProducts] Called outside QueryClientProvider, returning empty list')
47
- return {
48
- data: undefined,
49
- isLoading: false,
50
- error: null,
51
- isFetching: false,
52
- refetch: () => Promise.resolve({ data: undefined }),
53
- } as any
54
- }
55
42
  }
56
43
 
57
44
  /**
58
45
  * Hook for fetching popular products.
59
- * Fetches from Next.js API route to avoid exposing backend URLs.
60
46
  */
61
- export function usePopularProducts(params?: ListParams) {
62
- try {
63
- const queryParams = new URLSearchParams()
64
- if (params?.page) queryParams.set('page', params.page.toString())
65
- if (params?.pageSize) queryParams.set('pageSize', params.pageSize.toString())
66
-
67
- return useQuery({
68
- queryKey: productKeys.popular(params),
69
- queryFn: async () => {
70
- const response = await fetch(`/api/products/popular?${queryParams.toString()}`)
71
- if (!response.ok) throw new Error('Failed to fetch popular products')
72
- return response.json()
73
- },
74
- // medium — popular products change infrequently
75
- ...getCacheTier('medium'),
76
- })
77
- } catch (error) {
78
- logger.warn('[usePopularProducts] Called outside QueryClientProvider, returning empty list')
79
- return {
80
- data: undefined,
81
- isLoading: false,
82
- error: null,
83
- isFetching: false,
84
- refetch: () => Promise.resolve({ data: undefined }),
85
- } as any
86
- }
47
+ export function usePopularProducts(params?: ListParams, config?: SdkConfig) {
48
+ const ctx = useSdkContext()
49
+ const { config: cfg } = resolveHookArgs(config, undefined, ctx)
50
+ return useQuery({
51
+ queryKey: productKeys.popular(params),
52
+ queryFn: () => getPopularProducts(cfg, params),
53
+ ...getCacheTier('medium'),
54
+ })
87
55
  }
88
56
 
89
57
  /**
90
58
  * Hook for fetching featured products.
91
- * Fetches from Next.js API route to avoid exposing backend URLs.
92
59
  */
93
- export function useFeaturedProducts(params?: ListParams) {
94
- try {
95
- const queryParams = new URLSearchParams()
96
- if (params?.page) queryParams.set('page', params.page.toString())
97
- if (params?.pageSize) queryParams.set('pageSize', params.pageSize.toString())
98
-
99
- return useQuery({
100
- queryKey: productKeys.featured(params),
101
- queryFn: async () => {
102
- const response = await fetch(`/api/products/featured?${queryParams.toString()}`)
103
- if (!response.ok) throw new Error('Failed to fetch featured products')
104
- return response.json()
105
- },
106
- // medium — featured products change infrequently
107
- ...getCacheTier('medium'),
108
- })
109
- } catch (error) {
110
- logger.warn('[useFeaturedProducts] Called outside QueryClientProvider, returning empty list')
111
- return {
112
- data: undefined,
113
- isLoading: false,
114
- error: null,
115
- isFetching: false,
116
- refetch: () => Promise.resolve({ data: undefined }),
117
- } as any
118
- }
60
+ export function useFeaturedProducts(params?: ListParams, config?: SdkConfig) {
61
+ const ctx = useSdkContext()
62
+ const { config: cfg } = resolveHookArgs(config, undefined, ctx)
63
+ return useQuery({
64
+ queryKey: productKeys.featured(params),
65
+ queryFn: () => getFeaturedProducts(cfg, params),
66
+ ...getCacheTier('medium'),
67
+ })
119
68
  }
120
69
 
121
70
  /**
122
71
  * Hook for fetching new products.
123
- * Fetches from Next.js API route to avoid exposing backend URLs.
124
72
  */
125
- export function useNewProducts(params?: ListParams) {
126
- try {
127
- const queryParams = new URLSearchParams()
128
- if (params?.page) queryParams.set('page', params.page.toString())
129
- if (params?.pageSize) queryParams.set('pageSize', params.pageSize.toString())
130
-
131
- return useQuery({
132
- queryKey: productKeys.new(params),
133
- queryFn: async () => {
134
- const response = await fetch(`/api/products/new?${queryParams.toString()}`)
135
- if (!response.ok) throw new Error('Failed to fetch new products')
136
- return response.json()
137
- },
138
- // medium — new products change infrequently
139
- ...getCacheTier('medium'),
140
- })
141
- } catch (error) {
142
- logger.warn('[useNewProducts] Called outside QueryClientProvider, returning empty list')
143
- return {
144
- data: undefined,
145
- isLoading: false,
146
- error: null,
147
- isFetching: false,
148
- refetch: () => Promise.resolve({ data: undefined }),
149
- } as any
150
- }
73
+ export function useNewProducts(params?: ListParams, config?: SdkConfig) {
74
+ const ctx = useSdkContext()
75
+ const { config: cfg } = resolveHookArgs(config, undefined, ctx)
76
+ return useQuery({
77
+ queryKey: productKeys.new(params),
78
+ queryFn: () => getNewProducts(cfg, params),
79
+ ...getCacheTier('medium'),
80
+ })
151
81
  }
152
82
 
153
83
  /**
154
84
  * Hook for fetching products currently on sale.
155
- * Fetches from Next.js API route to avoid exposing backend URLs.
156
- */
157
- export function useOnSaleProducts(params?: ListParams) {
158
- try {
159
- const queryParams = new URLSearchParams()
160
- if (params?.page) queryParams.set('page', params.page.toString())
161
- if (params?.pageSize) queryParams.set('pageSize', params.pageSize.toString())
162
-
163
- return useQuery({
164
- queryKey: productKeys.onSale(params),
165
- queryFn: async () => {
166
- const response = await fetch(`/api/products/on-sale?${queryParams.toString()}`)
167
- if (!response.ok) throw new Error('Failed to fetch on-sale products')
168
- return response.json()
169
- },
170
- // medium — sale products change infrequently
171
- ...getCacheTier('medium'),
172
- })
173
- } catch (error) {
174
- logger.warn('[useOnSaleProducts] Called outside QueryClientProvider, returning empty list')
175
- return {
176
- data: undefined,
177
- isLoading: false,
178
- error: null,
179
- isFetching: false,
180
- refetch: () => Promise.resolve({ data: undefined }),
181
- } as any
182
- }
183
- }
184
-
185
- /**
186
- * Hook for fetching products with low stock.
187
- * Fetches from Next.js API route to avoid exposing backend URLs.
188
- */
189
- export function useLowStockProducts(params?: ListParams) {
190
- try {
191
- const queryParams = new URLSearchParams()
192
- if (params?.page) queryParams.set('page', params.page.toString())
193
- if (params?.pageSize) queryParams.set('pageSize', params.pageSize.toString())
194
-
195
- return useQuery({
196
- queryKey: productKeys.lowStock(params),
197
- queryFn: async () => {
198
- const response = await fetch(`/api/products/low-stock?${queryParams.toString()}`)
199
- if (!response.ok) throw new Error('Failed to fetch low-stock products')
200
- return response.json()
201
- },
202
- // low — low stock status changes frequently
203
- ...getCacheTier('low'),
204
- })
205
- } catch (error) {
206
- logger.warn('[useLowStockProducts] Called outside QueryClientProvider, returning empty list')
207
- return {
208
- data: undefined,
209
- isLoading: false,
210
- error: null,
211
- isFetching: false,
212
- refetch: () => Promise.resolve({ data: undefined }),
213
- } as any
214
- }
215
- }
216
-
217
- /**
218
- * Hook for fetching all active storefront products.
219
- * Fetches from Next.js API route to avoid exposing backend URLs.
220
- */
221
- export function useStorefrontProducts(params?: ListParams) {
222
- try {
223
- const queryParams = new URLSearchParams()
224
- if (params?.page) queryParams.set('page', params.page.toString())
225
- if (params?.pageSize) queryParams.set('pageSize', params.pageSize.toString())
226
- if (params?.collectionSlug) queryParams.set('collectionSlug', params.collectionSlug)
227
- if (params?.search) queryParams.set('search', params.search)
228
-
229
- return useQuery({
230
- queryKey: productKeys.list(params),
231
- queryFn: async () => {
232
- const response = await fetch(`/api/products?${queryParams.toString()}`)
233
- if (!response.ok) throw new Error('Failed to fetch products')
234
- return response.json()
235
- },
236
- // medium — product catalog changes infrequently
237
- ...getCacheTier('medium'),
238
- })
239
- } catch (error) {
240
- logger.warn('[useStorefrontProducts] Called outside QueryClientProvider, returning empty list')
241
- return {
242
- data: undefined,
243
- isLoading: false,
244
- error: null,
245
- isFetching: false,
246
- refetch: () => Promise.resolve({ data: undefined }),
247
- } as any
248
- }
249
- }
250
-
251
- /**
252
- * Hook for fetching a single product by slug.
253
- * Fetches from Next.js API route to avoid exposing backend URLs.
254
85
  */
255
- export function useProduct(slug: string) {
256
- try {
257
- return useQuery({
258
- queryKey: productKeys.detail(slug),
259
- queryFn: async () => {
260
- const response = await fetch(`/api/products/${slug}`)
261
- if (!response.ok) throw new Error('Failed to fetch product')
262
- return response.json()
263
- },
264
- // medium — product details change infrequently
265
- ...getCacheTier('medium'),
266
- enabled: !!slug,
267
- })
268
- } catch (error) {
269
- logger.warn('[useProduct] Called outside QueryClientProvider, returning undefined')
270
- return {
271
- data: undefined,
272
- isLoading: false,
273
- error: null,
274
- isFetching: false,
275
- refetch: () => Promise.resolve({ data: undefined }),
276
- } as any
277
- }
86
+ export function useOnSaleProducts(params?: ListParams, config?: SdkConfig) {
87
+ const ctx = useSdkContext()
88
+ const { config: cfg } = resolveHookArgs(config, undefined, ctx)
89
+ return useQuery({
90
+ queryKey: productKeys.onSale(params),
91
+ queryFn: () => getOnSaleProducts(cfg, params),
92
+ ...getCacheTier('medium'),
93
+ })
278
94
  }
279
95
 
280
96
  /**
281
97
  * Hook for fetching recommended products.
282
- * Fetches from Next.js API route to avoid exposing backend URLs.
283
98
  */
284
- export function useRecommendedProducts(params?: ListParams) {
285
- try {
286
- const queryParams = new URLSearchParams()
287
- if (params?.page) queryParams.set('page', params.page.toString())
288
- if (params?.pageSize) queryParams.set('pageSize', params.pageSize.toString())
289
- if (params?.excludeProductId) queryParams.set('excludeProductId', params.excludeProductId)
290
-
291
- return useQuery({
292
- queryKey: productKeys.recommended(params),
293
- queryFn: async () => {
294
- const response = await fetch(`/api/products/recommended?${queryParams.toString()}`)
295
- if (!response.ok) throw new Error('Failed to fetch recommended products')
296
- return response.json()
297
- },
298
- // medium — recommendations change infrequently
299
- ...getCacheTier('medium'),
300
- })
301
- } catch (error) {
302
- logger.warn('[useRecommendedProducts] Called outside QueryClientProvider, returning empty list')
303
- return {
304
- data: undefined,
305
- isLoading: false,
306
- error: null,
307
- isFetching: false,
308
- refetch: () => Promise.resolve({ data: undefined }),
309
- } as any
310
- }
99
+ export function useRecommendedProducts(params?: ListParams & { excludeProductId?: string }, config?: SdkConfig) {
100
+ const ctx = useSdkContext()
101
+ const { config: cfg } = resolveHookArgs(config, undefined, ctx)
102
+ return useQuery({
103
+ queryKey: productKeys.recommended(params),
104
+ queryFn: () => getRecommendedProducts(cfg, params),
105
+ ...getCacheTier('medium'),
106
+ })
311
107
  }
@@ -0,0 +1,122 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useState } from 'react';
4
+ import { useVapidPublicKey, useSubscribeToPush, useUnsubscribeFromPush } from './use-notifications';
5
+ import type { SdkConfig } from '../config';
6
+ import { logger } from '../utils/logger';
7
+
8
+ /**
9
+ * Converts a base64url VAPID public key (as the backend returns it) into
10
+ * the raw Uint8Array PushManager.subscribe's applicationServerKey expects —
11
+ * the standard, well-known conversion every Web Push integration needs.
12
+ */
13
+ function urlBase64ToUint8Array(base64String: string): Uint8Array<ArrayBuffer> {
14
+ const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
15
+ const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
16
+ const rawData = atob(base64);
17
+ const outputArray = new Uint8Array(rawData.length);
18
+ for (let i = 0; i < rawData.length; i++) {
19
+ outputArray[i] = rawData.charCodeAt(i);
20
+ }
21
+ return outputArray;
22
+ }
23
+
24
+ export type PushPermissionState = 'unsupported' | 'default' | 'granted' | 'denied';
25
+
26
+ /**
27
+ * Orchestrates the full Web Push subscribe/unsubscribe flow: registers
28
+ * /sw.js (the storefront app's own service worker — see its doc comment),
29
+ * requests Notification permission, subscribes via PushManager with the
30
+ * real VAPID key from the backend, and POSTs the subscription to
31
+ * kasuvia-notification-system-go. Returns current state + trigger functions
32
+ * — no UI of its own, so any storefront can build its own "enable
33
+ * notifications" affordance around it.
34
+ */
35
+ export function usePushNotifications(config?: SdkConfig, accessToken?: string | null) {
36
+ const [permission, setPermission] = useState<PushPermissionState>('default');
37
+ const [isSubscribed, setIsSubscribed] = useState(false);
38
+ const [isBusy, setIsBusy] = useState(false);
39
+
40
+ const { data: vapid } = useVapidPublicKey(config);
41
+ const subscribeMutation = useSubscribeToPush(config, accessToken);
42
+ const unsubscribeMutation = useUnsubscribeFromPush(config, accessToken);
43
+
44
+ useEffect(() => {
45
+ if (typeof window === 'undefined' || !('Notification' in window) || !('serviceWorker' in navigator) || !('PushManager' in window)) {
46
+ setPermission('unsupported');
47
+ return;
48
+ }
49
+ setPermission(Notification.permission as PushPermissionState);
50
+
51
+ navigator.serviceWorker.getRegistration('/sw.js').then(async (reg) => {
52
+ if (!reg) return;
53
+ const sub = await reg.pushManager.getSubscription();
54
+ setIsSubscribed(!!sub);
55
+ }).catch(() => {
56
+ // No registration yet — not subscribed.
57
+ });
58
+ }, []);
59
+
60
+ const subscribe = useCallback(async () => {
61
+ if (permission === 'unsupported') {
62
+ throw new Error('Push notifications are not supported in this browser.');
63
+ }
64
+ if (!vapid?.public_key) {
65
+ throw new Error('Notification service is not ready yet. Please try again in a moment.');
66
+ }
67
+ setIsBusy(true);
68
+ try {
69
+ const perm = await Notification.requestPermission();
70
+ setPermission(perm as PushPermissionState);
71
+ if (perm !== 'granted') {
72
+ throw new Error('Notification permission was not granted.');
73
+ }
74
+
75
+ const registration = await navigator.serviceWorker.register('/sw.js');
76
+ await navigator.serviceWorker.ready;
77
+
78
+ const existing = await registration.pushManager.getSubscription();
79
+ const pushSubscription = existing ?? await registration.pushManager.subscribe({
80
+ userVisibleOnly: true,
81
+ applicationServerKey: urlBase64ToUint8Array(vapid.public_key),
82
+ });
83
+
84
+ const json = pushSubscription.toJSON();
85
+ if (!json.endpoint || !json.keys?.p256dh || !json.keys?.auth) {
86
+ throw new Error('Browser returned an incomplete push subscription.');
87
+ }
88
+
89
+ await subscribeMutation.mutateAsync({
90
+ subscription: {
91
+ endpoint: json.endpoint,
92
+ keys: { p256dh: json.keys.p256dh, auth: json.keys.auth },
93
+ },
94
+ user_agent: navigator.userAgent,
95
+ });
96
+
97
+ setIsSubscribed(true);
98
+ } finally {
99
+ setIsBusy(false);
100
+ }
101
+ }, [permission, vapid, subscribeMutation]);
102
+
103
+ const unsubscribe = useCallback(async () => {
104
+ setIsBusy(true);
105
+ try {
106
+ const registration = await navigator.serviceWorker.getRegistration('/sw.js');
107
+ const sub = await registration?.pushManager.getSubscription();
108
+ if (sub) {
109
+ await unsubscribeMutation.mutateAsync(sub.endpoint);
110
+ await sub.unsubscribe();
111
+ }
112
+ setIsSubscribed(false);
113
+ } catch (error) {
114
+ logger.error('[usePushNotifications] Failed to unsubscribe:', error);
115
+ throw error;
116
+ } finally {
117
+ setIsBusy(false);
118
+ }
119
+ }, [unsubscribeMutation]);
120
+
121
+ return { permission, isSubscribed, isBusy, subscribe, unsubscribe };
122
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * React Query hooks for Reviews and Comments
3
+ */
4
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
5
+ import * as ReviewService from '../services/review.service'
6
+ import type { SdkConfig } from '../config'
7
+ import type {
8
+ ReviewCreate, ReviewUpdate, CommentCreate, CommentUpdate,
9
+ MerchantReplyCreate, ReviewModerationAction, CommentModerationAction
10
+ } from '../types/reviews'
11
+ import { useSdkContext, resolveHookArgs } from '../context/sdk-context'
12
+
13
+ /**
14
+ * Hook to create a new review
15
+ */
16
+ export function useCreateReview(config?: SdkConfig) {
17
+ const ctx = useSdkContext()
18
+ const queryClient = useQueryClient()
19
+ const { config: cfg } = resolveHookArgs(config, undefined, ctx)
20
+ return useMutation({
21
+ mutationFn: (reviewData: ReviewCreate) => ReviewService.createReview(reviewData, cfg),
22
+ onSuccess: () => {
23
+ queryClient.invalidateQueries({ queryKey: ['product-reviews'] })
24
+ },
25
+ })
26
+ }
27
+
28
+ /**
29
+ * Hook to update an existing review
30
+ */
31
+ export function useUpdateReview(config?: SdkConfig) {
32
+ const ctx = useSdkContext()
33
+ const queryClient = useQueryClient()
34
+ const { config: cfg } = resolveHookArgs(config, undefined, ctx)
35
+ return useMutation({
36
+ mutationFn: ({ reviewId, reviewData }: { reviewId: string; reviewData: ReviewUpdate }) =>
37
+ ReviewService.updateReview(reviewId, reviewData, cfg),
38
+ onSuccess: () => {
39
+ queryClient.invalidateQueries({ queryKey: ['product-reviews'] })
40
+ },
41
+ })
42
+ }
43
+
44
+ /**
45
+ * Hook to get product reviews
46
+ */
47
+ export function useProductReviews(productId: string, params?: { page?: number; page_size?: number }, config?: SdkConfig) {
48
+ const ctx = useSdkContext()
49
+ const { config: cfg } = resolveHookArgs(config, undefined, ctx)
50
+ return useQuery({
51
+ queryKey: ['product-reviews', productId, params],
52
+ queryFn: () => ReviewService.getProductReviews(productId, params, cfg),
53
+ enabled: !!productId,
54
+ })
55
+ }
56
+
57
+ /**
58
+ * Hook to create a new comment
59
+ */
60
+ export function useCreateComment(config?: SdkConfig) {
61
+ const ctx = useSdkContext()
62
+ const queryClient = useQueryClient()
63
+ const { config: cfg } = resolveHookArgs(config, undefined, ctx)
64
+ return useMutation({
65
+ mutationFn: (commentData: CommentCreate) => ReviewService.createComment(commentData, cfg),
66
+ onSuccess: () => {
67
+ queryClient.invalidateQueries({ queryKey: ['product-comments'] })
68
+ },
69
+ })
70
+ }
71
+
72
+ /**
73
+ * Hook to update an existing comment
74
+ */
75
+ export function useUpdateComment(config?: SdkConfig) {
76
+ const ctx = useSdkContext()
77
+ const queryClient = useQueryClient()
78
+ const { config: cfg } = resolveHookArgs(config, undefined, ctx)
79
+ return useMutation({
80
+ mutationFn: ({ commentId, commentData }: { commentId: string; commentData: CommentUpdate }) =>
81
+ ReviewService.updateComment(commentId, commentData, cfg),
82
+ onSuccess: () => {
83
+ queryClient.invalidateQueries({ queryKey: ['product-comments'] })
84
+ },
85
+ })
86
+ }
87
+
88
+ /**
89
+ * Hook to get product comments
90
+ */
91
+ export function useProductComments(productId: string, params?: { page?: number; page_size?: number }, config?: SdkConfig) {
92
+ const ctx = useSdkContext()
93
+ const { config: cfg } = resolveHookArgs(config, undefined, ctx)
94
+ return useQuery({
95
+ queryKey: ['product-comments', productId, params],
96
+ queryFn: () => ReviewService.getProductComments(productId, params, cfg),
97
+ enabled: !!productId,
98
+ })
99
+ }
100
+
101
+ /**
102
+ * Hook to get pending reviews (merchant/staff only)
103
+ */
104
+ export function usePendingReviews(config?: SdkConfig) {
105
+ const ctx = useSdkContext()
106
+ const { config: cfg } = resolveHookArgs(config, undefined, ctx)
107
+ return useQuery({
108
+ queryKey: ['pending-reviews'],
109
+ queryFn: () => ReviewService.getPendingReviews(cfg),
110
+ refetchInterval: 30000,
111
+ })
112
+ }
113
+
114
+ /**
115
+ * Hook to get pending comments (merchant/staff only)
116
+ */
117
+ export function usePendingComments(config?: SdkConfig) {
118
+ const ctx = useSdkContext()
119
+ const { config: cfg } = resolveHookArgs(config, undefined, ctx)
120
+ return useQuery({
121
+ queryKey: ['pending-comments'],
122
+ queryFn: () => ReviewService.getPendingComments(cfg),
123
+ refetchInterval: 30000,
124
+ })
125
+ }
126
+
127
+ /**
128
+ * Hook to moderate a review (merchant/staff only)
129
+ */
130
+ export function useModerateReview(config?: SdkConfig) {
131
+ const ctx = useSdkContext()
132
+ const queryClient = useQueryClient()
133
+ const { config: cfg } = resolveHookArgs(config, undefined, ctx)
134
+ return useMutation({
135
+ mutationFn: ({ reviewId, action }: { reviewId: string; action: ReviewModerationAction }) =>
136
+ ReviewService.moderateReview(reviewId, action, cfg),
137
+ onSuccess: () => {
138
+ queryClient.invalidateQueries({ queryKey: ['pending-reviews'] })
139
+ queryClient.invalidateQueries({ queryKey: ['product-reviews'] })
140
+ },
141
+ })
142
+ }
143
+
144
+ /**
145
+ * Hook to moderate a comment (merchant/staff only)
146
+ */
147
+ export function useModerateComment(config?: SdkConfig) {
148
+ const ctx = useSdkContext()
149
+ const queryClient = useQueryClient()
150
+ const { config: cfg } = resolveHookArgs(config, undefined, ctx)
151
+ return useMutation({
152
+ mutationFn: ({ commentId, action }: { commentId: string; action: CommentModerationAction }) =>
153
+ ReviewService.moderateComment(commentId, action, cfg),
154
+ onSuccess: () => {
155
+ queryClient.invalidateQueries({ queryKey: ['pending-comments'] })
156
+ queryClient.invalidateQueries({ queryKey: ['product-comments'] })
157
+ },
158
+ })
159
+ }
160
+
161
+ /**
162
+ * Hook to post a merchant reply to a review (merchant/staff only)
163
+ */
164
+ export function useMerchantReplyToReview(config?: SdkConfig) {
165
+ const ctx = useSdkContext()
166
+ const queryClient = useQueryClient()
167
+ const { config: cfg } = resolveHookArgs(config, undefined, ctx)
168
+ return useMutation({
169
+ mutationFn: ({ reviewId, replyData }: { reviewId: string; replyData: MerchantReplyCreate }) =>
170
+ ReviewService.merchantReplyToReview(reviewId, replyData, cfg),
171
+ onSuccess: () => {
172
+ queryClient.invalidateQueries({ queryKey: ['product-reviews'] })
173
+ },
174
+ })
175
+ }