@kasuvia/sdk 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +112 -0
  3. package/dist/index.d.mts +1891 -0
  4. package/dist/index.d.ts +1891 -0
  5. package/dist/index.js +3540 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/index.mjs +3314 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +59 -0
  10. package/src/client/authClient.ts +135 -0
  11. package/src/client/graphqlClient.ts +53 -0
  12. package/src/client/httpClient.ts +298 -0
  13. package/src/config/index.ts +102 -0
  14. package/src/graphql/collections.graphql.ts +56 -0
  15. package/src/graphql/index.ts +7 -0
  16. package/src/graphql/products.graphql.ts +710 -0
  17. package/src/graphql/wishlist.graphql.ts +33 -0
  18. package/src/hooks/index.ts +26 -0
  19. package/src/hooks/use-auth.ts +182 -0
  20. package/src/hooks/use-bespoke-availability.ts +20 -0
  21. package/src/hooks/use-cart.ts +207 -0
  22. package/src/hooks/use-cms.ts +165 -0
  23. package/src/hooks/use-collections.ts +40 -0
  24. package/src/hooks/use-consultation-stream.ts +141 -0
  25. package/src/hooks/use-consultations.ts +76 -0
  26. package/src/hooks/use-countries.ts +24 -0
  27. package/src/hooks/use-currencies.ts +27 -0
  28. package/src/hooks/use-google-translate.ts +164 -0
  29. package/src/hooks/use-is-mounted.ts +9 -0
  30. package/src/hooks/use-languages.ts +33 -0
  31. package/src/hooks/use-order-stream.ts +195 -0
  32. package/src/hooks/use-orders.ts +64 -0
  33. package/src/hooks/use-payment.ts +68 -0
  34. package/src/hooks/use-products.ts +113 -0
  35. package/src/hooks/use-service-booking.ts +118 -0
  36. package/src/hooks/use-states.ts +28 -0
  37. package/src/hooks/use-wishlist.ts +121 -0
  38. package/src/index.ts +65 -0
  39. package/src/services/app.service.ts +17 -0
  40. package/src/services/auth.service.ts +332 -0
  41. package/src/services/bespoke.service.ts +17 -0
  42. package/src/services/cart.service.ts +245 -0
  43. package/src/services/cms.service.ts +112 -0
  44. package/src/services/collection.service.ts +77 -0
  45. package/src/services/config.service.ts +42 -0
  46. package/src/services/consultation.service.ts +51 -0
  47. package/src/services/index.ts +21 -0
  48. package/src/services/logistics.service.ts +95 -0
  49. package/src/services/notifications.service.ts +50 -0
  50. package/src/services/order.service.ts +169 -0
  51. package/src/services/payment.service.ts +127 -0
  52. package/src/services/product.service.ts +678 -0
  53. package/src/services/service-booking.service.ts +86 -0
  54. package/src/services/wishlist.service.ts +84 -0
  55. package/src/types/auth/index.ts +126 -0
  56. package/src/types/bespoke/index.ts +39 -0
  57. package/src/types/business/index.ts +6 -0
  58. package/src/types/cms/index.ts +77 -0
  59. package/src/types/consultations/index.ts +79 -0
  60. package/src/types/index.ts +27 -0
  61. package/src/types/media/index.ts +41 -0
  62. package/src/types/payments/index.ts +120 -0
  63. package/src/types/services/index.ts +80 -0
  64. package/src/types/store/index.ts +282 -0
  65. package/src/types/store/shipping.ts +38 -0
  66. package/src/types/ui/index.ts +32 -0
  67. package/src/utils/cache.ts +33 -0
  68. package/src/utils/checkout.ts +39 -0
  69. package/src/utils/customer.ts +20 -0
  70. package/src/utils/error-handler.ts +14 -0
  71. package/src/utils/index.ts +10 -0
  72. package/src/utils/logger.ts +79 -0
  73. package/src/utils/sanitize-url.ts +55 -0
