@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
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Customer Saved-Address Book Hooks
3
+ */
4
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
5
+ import {
6
+ getCustomerAddresses,
7
+ createCustomerAddress,
8
+ updateCustomerAddress,
9
+ deleteCustomerAddress,
10
+ } from '../services/auth.service'
11
+ import type { CreateCustomerAddressPayload, UpdateCustomerAddressPayload } from '../types/auth'
12
+ import type { SdkConfig } from '../config'
13
+ import { useSdkContext, resolveHookArgs } from '../context/sdk-context'
14
+
15
+ const ADDRESSES_QUERY_KEY = 'customer-addresses'
16
+
17
+ export function useCustomerAddresses(config?: SdkConfig, accessToken?: string | null) {
18
+ const ctx = useSdkContext()
19
+ const { config: cfg, accessToken: token } = resolveHookArgs(config, accessToken, ctx)
20
+ return useQuery({
21
+ queryKey: [ADDRESSES_QUERY_KEY, token],
22
+ queryFn: () => token ? getCustomerAddresses(cfg, token) : Promise.reject(new Error('No access token available')),
23
+ enabled: !!token,
24
+ })
25
+ }
26
+
27
+ export function useCreateCustomerAddress(config?: SdkConfig, accessToken?: string | null) {
28
+ const ctx = useSdkContext()
29
+ const { config: cfg, accessToken: token } = resolveHookArgs(config, accessToken, ctx)
30
+ const queryClient = useQueryClient()
31
+ return useMutation({
32
+ mutationFn: (payload: CreateCustomerAddressPayload) =>
33
+ token ? createCustomerAddress(cfg, token, payload) : Promise.reject(new Error('Please sign in to save an address.')),
34
+ onSuccess: () => {
35
+ queryClient.invalidateQueries({ queryKey: [ADDRESSES_QUERY_KEY] })
36
+ },
37
+ })
38
+ }
39
+
40
+ export function useUpdateCustomerAddress(config?: SdkConfig, accessToken?: string | null) {
41
+ const ctx = useSdkContext()
42
+ const { config: cfg, accessToken: token } = resolveHookArgs(config, accessToken, ctx)
43
+ const queryClient = useQueryClient()
44
+ return useMutation({
45
+ mutationFn: ({ addressId, payload }: { addressId: string; payload: UpdateCustomerAddressPayload }) =>
46
+ token ? updateCustomerAddress(cfg, token, addressId, payload) : Promise.reject(new Error('Please sign in to update this address.')),
47
+ onSuccess: () => {
48
+ queryClient.invalidateQueries({ queryKey: [ADDRESSES_QUERY_KEY] })
49
+ },
50
+ })
51
+ }
52
+
53
+ export function useDeleteCustomerAddress(config?: SdkConfig, accessToken?: string | null) {
54
+ const ctx = useSdkContext()
55
+ const { config: cfg, accessToken: token } = resolveHookArgs(config, accessToken, ctx)
56
+ const queryClient = useQueryClient()
57
+ return useMutation({
58
+ mutationFn: (addressId: string) =>
59
+ token ? deleteCustomerAddress(cfg, token, addressId) : Promise.reject(new Error('Please sign in to remove this address.')),
60
+ onSuccess: () => {
61
+ queryClient.invalidateQueries({ queryKey: [ADDRESSES_QUERY_KEY] })
62
+ },
63
+ })
64
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Feedback Hooks
3
+ */
4
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
5
+ import { createFeedback, getMyFeedback, type CreateFeedbackParams } from '../services/feedback.service'
6
+ import type { SdkConfig } from '../config'
7
+ import { useSdkContext, resolveHookArgs } from '../context/sdk-context'
8
+
9
+ export function useCreateFeedback(config?: SdkConfig, accessToken?: string | null) {
10
+ const ctx = useSdkContext()
11
+ const { config: cfg, accessToken: token } = resolveHookArgs(config, accessToken, ctx)
12
+ const queryClient = useQueryClient()
13
+ return useMutation({
14
+ mutationFn: (params: CreateFeedbackParams) =>
15
+ token ? createFeedback(cfg, token, params) : Promise.reject(new Error('Please sign in to submit a message.')),
16
+ onSuccess: () => {
17
+ queryClient.invalidateQueries({ queryKey: ['my-feedback'] })
18
+ },
19
+ })
20
+ }
21
+
22
+ export function useMyFeedback(
23
+ params?: { page?: number; page_size?: number },
24
+ config?: SdkConfig,
25
+ accessToken?: string | null
26
+ ) {
27
+ const ctx = useSdkContext()
28
+ const { config: cfg, accessToken: token } = resolveHookArgs(config, accessToken, ctx)
29
+ return useQuery({
30
+ queryKey: ['my-feedback', token, params],
31
+ queryFn: () => token ? getMyFeedback(cfg, token, params) : Promise.reject(new Error('No access token available')),
32
+ enabled: !!token,
33
+ })
34
+ }
@@ -7,47 +7,34 @@ import { useSdkContext, resolveHookArgs } from '../context/sdk-context'
7
7
 
8
8
  export async function getSupportedLanguagesList(config: SdkConfig): Promise<SupportedLang[]> {
9
9
  const data = await getSupportedLanguages(config)
10
- return (data.languages || []).map((language: BackendLanguage) => ({
11
- code: language.code,
12
- appCode: language.code,
13
- label: language.label,
14
- native: language.label,
15
- flag: language.flag,
16
- }))
10
+ return (data.languages || [])
11
+ .filter((language: BackendLanguage) => language.code !== 'auto')
12
+ .map((language: BackendLanguage) => ({
13
+ code: language.code,
14
+ appCode: language.code,
15
+ label: language.label,
16
+ native: language.label,
17
+ flag: language.flag,
18
+ }))
17
19
  }
18
20
 
19
21
  export function useSupportedLanguages(config?: SdkConfig): UseQueryResult<SupportedLang[], Error> {
20
22
  const ctx = useSdkContext()
21
- try {
22
- const { config: cfg } = resolveHookArgs(config, undefined, ctx)
23
- return useQuery({
24
- queryKey: ['supported-languages'],
25
- queryFn: () => getSupportedLanguagesList(cfg),
26
- retry: 1,
27
- ...getCacheTier('high'),
28
- });
29
- } catch {
30
- return {
31
- data: [],
32
- isLoading: false,
33
- error: null,
34
- isFetching: false,
35
- isPending: false,
36
- isError: false,
37
- isSuccess: true,
38
- status: 'success',
39
- fetchStatus: 'idle',
40
- refetch: async () => ({
41
- data: [],
42
- isLoading: false,
43
- error: null,
44
- isFetching: false,
45
- isPending: false,
46
- isError: false,
47
- isSuccess: true,
48
- status: 'success',
49
- fetchStatus: 'idle',
50
- } as unknown as UseQueryResult<SupportedLang[], Error>),
51
- } as unknown as UseQueryResult<SupportedLang[], Error>;
52
- }
23
+ // REAL BUG FIXED (2026-09-22): wrapping resolveHookArgs (and the useQuery
24
+ // call itself) in try/catch swallowed the ONE thing that can actually
25
+ // throw here — a missing SdkConfig/SdkProvider, resolveHookArgs' own
26
+ // deliberate, explicit failure — into a fabricated `{ data: [], isSuccess:
27
+ // true, status: 'success' }`. A caller would read that as "this business
28
+ // genuinely has zero supported languages," not "the SDK isn't wired up,"
29
+ // hiding a real integration bug behind a fake successful empty state.
30
+ // Also called useQuery conditionally inside a try block, which the Rules
31
+ // of Hooks forbid. No other hook in this SDK catches resolveHookArgs;
32
+ // this one shouldn't either.
33
+ const { config: cfg } = resolveHookArgs(config, undefined, ctx)
34
+ return useQuery({
35
+ queryKey: ['supported-languages', cfg.businessId],
36
+ queryFn: () => getSupportedLanguagesList(cfg),
37
+ retry: 1,
38
+ ...getCacheTier('high'),
39
+ });
53
40
  }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Media Upload Hook
3
+ *
4
+ * Wraps media.service.ts's uploadMediaFile/uploadMediaFiles (the real
5
+ * presign → PUT → confirm sequence) as a React Query mutation. No SdkConfig
6
+ * or access token is needed here — the two proxy routes it calls read the
7
+ * customer's session cookie server-side.
8
+ */
9
+ import { useMutation } from '@tanstack/react-query'
10
+ import { uploadMediaFile, uploadMediaFiles, type UploadMediaFileParams } from '../services/media.service'
11
+
12
+ export function useUploadMediaFile() {
13
+ return useMutation({
14
+ mutationFn: (params: UploadMediaFileParams) => uploadMediaFile(params),
15
+ })
16
+ }
17
+
18
+ export function useUploadMediaFiles() {
19
+ return useMutation({
20
+ mutationFn: ({ files, ...params }: Omit<UploadMediaFileParams, 'file'> & { files: File[] }) =>
21
+ uploadMediaFiles(files, params),
22
+ })
23
+ }
@@ -0,0 +1,88 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef } from 'react';
4
+ import { fetchEventSource, EventSourceMessage } from '@microsoft/fetch-event-source';
5
+ import type { SdkConfig } from '../config';
6
+ import { logger } from '../utils';
7
+ import { useSdkContext } from '../context/sdk-context';
8
+
9
+ interface NotificationStreamMessage {
10
+ type: string;
11
+ title?: string;
12
+ body?: string;
13
+ data?: Record<string, unknown>;
14
+ timestamp?: string;
15
+ }
16
+
17
+ export interface UseNotificationStreamOptions {
18
+ config?: SdkConfig;
19
+ accessToken?: string | null;
20
+ onNotification?: (message: NotificationStreamMessage) => void;
21
+ onError?: (error: Error) => void;
22
+ }
23
+
24
+ /**
25
+ * Live connection to the authenticated customer's real-time notification
26
+ * stream (kasuvia-notification-system-go's /notifications/sse — now
27
+ * Authorization-header-gated, see realtime.go's 2026-09-21 fix). Mirrors
28
+ * use-order-stream.ts's proven fetch-event-source pattern exactly.
29
+ */
30
+ export function useNotificationStream({
31
+ config: explicitConfig,
32
+ accessToken: explicitToken,
33
+ onNotification,
34
+ onError,
35
+ }: UseNotificationStreamOptions) {
36
+ const ctx = useSdkContext();
37
+ const config = explicitConfig ?? ctx?.config;
38
+ const accessToken = explicitToken !== undefined ? explicitToken : (ctx?.accessToken ?? null);
39
+ const abortControllerRef = useRef<AbortController | null>(null);
40
+
41
+ useEffect(() => {
42
+ if (!accessToken || !config) {
43
+ return;
44
+ }
45
+
46
+ const abortController = new AbortController();
47
+ abortControllerRef.current = abortController;
48
+
49
+ const streamUrl = `${config.backendUrls.notifications}/notifications/sse`;
50
+
51
+ fetchEventSource(streamUrl, {
52
+ method: 'GET',
53
+ headers: { Authorization: `Bearer ${accessToken}` },
54
+ signal: abortController.signal,
55
+ openWhenHidden: true,
56
+ onopen: async (response) => {
57
+ if (response.ok) {
58
+ logger.log('[NotificationStream] Connection established');
59
+ return;
60
+ }
61
+ throw new Error(`Unexpected response: ${response.status}`);
62
+ },
63
+ onmessage: (event: EventSourceMessage) => {
64
+ try {
65
+ if (!event.data || event.data.trim() === '') return;
66
+ const data = JSON.parse(event.data) as NotificationStreamMessage;
67
+ if (data.type === 'connected') return; // handshake frame, not a real notification
68
+ logger.log('[NotificationStream] Received:', data);
69
+ onNotification?.(data);
70
+ } catch (error) {
71
+ logger.error('[NotificationStream] Error parsing event:', error);
72
+ }
73
+ },
74
+ onerror: (error: Error) => {
75
+ if (abortController.signal.aborted) return;
76
+ logger.error('[NotificationStream] Connection error:', error);
77
+ onError?.(error);
78
+ },
79
+ }).catch((error) => {
80
+ if (abortController.signal.aborted || error.name === 'AbortError') return;
81
+ logger.error('[NotificationStream] fetchEventSource failed:', error);
82
+ });
83
+
84
+ return () => {
85
+ abortController.abort();
86
+ };
87
+ }, [accessToken, config, onNotification, onError]);
88
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Notification Preference Hooks
3
+ *
4
+ * React Query hooks for notification channel preferences — both the
5
+ * caller's own self-service preferences (customer or business owner) and
6
+ * the merchant-level channel policy (owner-only).
7
+ * Stateless - requires config + accessToken.
8
+ */
9
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
10
+ import {
11
+ getNotificationPreferences,
12
+ updateNotificationPreferences,
13
+ sendContactEmail,
14
+ sendContactConfirmation,
15
+ getVapidPublicKey,
16
+ subscribeToPush,
17
+ unsubscribeFromPush,
18
+ getNotificationInbox,
19
+ markNotificationRead,
20
+ markAllNotificationsRead,
21
+ trackNotificationClick,
22
+ type ContactFormData,
23
+ type SubscribeToPushParams,
24
+ } from '../services/notifications.service'
25
+ import type { NotificationPreferences } from '../types/auth'
26
+ import type { SdkConfig } from '../config'
27
+ import { useSdkContext, resolveHookArgs } from '../context/sdk-context'
28
+ import { logger } from '../utils/logger'
29
+
30
+ export function useNotificationPreferences(config?: SdkConfig, accessToken?: string) {
31
+ const ctx = useSdkContext()
32
+ const { config: cfg, accessToken: token } = resolveHookArgs(config, accessToken ?? null, ctx)
33
+ return useQuery({
34
+ queryKey: ['notification-preferences'],
35
+ queryFn: () => getNotificationPreferences(cfg, token ?? ''),
36
+ enabled: !!token,
37
+ })
38
+ }
39
+
40
+ export function useUpdateNotificationPreferences(config?: SdkConfig, accessToken?: string | null) {
41
+ const ctx = useSdkContext()
42
+ const { config: cfg, accessToken: token } = resolveHookArgs(config, accessToken, ctx)
43
+ const queryClient = useQueryClient()
44
+ return useMutation({
45
+ mutationFn: (payload: Partial<NotificationPreferences>) =>
46
+ token ? updateNotificationPreferences(cfg, token, payload) : Promise.reject(new Error('No access token available')),
47
+ onSuccess: () => {
48
+ queryClient.invalidateQueries({ queryKey: ['notification-preferences'] })
49
+ },
50
+ })
51
+ }
52
+
53
+ // ── Push subscriptions ───────────────────────────────────────────────────
54
+
55
+ export function useVapidPublicKey(config?: SdkConfig) {
56
+ const ctx = useSdkContext()
57
+ const { config: cfg } = resolveHookArgs(config, null, ctx)
58
+ return useQuery({
59
+ queryKey: ['vapid-public-key'],
60
+ queryFn: () => getVapidPublicKey(cfg),
61
+ staleTime: Infinity,
62
+ })
63
+ }
64
+
65
+ export function useSubscribeToPush(config?: SdkConfig, accessToken?: string | null) {
66
+ const ctx = useSdkContext()
67
+ const { config: cfg, accessToken: token } = resolveHookArgs(config, accessToken, ctx)
68
+ return useMutation({
69
+ mutationFn: (params: SubscribeToPushParams) =>
70
+ token ? subscribeToPush(cfg, token, params) : Promise.reject(new Error('Please sign in to enable push notifications.')),
71
+ })
72
+ }
73
+
74
+ export function useUnsubscribeFromPush(config?: SdkConfig, accessToken?: string | null) {
75
+ const ctx = useSdkContext()
76
+ const { config: cfg, accessToken: token } = resolveHookArgs(config, accessToken, ctx)
77
+ return useMutation({
78
+ mutationFn: (endpoint: string) =>
79
+ token ? unsubscribeFromPush(cfg, token, endpoint) : Promise.reject(new Error('No access token available')),
80
+ })
81
+ }
82
+
83
+ // ── Notification inbox ──────────────────────────────────────────────────
84
+
85
+ export function useNotificationInbox(
86
+ params?: { page?: number; limit?: number; unread_only?: boolean },
87
+ config?: SdkConfig,
88
+ accessToken?: string | null
89
+ ) {
90
+ const ctx = useSdkContext()
91
+ const { config: cfg, accessToken: token } = resolveHookArgs(config, accessToken, ctx)
92
+ return useQuery({
93
+ queryKey: ['notification-inbox', token, params],
94
+ queryFn: () => token ? getNotificationInbox(cfg, token, params) : Promise.reject(new Error('No access token available')),
95
+ enabled: !!token,
96
+ })
97
+ }
98
+
99
+ export function useMarkNotificationRead(config?: SdkConfig, accessToken?: string | null) {
100
+ const ctx = useSdkContext()
101
+ const { config: cfg, accessToken: token } = resolveHookArgs(config, accessToken, ctx)
102
+ const queryClient = useQueryClient()
103
+ return useMutation({
104
+ mutationFn: (notificationId: string) =>
105
+ token ? markNotificationRead(cfg, token, notificationId) : Promise.reject(new Error('No access token available')),
106
+ onSuccess: () => {
107
+ queryClient.invalidateQueries({ queryKey: ['notification-inbox'] })
108
+ },
109
+ })
110
+ }
111
+
112
+ export function useMarkAllNotificationsRead(config?: SdkConfig, accessToken?: string | null) {
113
+ const ctx = useSdkContext()
114
+ const { config: cfg, accessToken: token } = resolveHookArgs(config, accessToken, ctx)
115
+ const queryClient = useQueryClient()
116
+ return useMutation({
117
+ mutationFn: () =>
118
+ token ? markAllNotificationsRead(cfg, token) : Promise.reject(new Error('No access token available')),
119
+ onSuccess: () => {
120
+ queryClient.invalidateQueries({ queryKey: ['notification-inbox'] })
121
+ },
122
+ })
123
+ }
124
+
125
+ export function useTrackNotificationClick(config?: SdkConfig, accessToken?: string | null) {
126
+ const ctx = useSdkContext()
127
+ const { config: cfg, accessToken: token } = resolveHookArgs(config, accessToken, ctx)
128
+ return useMutation({
129
+ mutationFn: (notificationId: string) =>
130
+ token ? trackNotificationClick(cfg, token, notificationId) : Promise.reject(new Error('No access token available')),
131
+ })
132
+ }
133
+
134
+ /**
135
+ * Submit contact form hook — sends contact email to merchant and confirmation to sender.
136
+ */
137
+ export function useSubmitContactForm(config?: SdkConfig) {
138
+ const ctx = useSdkContext()
139
+ const { config: cfg } = resolveHookArgs(config, null, ctx)
140
+ return useMutation({
141
+ mutationFn: async (payload: ContactFormData) => {
142
+ // REAL BUG FIXED (2026-09-22): defaulted a missing `source` to the
143
+ // literal string 'kasuvia-storefront' — a specific app name hardcoded
144
+ // into a package meant to be reused by other storefronts too. A
145
+ // different consuming app that forgot to pass `source` would have its
146
+ // contact-form submissions silently mislabeled as coming from an app
147
+ // it isn't. The caller decides what `source` means; not passing one
148
+ // is left as-is rather than guessed.
149
+ const emailPayload = {
150
+ ...payload,
151
+ current_year: new Date().getFullYear(),
152
+ }
153
+ const res = await sendContactEmail(cfg, emailPayload)
154
+ try {
155
+ await sendContactConfirmation(cfg, emailPayload)
156
+ } catch (confirmError) {
157
+ logger.error('[useSubmitContactForm] Failed to send confirmation email:', confirmError)
158
+ }
159
+ return res
160
+ },
161
+ })
162
+ }
163
+
@@ -17,6 +17,16 @@ interface PaymentSuccessData {
17
17
  };
18
18
  }
