@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.
- package/README.md +42 -15
- package/dist/AddressAutocomplete-CjcqPElD.d.mts +46 -0
- package/dist/AddressAutocomplete-CvJHuRkf.d.ts +46 -0
- package/dist/components/index.d.mts +497 -0
- package/dist/components/index.d.ts +497 -0
- package/dist/components/index.js +4217 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/index.mjs +4194 -0
- package/dist/components/index.mjs.map +1 -0
- package/dist/config/server.d.mts +26 -0
- package/dist/config/server.d.ts +26 -0
- package/dist/config/server.js +38 -0
- package/dist/config/server.js.map +1 -0
- package/dist/config/server.mjs +13 -0
- package/dist/config/server.mjs.map +1 -0
- package/dist/hooks/index.d.mts +393 -160
- package/dist/hooks/index.d.ts +393 -160
- package/dist/hooks/index.js +2792 -2137
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/index.mjs +2734 -2121
- package/dist/hooks/index.mjs.map +1 -1
- package/dist/index-CNW_LINy.d.mts +578 -0
- package/dist/index-CNW_LINy.d.ts +578 -0
- package/dist/{index-C1chUA3P.d.mts → index-F7N6UItS.d.mts} +1 -1
- package/dist/{index-C1chUA3P.d.ts → index-F7N6UItS.d.ts} +1 -1
- package/dist/index-NkI10J_E.d.mts +40 -0
- package/dist/index-NkI10J_E.d.ts +40 -0
- package/dist/index.d.mts +20437 -103
- package/dist/index.d.ts +20437 -103
- package/dist/index.js +2111 -1802
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2006 -1786
- package/dist/index.mjs.map +1 -1
- package/dist/media.service-Cx7dQjzX.d.mts +1393 -0
- package/dist/media.service-mrBFJ2UQ.d.ts +1393 -0
- package/dist/services/index.d.mts +908 -2
- package/dist/services/index.d.ts +908 -2
- package/dist/services/index.js +1492 -1686
- package/dist/services/index.js.map +1 -1
- package/dist/services/index.mjs +1431 -1670
- package/dist/services/index.mjs.map +1 -1
- package/package.json +46 -6
- package/src/components/bespoke/BespokeAvailabilityBadge.tsx +32 -0
- package/src/components/bespoke/BespokeProgressTracker.tsx +58 -0
- package/src/components/bespoke/index.ts +3 -0
- package/src/components/booking/SlotPicker.tsx +97 -0
- package/src/components/booking/index.ts +1 -0
- package/src/components/cart/CartItem.tsx +81 -0
- package/src/components/cart/CartSidebar.tsx +115 -0
- package/src/components/cart/CartTypeMismatchBanner.tsx +55 -0
- package/src/components/cart/index.ts +3 -0
- package/src/components/index.ts +8 -0
- package/src/components/orders/DeliverableReviewer.tsx +139 -0
- package/src/components/orders/index.ts +1 -0
- package/src/components/product/ProductCard.tsx +350 -0
- package/src/components/product/ProductGrid.tsx +84 -0
- package/src/components/product/index.ts +2 -0
- package/src/components/shared/AddressAutocomplete.tsx +212 -0
- package/src/components/shared/AnalyticsScripts.tsx +56 -0
- package/src/components/shared/CookieConsentBanner.tsx +98 -0
- package/src/components/shared/ImageEditorModal.tsx +1066 -0
- package/src/components/shared/ImageViewerModal.tsx +598 -0
- package/src/components/shared/LucideReactIcon.tsx +52 -0
- package/src/components/shared/MapPinDrop.tsx +101 -0
- package/src/components/shared/MultiSelectPicker.tsx +193 -0
- package/src/components/shared/OfflineBanner.tsx +46 -0
- package/src/components/shared/SearchableInput.tsx +153 -0
- package/src/components/shared/SearchablePopover.tsx +166 -0
- package/src/components/shared/SmartImage.tsx +311 -0
- package/src/components/shared/TruncatedValue.tsx +27 -0
- package/src/components/shared/UnsavedChangesGuard.tsx +138 -0
- package/src/components/shared/index.ts +15 -0
- package/src/components/skeletons/CardSkeleton.tsx +26 -0
- package/src/components/skeletons/CartItemSkeleton.tsx +26 -0
- package/src/components/skeletons/OrderItemSkeleton.tsx +26 -0
- package/src/components/skeletons/ProductCardSkeleton.tsx +26 -0
- package/src/components/skeletons/TableSkeleton.tsx +42 -0
- package/src/components/skeletons/index.ts +5 -0
- package/src/config/index.ts +22 -4
- package/src/config/server.ts +45 -0
- package/src/context/sdk-context.tsx +5 -2
- package/src/generated/auth-openapi.ts +2935 -0
- package/src/generated/cart-openapi.ts +652 -0
- package/src/generated/cms-openapi.ts +1668 -0
- package/src/generated/crm-openapi.ts +986 -0
- package/src/generated/graphql.ts +126 -0
- package/src/generated/index.ts +11 -0
- package/src/generated/media-openapi.ts +613 -0
- package/src/generated/notification-openapi.ts +862 -0
- package/src/generated/payment-openapi.ts +2713 -0
- package/src/generated/store-openapi.ts +9510 -0
- package/src/graphql/operations/collections.graphql +32 -0
- package/src/graphql/operations/products.graphql +197 -0
- package/src/graphql/operations/wishlist.graphql +25 -0
- package/src/hooks/index.ts +9 -2
- package/src/hooks/use-auth.ts +163 -69
- package/src/hooks/use-bespoke-availability.ts +39 -22
- package/src/hooks/use-business-config.ts +51 -0
- package/src/hooks/use-cart.ts +310 -238
- package/src/hooks/use-cms.ts +107 -127
- package/src/hooks/use-collections.ts +35 -55
- package/src/hooks/use-consultations.ts +9 -136
- package/src/hooks/use-countries.ts +19 -28
- package/src/hooks/use-currencies.ts +8 -20
- package/src/hooks/use-customer-address.ts +64 -0
- package/src/hooks/use-feedback.ts +34 -0
- package/src/hooks/use-languages.ts +26 -39
- package/src/hooks/use-media-upload.ts +23 -0
- package/src/hooks/use-notification-stream.ts +88 -0
- package/src/hooks/use-notifications.ts +163 -0
- package/src/hooks/use-order-stream.ts +29 -29
- package/src/hooks/use-orders.ts +91 -73
- package/src/hooks/use-payment.ts +116 -84
- package/src/hooks/use-products.ts +65 -269
- package/src/hooks/use-push-notifications.ts +122 -0
- package/src/hooks/use-reviews.ts +175 -0
- package/src/hooks/use-service-booking.ts +109 -132
- package/src/hooks/use-states.ts +16 -30
- package/src/hooks/use-wishlist.ts +44 -69
- package/src/index.ts +15 -11
- package/src/services/auth.service.ts +422 -132
- package/src/services/bespoke.service.ts +69 -10
- package/src/services/cart.service.ts +155 -186
- package/src/services/cms.service.ts +41 -50
- package/src/services/collection.service.ts +25 -63
- package/src/services/config.service.ts +10 -21
- package/src/services/consultation.service.ts +36 -93
- package/src/services/feedback.service.ts +76 -0
- package/src/services/index.ts +7 -1
- package/src/services/legal.service.ts +62 -0
- package/src/services/logistics.service.ts +235 -53
- package/src/services/media.service.ts +115 -0
- package/src/services/notifications.service.ts +204 -20
- package/src/services/order.service.ts +498 -127
- package/src/services/payment.service.ts +149 -100
- package/src/services/product.service.ts +157 -814
- package/src/services/review.service.ts +211 -0
- package/src/services/service-booking.service.ts +198 -99
- package/src/services/wishlist.service.ts +27 -74
- package/src/types/auth/index.ts +161 -18
- package/src/types/business/index.ts +9 -1
- package/src/types/cms/index.ts +91 -6
- package/src/types/cms/legal.ts +18 -0
- package/src/types/consultations/index.ts +60 -68
- package/src/types/index.ts +5 -3
- package/src/types/media/index.ts +35 -3
- package/src/types/payments/index.ts +80 -16
- package/src/types/reviews/index.ts +116 -0
- package/src/types/services/index.ts +113 -38
- package/src/types/store/checkout.ts +53 -0
- package/src/types/store/index.ts +273 -52
- package/src/types/store/invoice.ts +31 -0
- package/src/types/store/shipping.ts +84 -3
- package/src/types/ui/index.ts +1 -1
- package/src/utils/checkout.ts +19 -3
- package/src/utils/cookieConsent.ts +34 -0
- package/src/utils/format.ts +408 -0
- package/src/utils/geolocation.ts +134 -0
- package/src/utils/graphql-url.ts +47 -0
- package/src/utils/guest-session.ts +59 -0
- package/src/utils/http-client.ts +147 -0
- package/src/utils/index.ts +11 -4
- package/src/utils/status-colors.ts +79 -0
- package/src/utils/token-refresh.ts +115 -0
- package/src/utils/validators.ts +13 -0
- package/src/utils/youtube.ts +18 -0
- package/dist/index-C5YO9gTl.d.mts +0 -507
- package/dist/index-C5YO9gTl.d.ts +0 -507
- package/dist/index-CefG7_xq.d.mts +0 -793
- package/dist/index-DeHCf3zI.d.ts +0 -793
- package/src/graphql/collections.graphql.ts +0 -48
- package/src/graphql/index.ts +0 -7
- package/src/graphql/products.graphql.ts +0 -860
- package/src/graphql/wishlist.graphql.ts +0 -34
- package/src/hooks/use-consultation-stream.ts +0 -142
- 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 || [])
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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,
|
|
196
|
+
}, [orderId, onPaymentComplete, onPaymentFailed, onDigitalReady, onBespokeUpdate, onServiceBookingUpdate, onError]);
|
|
197
197
|
|
|
198
198
|
return null;
|
|
199
199
|
}
|