@@ -0,0 +1,141 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { fetchEventSource, EventSourceMessage } from '@microsoft/fetch-event-source';
5
+ import { getBackendUrl } from '../config';
6
+ import { logger } from '../utils';
7
+
8
+ interface ConsultationMessageData {
9
+ event: string;
10
+ data: {
11
+ consultation_id: string;
12
+ message_id: string;
13
+ sender_id: string;
14
+ sender_role: string;
15
+ message: string;
16
+ attachments?: Array<{ url: string; type: string; name: string }>;
17
+ created_at: string;
18
+ };
19
+ }
20
+
21
+ interface ConsultationQuoteData {
22
+ event: string;
23
+ data: {
24
+ consultation_id: string;
25
+ quote_amount: number;
26
+ deposit_amount?: number;
27
+ merchant_notes?: string;
28
+ updated_at: string;
29
+ };
30
+ }
31
+
32
+ interface ConsultationStatusData {
33
+ event: string;
34
+ data: {
35
+ consultation_id: string;
36
+ status: string;
37
+ updated_at: string;
38
+ };
39
+ }
40
+
41
+ interface UseConsultationStreamOptions {
42
+ consultationId: string;
43
+ onMessage?: (data: ConsultationMessageData) => void;
44
+ onQuote?: (data: ConsultationQuoteData) => void;
45
+ onStatusChange?: (data: ConsultationStatusData) => void;
46
+ onError?: (error: Error) => void;
47
+ }
48
+
49
+ export function useConsultationStream({
50
+ consultationId,
51
+ onMessage,
52
+ onQuote,
53
+ onStatusChange,
54
+ onError,
55
+ }: UseConsultationStreamOptions) {
56
+ const abortControllerRef = useRef<AbortController | null>(null);
57
+ const [isConnected, setIsConnected] = useState(false);
58
+ const [lastEvent, setLastEvent] = useState<any>(null);
59
+
60
+ useEffect(() => {
61
+ if (!consultationId) {
62
+ return;
63
+ }
64
+
65
+ const abortController = new AbortController();
66
+ abortControllerRef.current = abortController;
67
+
68
+ const paymentUrl = getBackendUrl('payment');
69
+ const streamUrl = `${paymentUrl}/events/stream/consultation/${consultationId}`;
70
+
71
+ fetchEventSource(streamUrl, {
72
+ method: 'GET',
73
+ signal: abortController.signal,
74
+ onmessage: (event: EventSourceMessage) => {
75
+ try {
76
+ if (!event.data || event.data.trim() === '') {
77
+ return;
78
+ }
79
+
80
+ const data = JSON.parse(event.data);
81
+ logger.log('[ConsultationStream] Received event:', data);
82
+ setLastEvent(data);
83
+
84
+ if (data.event === 'consultation.message') {
85
+ logger.log('[ConsultationStream] Message detected, calling onMessage');
86
+ if (onMessage) {
87
+ onMessage(data as ConsultationMessageData);
88
+ }
89
+ }
90
+
91
+ if (data.event === 'consultation.quote') {
92
+ logger.log('[ConsultationStream] Quote detected, calling onQuote');
93
+ if (onQuote) {
94
+ onQuote(data as ConsultationQuoteData);
95
+ }
96
+ }
97
+
98
+ if (data.event === 'consultation.status') {
99
+ logger.log('[ConsultationStream] Status change detected, calling onStatusChange');
100
+ if (onStatusChange) {
101
+ onStatusChange(data as ConsultationStatusData);
102
+ }
103
+ }
104
+ } catch (error) {
105
+ logger.error('[ConsultationStream] Error parsing event:', error);
106
+ }
107
+ },
108
+ onerror: (error: Error) => {
109
+ if (abortController.signal.aborted) {
110
+ logger.log('[ConsultationStream] Connection closed via AbortController');
111
+ return;
112
+ }
113
+ logger.error('[ConsultationStream] Connection error:', error);
114
+ setIsConnected(false);
115
+ if (onError) {
116
+ onError(error);
117
+ }
118
+ },
119
+ onopen: async (response: Response) => {
120
+ if (response.ok && response.headers.get('content-type')?.includes('text/event-stream')) {
121
+ logger.log('[ConsultationStream] Connection established for consultation:', consultationId);
122
+ setIsConnected(true);
123
+ return;
124
+ }
125
+ throw new Error(`Unexpected response: ${response.status}`);
126
+ },
127
+ }).catch(error => {
128
+ if (abortController.signal.aborted || error.name === 'AbortError') {
129
+ return;
130
+ }
131
+ logger.error('[ConsultationStream] fetchEventSource failed:', error);
132
+ });
133
+
134
+ return () => {
135
+ abortController.abort();
136
+ setIsConnected(false);
137
+ };
138
+ }, [consultationId, onMessage, onQuote, onStatusChange, onError]);
139
+
140
+ return { isConnected, lastEvent };
141
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Consultations Hooks
3
+ *
4
+ * React Query hooks for consultation operations.
5
+ */
6
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
7
+ import { consultationService } from '../services/consultation.service'
8
+ import type {
9
+ Consultation,
10
+ ConsultationCreatePayload,
11
+ ConsultationMessagePayload,
12
+ ConsultationQuoteAcceptPayload,
13
+ } from '../types/consultations'
14
+
15
+ export function useConsultations() {
16
+ return useQuery({
17
+ queryKey: ['consultations'],
18
+ queryFn: () => consultationService.getConsultations(),
19
+ })
20
+ }
21
+
22
+ export function useConsultation(id: string) {
23
+ return useQuery({
24
+ queryKey: ['consultation', id],
25
+ queryFn: () => consultationService.getConsultation(id),
26
+ enabled: !!id,
27
+ })
28
+ }
29
+
30
+ export function useCreateConsultation() {
31
+ const queryClient = useQueryClient()
32
+
33
+ return useMutation({
34
+ mutationFn: (payload: ConsultationCreatePayload) =>
35
+ consultationService.createConsultation(payload),
36
+ onSuccess: () => {
37
+ queryClient.invalidateQueries({ queryKey: ['consultations'] })
38
+ },
39
+ })
40
+ }
41
+
42
+ export function useSendMessage(consultationId: string) {
43
+ const queryClient = useQueryClient()
44
+
45
+ return useMutation({
46
+ mutationFn: (payload: ConsultationMessagePayload) =>
47
+ consultationService.sendMessage(consultationId, payload),
48
+ onSuccess: () => {
49
+ queryClient.invalidateQueries({ queryKey: ['consultation', consultationId] })
50
+ },
51
+ })
52
+ }
53
+
54
+ export function useAcceptQuote(consultationId: string) {
55
+ const queryClient = useQueryClient()
56
+
57
+ return useMutation({
58
+ mutationFn: (payload: ConsultationQuoteAcceptPayload) =>
59
+ consultationService.acceptQuote(consultationId, payload),
60
+ onSuccess: () => {
61
+ queryClient.invalidateQueries({ queryKey: ['consultation', consultationId] })
62
+ },
63
+ })
64
+ }
65
+
66
+ export function useRejectQuote(consultationId: string) {
67
+ const queryClient = useQueryClient()
68
+
69
+ return useMutation({
70
+ mutationFn: (reason?: string) =>
71
+ consultationService.rejectQuote(consultationId, reason),
72
+ onSuccess: () => {
73
+ queryClient.invalidateQueries({ queryKey: ['consultation', consultationId] })
74
+ },
75
+ })
76
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Countries Hook
3
+ *
4
+ * React Query hook for fetching countries list.
5
+ */
6
+ import { useQuery } from '@tanstack/react-query'
7
+ import { authAxios } from '../client/httpClient'
8
+ import type { Country } from '../types/ui'
9
+
10
+ export async function getCountries(): Promise<Country[]> {
11
+ const { data } = await authAxios.get<Country[]>('auth/countries/')
12
+ return data
13
+ }
14
+
15
+ export function useCountries() {
16
+ return useQuery({
17
+ queryKey: ['countries'],
18
+ queryFn: () => getCountries(),
19
+ retry: 1,
20
+ refetchOnWindowFocus: false,
21
+ refetchOnReconnect: false,
22
+ staleTime: 24 * 60 * 60 * 1000, // 24 hours
23
+ })
24
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Currencies Hook
3
+ *
4
+ * React Query hook for fetching supported currencies from the payment service.
5
+ * The query is disabled until the SDK is initialized so that no request fires
6
+ * before tenant credentials (businessId, storefrontApiKey) are available.
7
+ */
8
+ import { useQuery } from '@tanstack/react-query'
9
+ import { getCacheTier } from '../utils/cache'
10
+ import { getSupportedCurrencies } from '../services/config.service'
11
+ import { isInitialized } from '../config'
12
+ import type { Currency } from '../types/payments'
13
+
14
+ export async function getSupportedCurrenciesList(): Promise<Currency[]> {
15
+ const data = await getSupportedCurrencies()
16
+ return data.currencies || []
17
+ }
18
+
19
+ export function useCurrencies() {
20
+ return useQuery({
21
+ queryKey: ['supported-currencies'],
22
+ queryFn: () => getSupportedCurrenciesList(),
23
+ enabled: isInitialized(),
24
+ retry: 1,
25
+ ...getCacheTier('high'),
26
+ })
27
+ }
@@ -0,0 +1,164 @@
1
+ import { useEffect, useCallback, useState, useRef } from "react";
2
+ import { useSupportedLanguages } from "./use-languages";
3
+
4
+ interface GoogleTranslateOptions {
5
+ pageLanguage: string;
6
+ includedLanguages?: string;
7
+ autoDisplay?: boolean;
8
+ layout?: number;
9
+ }
10
+
11
+ interface GoogleTranslateElement {
12
+ new(options: GoogleTranslateOptions, elementId: string): void;
13
+ }
14
+
15
+ interface GoogleNamespace {
16
+ translate?: {
17
+ TranslateElement: GoogleTranslateElement;
18
+ };
19
+ }
20
+
21
+ declare global {
22
+ interface Window {
23
+ googleTranslateElementInit?: () => void;
24
+ google?: GoogleNamespace;
25
+ }
26
+ }
27
+
28
+ const SCRIPT_ID = "google-translate-script";
29
+ const ELEMENT_ID = "google_translate_element";
30
+
31
+ function fireChange(googleCode: string) {
32
+ const combo = document.querySelector<HTMLSelectElement>(".goog-te-combo");
33
+ if (combo) {
34
+ combo.value = googleCode;
35
+ combo.dispatchEvent(new Event("change"));
36
+ return true;
37
+ }
38
+ return false;
39
+ }
40
+
41
+ export function useGoogleTranslate(appLang: string) {
42
+ const [isLoaded, setIsLoaded] = useState(false);
43
+ const initRef = useRef(false);
44
+ const { data: supportedLangs = [] } = useSupportedLanguages();
45
+
46
+ const clearCookies = useCallback(() => {
47
+ if (typeof window === "undefined") return;
48
+ const domains = [
49
+ window.location.hostname,
50
+ "." + window.location.hostname,
51
+ "." + window.location.hostname.split('.').slice(-2).join('.')
52
+ ];
53
+ const path = "/";
54
+ // Clear googtrans
55
+ document.cookie = `googtrans=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=${path};`;
56
+ domains.forEach(domain => {
57
+ document.cookie = `googtrans=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=${path}; domain=${domain};`;
58
+ });
59
+ }, []);
60
+
61
+ const resetToEnglish = useCallback(() => {
62
+ if (typeof window === "undefined") return;
63
+ // 1. Check if a translation is actually active via cookie
64
+ const googtrans = document.cookie.split('; ').find(row => row.startsWith('googtrans='))?.split('=')[1];
65
+
66
+ // 2. Aggressive cookie clearing
67
+ const domains = [
68
+ window.location.hostname,
69
+ "." + window.location.hostname,
70
+ window.location.hostname.split('.').slice(-2).join('.'),
71
+ "." + window.location.hostname.split('.').slice(-2).join('.')
72
+ ];
73
+
74
+ const clear = (name: string) => {
75
+ document.cookie = `${name}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
76
+ domains.forEach(d => {
77
+ document.cookie = `${name}=; Path=/; Domain=${d}; Expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
78
+ });
79
+ };
80
+
81
+ clear("googtrans");
82
+
83
+ // 3. If we were translated, force a reload to get clean English
84
+ // We check googtrans existence OR the presence of Google's banner/classes
85
+ if (googtrans || document.documentElement.classList.contains("translated-ltr") || document.querySelector(".goog-te-banner-frame")) {
86
+ window.location.reload();
87
+ return;
88
+ }
89
+
90
+ // 4. UI Cleanup (for cases where reload isn't triggered)
91
+ document.body.style.top = "0";
92
+ document.documentElement.classList.remove("translated-ltr", "translated-rtl");
93
+ const container = document.getElementById(":1.container");
94
+ if (container) container.style.display = "none";
95
+ }, []);
96
+
97
+ const changeLanguage = useCallback((nextAppCode: string) => {
98
+ if (nextAppCode === "en") {
99
+ resetToEnglish();
100
+ return;
101
+ }
102
+
103
+ const googleCode = nextAppCode;
104
+
105
+ // Show container if hidden
106
+ const container = document.getElementById(":1.container");
107
+ if (container) container.style.display = "block";
108
+
109
+ let tries = 0;
110
+ const tick = () => {
111
+ if (fireChange(googleCode)) return;
112
+ if (tries++ < 30) setTimeout(tick, 200);
113
+ };
114
+ tick();
115
+ }, [resetToEnglish]);
116
+
117
+ // Load script only when a translation is requested
118
+ useEffect(() => {
119
+ if (typeof window === "undefined") return;
120
+ if (appLang === "en" && !initRef.current) return;
121
+ if (initRef.current) return;
122
+ if (supportedLangs.length === 0) return;
123
+
124
+ initRef.current = true;
125
+
126
+ if (!document.getElementById(ELEMENT_ID)) {
127
+ const host = document.createElement("div");
128
+ host.id = ELEMENT_ID;
129
+ host.style.display = "none";
130
+ document.body.appendChild(host);
131
+ }
132
+
133
+ const includedLanguages = supportedLangs.map((l) => l.code).join(",");
134
+
135
+ window.googleTranslateElementInit = () => {
136
+ if (window.google?.translate?.TranslateElement) {
137
+ new window.google.translate.TranslateElement(
138
+ { pageLanguage: "en", includedLanguages, autoDisplay: false },
139
+ ELEMENT_ID,
140
+ );
141
+ setIsLoaded(true);
142
+ }
143
+ };
144
+
145
+ if (!document.getElementById(SCRIPT_ID)) {
146
+ const script = document.createElement("script");
147
+ script.id = SCRIPT_ID;
148
+ script.src = "//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit";
149
+ script.async = true;
150
+ document.body.appendChild(script);
151
+ } else {
152
+ if (window.google?.translate) window.googleTranslateElementInit?.();
153
+ }
154
+ }, [appLang, supportedLangs]);
155
+
156
+ // Sync language changes
157
+ useEffect(() => {
158
+ if (isLoaded || (appLang === "en" && initRef.current)) {
159
+ changeLanguage(appLang);
160
+ }
161
+ }, [appLang, isLoaded, changeLanguage]);
162
+
163
+ return { isLoaded };
164
+ }
@@ -0,0 +1,9 @@
1
+ import { useEffect, useState } from 'react'
2
+
3
+ export function useIsMounted() {
4
+ const [mounted, setMounted] = useState(false)
5
+ useEffect(() => {
6
+ setMounted(true)
7
+ }, [])
8
+ return mounted
9
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Languages Hook
3
+ *
4
+ * React Query hook for fetching supported languages from the payment service.
5
+ * The query is disabled until the SDK is initialized so that no request fires
6
+ * before tenant credentials (businessId, storefrontApiKey) are available.
7
+ */
8
+ import { useQuery } from '@tanstack/react-query'
9
+ import { getCacheTier } from '../utils/cache'
10
+ import { getSupportedLanguages } from '../services/config.service'
11
+ import { isInitialized } from '../config'
12
+ import type { BackendLanguage, SupportedLang } from '../types/payments'
13
+
14
+ export async function getSupportedLanguagesList(): Promise<SupportedLang[]> {
15
+ const data = await getSupportedLanguages()
16
+ return data.languages.map((language: BackendLanguage) => ({
17
+ code: language.code,
18
+ appCode: language.code,
19
+ label: language.label,
20
+ native: language.label,
21
+ flag: language.flag,
22
+ }))
23
+ }
24
+
25
+ export function useSupportedLanguages() {
26
+ return useQuery({
27
+ queryKey: ['supported-languages'],
28
+ queryFn: () => getSupportedLanguagesList(),
29
+ enabled: isInitialized(),
30
+ retry: 1,
31
+ ...getCacheTier('high'),
32
+ })
33
+ }
@@ -0,0 +1,195 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef } from 'react';
4
+ import { fetchEventSource, EventSourceMessage } from '@microsoft/fetch-event-source';
5
+ import { getBackendUrl, getRuntimeConfig, isInitialized } from '../config';
6
+ import { logger } from '../utils';
7
+
8
+ interface PaymentSuccessData {
9
+ event: string;
10
+ data: {
11
+ transaction_id: string;
12
+ order_id: string;
13
+ amount: number;
14
+ currency: string;
15
+ status: string;
16
+ };
17
+ }
18
+
19
+ interface DigitalReadyData {
20
+ event: string;
21
+ data: {
22
+ order_id: string;
23
+ business_id: string;
24
+ customer_id: string;
25
+ download_links: Array<{
26
+ item_id: string;
27
+ product_name: string;
28
+ url: string;
29
+ token: string;
30
+ expires_at: string;
31
+ max_downloads: number;
32
+ }>;
33
+ fulfilled_at: string;
34
+ };
35
+ }
36
+
37
+ interface BespokeUpdateData {
38
+ event: string;
39
+ data: {
40
+ order_id: string;
41
+ business_id: string;
42
+ update_type: string;
43
+ message: string;
44
+ stage?: string;
45
+ updated_at: string;
46
+ };
47
+ }
48
+
49
+ interface ConsultationUpdateData {
50
+ event: string;
51
+ data: {
52
+ consultation_id: string;
53
+ business_id: string;
54
+ customer_id: string;
55
+ status: string;
56
+ message?: string;
57
+ updated_at: string;
58
+ };
59
+ }
60
+
61
+ interface ServiceBookingUpdateData {
62
+ event: string;
63
+ data: {
64
+ booking_id: string;
65
+ order_id: string;
66
+ business_id: string;
67
+ customer_id: string;
68
+ status: string;
69
+ appointment_date?: string;
70
+ updated_at: string;
71
+ };
72
+ }
73
+
74
+ interface UseOrderStreamOptions {
75
+ orderId: string;
76
+ onPaymentComplete?: (data: PaymentSuccessData) => void;
77
+ onDigitalReady?: (data: DigitalReadyData) => void;
78
+ onBespokeUpdate?: (data: BespokeUpdateData) => void;
79
+ onConsultationUpdate?: (data: ConsultationUpdateData) => void;
80
+ onServiceBookingUpdate?: (data: ServiceBookingUpdateData) => void;
81
+ onError?: (error: Error) => void;
82
+ }
83
+
84
+ export function useOrderStream({
85
+ orderId,
86
+ onPaymentComplete,
87
+ onDigitalReady,
88
+ onBespokeUpdate,
89
+ onConsultationUpdate,
90
+ onServiceBookingUpdate,
91
+ onError
92
+ }: UseOrderStreamOptions) {
93
+ const abortControllerRef = useRef<AbortController | null>(null);
94
+
95
+ useEffect(() => {
96
+ if (!orderId) {
97
+ return;
98
+ }
99
+
100
+ const abortController = new AbortController();
101
+ abortControllerRef.current = abortController;
102
+
103
+ const paymentUrl = getBackendUrl('payment');
104
+ const streamUrl = `${paymentUrl}/events/stream/order/${encodeURIComponent(orderId)}`;
105
+
106
+ fetchEventSource(streamUrl, {
107
+ method: 'GET',
108
+ signal: abortController.signal,
109
+ onmessage: (event: EventSourceMessage) => {
110
+ try {
111
+ if (!event.data || event.data.trim() === '') {
112
+ return;
113
+ }
114
+
115
+ const data = JSON.parse(event.data);
116
+ logger.log('[OrderStream] Received event:', data);
117
+
118
+ if (data.event === 'payment.success') {
119
+ logger.log('[OrderStream] Payment success detected, calling onPaymentComplete');
120
+ if (onPaymentComplete) {
121
+ onPaymentComplete(data as PaymentSuccessData);
122
+ }
123
+ abortController.abort();
124
+ }
125
+
126
+ if (data.event === 'digital.ready') {
127
+ logger.log('[OrderStream] Digital delivery ready detected, calling onDigitalReady');
128
+ if (onDigitalReady) {
129
+ onDigitalReady(data as DigitalReadyData);
130
+ }
131
+ }
132
+
133
+ if (data.event === 'bespoke.update') {
134
+ logger.log('[OrderStream] Bespoke update detected, calling onBespokeUpdate');
135
+ if (onBespokeUpdate) {
136
+ onBespokeUpdate(data as BespokeUpdateData);
137
+ }
138
+ }
139
+
140
+ if (data.event === 'consultation.update') {
141
+ logger.log('[OrderStream] Consultation update detected, calling onConsultationUpdate');
142
+ if (onConsultationUpdate) {
143
+ onConsultationUpdate(data as ConsultationUpdateData);
144
+ }
145
+ }
146
+
147
+ if (data.event === 'service_booking.update') {
148
+ logger.log('[OrderStream] Service booking update detected, calling onServiceBookingUpdate');
149
+ if (onServiceBookingUpdate) {
150
+ onServiceBookingUpdate(data as ServiceBookingUpdateData);
151
+ }
152
+ }
153
+ } catch (error) {
154
+ logger.error('[OrderStream] Error parsing event:', error);
155
+ }
156
+ },
157
+ onerror: (error: Error) => {
158
+ if (abortController.signal.aborted) {
159
+ logger.log('[OrderStream] Connection closed via AbortController');
160
+ return;
161
+ }
162
+ if (error.message?.includes('404') || error.message?.includes('Not Found')) {
163
+ logger.warn('[OrderStream] Stream endpoint unavailable for order:', orderId);
164
+ return;
165
+ }
166
+ logger.error('[OrderStream] Connection error:', error);
167
+ if (onError) {
168
+ onError(error);
169
+ }
170
+ },
171
+ onopen: async (response: Response) => {
172
+ if (response.ok && response.headers.get('content-type')?.includes('text/event-stream')) {
173
+ logger.log('[OrderStream] Connection established for order:', orderId);
174
+ return;
175
+ }
176
+ if (response.status === 404) {
177
+ logger.warn('[OrderStream] Stream endpoint unavailable for order:', orderId);
178
+ return;
179
+ }
180
+ throw new Error(`Unexpected response: ${response.status}`);
181
+ },
182
+ }).catch(error => {
183
+ if (abortController.signal.aborted || error.name === 'AbortError') {
184
+ return;
185
+ }
186
+ logger.error('[OrderStream] fetchEventSource failed:', error);
187
+ });
188
+
189
+ return () => {
190
+ abortController.abort();
191
+ };
192
+ }, [orderId, onPaymentComplete, onDigitalReady, onBespokeUpdate, onConsultationUpdate, onServiceBookingUpdate, onError]);
193
+
194
+ return null;
195
+ }