19
19
 
20
+ interface PaymentFailedData {
21
+ event: string;
22
+ data: {
23
+ transaction_id: string;
24
+ order_id: string;
25
+ status: string;
26
+ gateway: string;
27
+ };
28
+ }
29
+
20
30
  interface DigitalReadyData {
21
31
  event: string;
22
32
  data: {
@@ -47,18 +57,6 @@ interface BespokeUpdateData {
47
57
  };
48
58
  }
49
59
 
50
- interface ConsultationUpdateData {
51
- event: string;
52
- data: {
53
- consultation_id: string;
54
- business_id: string;
55
- customer_id: string;
56
- status: string;
57
- message?: string;
58
- updated_at: string;
59
- };
60
- }
61
-
62
60
  interface ServiceBookingUpdateData {
63
61
  event: string;
64
62
  data: {
@@ -76,22 +74,23 @@ export interface UseOrderStreamOptions {
76
74
  config?: SdkConfig;
77
75
  orderId: string;
78
76
  onPaymentComplete?: (data: PaymentSuccessData) => void;
77
+ /** Fires on a real "payment.failed" event from the order-scoped stream (kasuvia-payment-system-go's webhook services now publish this — see monnify_webhook_service.go/flutterwave_webhook_service.go). */
78
+ onPaymentFailed?: (data: PaymentFailedData) => void;
79
79
  onDigitalReady?: (data: DigitalReadyData) => void;
80
80
  onBespokeUpdate?: (data: BespokeUpdateData) => void;
81
- onConsultationUpdate?: (data: ConsultationUpdateData) => void;
82
81
  onServiceBookingUpdate?: (data: ServiceBookingUpdateData) => void;
83
82
  onError?: (error: Error) => void;
84
83
  }
85
84
 
86
- export function useOrderStream({
85
+ export function useOrderStream({
87
86
  config: explicitConfig,
88
- orderId,
89
- onPaymentComplete,
90
- onDigitalReady,
91
- onBespokeUpdate,
92
- onConsultationUpdate,
93
- onServiceBookingUpdate,
94
- onError
87
+ orderId,
88
+ onPaymentComplete,
89
+ onPaymentFailed,
90
+ onDigitalReady,
91
+ onBespokeUpdate,
92
+ onServiceBookingUpdate,
93
+ onError
95
94
  }: UseOrderStreamOptions) {
96
95
  const ctx = useSdkContext();
97
96
  const config = explicitConfig ?? ctx?.config;
@@ -127,6 +126,14 @@ export function useOrderStream({
127
126
  abortController.abort();
128
127
  }
129
128
 
129
+ if (data.event === 'payment.failed') {
130
+ logger.log('[OrderStream] Payment failed detected, calling onPaymentFailed');
131
+ if (onPaymentFailed) {
132
+ onPaymentFailed(data as PaymentFailedData);
133
+ }
134
+ abortController.abort();
135
+ }
136
+
130
137
  if (data.event === 'digital.ready') {
131
138
  logger.log('[OrderStream] Digital delivery ready detected, calling onDigitalReady');
132
139
  if (onDigitalReady) {
@@ -141,13 +148,6 @@ export function useOrderStream({
141
148
  }
142
149
  }
143
150
 
144
- if (data.event === 'consultation.update') {
145
- logger.log('[OrderStream] Consultation update detected, calling onConsultationUpdate');
146
- if (onConsultationUpdate) {
147
- onConsultationUpdate(data as ConsultationUpdateData);
148
- }
149
- }
150
-
151
151
  if (data.event === 'service_booking.update') {
152
152
  logger.log('[OrderStream] Service booking update detected, calling onServiceBookingUpdate');
153
153
  if (onServiceBookingUpdate) {
@@ -193,7 +193,7 @@ export function useOrderStream({
193
193
  return () => {
194
194
  abortController.abort();
195
195
  };
196
- }, [orderId, onPaymentComplete, onDigitalReady, onBespokeUpdate, onConsultationUpdate, onServiceBookingUpdate, onError]);
196
+ }, [orderId, onPaymentComplete, onPaymentFailed, onDigitalReady, onBespokeUpdate, onServiceBookingUpdate, onError]);
197
197
 
198
198
  return null;
199
199
  }