@qafka/react-native 2.5.0 → 2.6.0
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/CHANGELOG.md +18 -0
- package/dist/QafkaSDK.d.ts +13 -2
- package/dist/QafkaSDK.js +44 -6
- package/dist/components/ChatPage.d.ts +7 -1
- package/dist/components/ChatPage.js +19 -8
- package/dist/components/Qafka.js +5 -2
- package/dist/components/Qafka.types.d.ts +36 -15
- package/dist/hooks/useChatMessages.js +8 -0
- package/dist/hooks/useSDK.d.ts +9 -1
- package/dist/hooks/useSDK.js +15 -1
- package/dist/services/BackendService.d.ts +8 -1
- package/dist/services/BackendService.js +42 -8
- package/dist/services/projectUnavailable.d.ts +26 -0
- package/dist/services/projectUnavailable.js +46 -0
- package/dist/types/sdk.d.ts +5 -4
- package/docs/qafka-tool-authoring-guide.md +18 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.6.0] — 2026-09-15
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Unavailable state: when the assistant for a project is temporarily unavailable, the chat screen now shows a neutral, localized message ("The assistant is currently unavailable." / Turkish when `locale` starts with `tr`) instead of an error. The input is hidden and no error bubble or initialization banner is shown.
|
|
15
|
+
- New `onUnavailable` prop on `<Qafka />`, called when the assistant becomes unavailable (at start-up or mid-conversation). Use it to hide or disable your own entry point to the chat.
|
|
16
|
+
- `SDKStatusType` gains `'unavailable'`, delivered through `onStatusChange` and `QafkaSDK.getStatus()`.
|
|
17
|
+
- Localized greeting: when the `locale` prop is set, the project theme request now includes it, so the greeting and initial message are returned in that language when a translation exists. Without `locale` the request is unchanged.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- `QafkaSDK.initialize()` now waits for the project theme request to settle before resolving, so the final status (`'ready'` or `'unavailable'`) is known when it returns. `onError` is not called for the unavailable state.
|
|
22
|
+
- If you `switch` exhaustively over `SDKStatusType` in TypeScript, add a case for `'unavailable'`.
|
|
23
|
+
|
|
24
|
+
### Documentation
|
|
25
|
+
|
|
26
|
+
- Clarified how the `locale`, `context` and `contextDescription` props are used, with examples, and documented `onUnavailable` in the tool authoring guide.
|
|
27
|
+
|
|
10
28
|
## [2.5.0] — 2026-07-19
|
|
11
29
|
|
|
12
30
|
### Added
|
package/dist/QafkaSDK.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export declare class QafkaSDK {
|
|
|
13
13
|
private conversationManager;
|
|
14
14
|
private navigationHandler;
|
|
15
15
|
private themePrefetchPromise;
|
|
16
|
+
private themePrefetchError;
|
|
16
17
|
private constructor();
|
|
17
18
|
/**
|
|
18
19
|
* Get singleton instance
|
|
@@ -25,8 +26,9 @@ export declare class QafkaSDK {
|
|
|
25
26
|
/**
|
|
26
27
|
* developer-driven locale (BCP 47 — e.g. "tr", "en", "tr-TR").
|
|
27
28
|
* Forwarded to BackendService so it lands in `sdkContext.locale` on every
|
|
28
|
-
* outgoing chat request
|
|
29
|
-
*
|
|
29
|
+
* outgoing chat request — the language the assistant answers in. Explicit
|
|
30
|
+
* only: never derived from the device language, and it leaves the SDK's own
|
|
31
|
+
* UI strings untouched.
|
|
30
32
|
*
|
|
31
33
|
* Pass `null` to clear (e.g. when switching back to instructions-default).
|
|
32
34
|
* Calling before initialize() is a no-op; pass `locale` via SDKConfig instead.
|
|
@@ -155,6 +157,15 @@ export declare class QafkaSDK {
|
|
|
155
157
|
*/
|
|
156
158
|
getStatus(): SDKStatusType;
|
|
157
159
|
/**
|
|
160
|
+
* Internal — not part of the documented public surface. Flips the SDK to
|
|
161
|
+
* the 'unavailable' status mid-session (e.g. a chat request comes back
|
|
162
|
+
* with `code === 'PROJECT_UNAVAILABLE'` after the project was already
|
|
163
|
+
* initialized successfully). Notifies the same `onStatusChange` listener
|
|
164
|
+
* `initialize()` uses, so `useSDK` reacts identically regardless of
|
|
165
|
+
* whether unavailability was detected at init time or mid-session.
|
|
166
|
+
*/
|
|
167
|
+
markUnavailable(): void;
|
|
168
|
+
/**
|
|
158
169
|
* Get SDK configuration
|
|
159
170
|
*/
|
|
160
171
|
getConfig(): SDKConfig | null;
|
package/dist/QafkaSDK.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.getSDK = exports.QafkaSDK = void 0;
|
|
4
4
|
const storage_1 = require("./services/storage");
|
|
5
5
|
const BackendService_1 = require("./services/BackendService");
|
|
6
|
+
const projectUnavailable_1 = require("./services/projectUnavailable");
|
|
6
7
|
const AttestationManager_1 = require("./services/AttestationManager");
|
|
7
8
|
const ConversationManager_1 = require("./services/ConversationManager");
|
|
8
9
|
const NavigationHandler_1 = require("./services/NavigationHandler");
|
|
@@ -24,6 +25,11 @@ class QafkaSDK {
|
|
|
24
25
|
// ChatPage can render the branded greeting without waiting for SDK ready.
|
|
25
26
|
// See `useProjectTheme` which awaits this promise.
|
|
26
27
|
themePrefetchPromise = null;
|
|
28
|
+
// Captured alongside themePrefetchPromise (which swallows its rejection so
|
|
29
|
+
// useProjectTheme always gets a settled promise). initialize() inspects
|
|
30
|
+
// this after the prefetch settles to decide whether the final status is
|
|
31
|
+
// 'ready' or 'unavailable'.
|
|
32
|
+
themePrefetchError = null;
|
|
27
33
|
constructor() { }
|
|
28
34
|
/**
|
|
29
35
|
* Get singleton instance
|
|
@@ -78,7 +84,10 @@ class QafkaSDK {
|
|
|
78
84
|
// needs are not ready yet — firing the prefetch now would fail, blank out
|
|
79
85
|
// the greeting, and need a retry. Deferred to after init below.
|
|
80
86
|
if (apiKey) {
|
|
81
|
-
this.themePrefetchPromise = this.prefetchAndCacheTheme().catch(() =>
|
|
87
|
+
this.themePrefetchPromise = this.prefetchAndCacheTheme().catch((err) => {
|
|
88
|
+
this.themePrefetchError = err;
|
|
89
|
+
return null;
|
|
90
|
+
});
|
|
82
91
|
}
|
|
83
92
|
// Save config to storage
|
|
84
93
|
await storage_1.storage.setItem('@qafka/config', JSON.stringify(config));
|
|
@@ -103,7 +112,10 @@ class QafkaSDK {
|
|
|
103
112
|
// credentials it needs and the prefetch will succeed on first try,
|
|
104
113
|
// hydrating useProjectTheme before the chat surface paints.
|
|
105
114
|
if (!apiKey && !this.themePrefetchPromise) {
|
|
106
|
-
this.themePrefetchPromise = this.prefetchAndCacheTheme().catch(() =>
|
|
115
|
+
this.themePrefetchPromise = this.prefetchAndCacheTheme().catch((err) => {
|
|
116
|
+
this.themePrefetchError = err;
|
|
117
|
+
return null;
|
|
118
|
+
});
|
|
107
119
|
}
|
|
108
120
|
}
|
|
109
121
|
catch (error) {
|
|
@@ -119,9 +131,19 @@ class QafkaSDK {
|
|
|
119
131
|
throw error;
|
|
120
132
|
}
|
|
121
133
|
}
|
|
122
|
-
|
|
134
|
+
// Decide the final status after the theme prefetch settles — a
|
|
135
|
+
// ProjectUnavailableError there means the project is suspended, which
|
|
136
|
+
// takes priority over an otherwise-successful attestation. This never
|
|
137
|
+
// rejects: the prefetch promise already swallows its own rejection
|
|
138
|
+
// (see themePrefetchError above).
|
|
139
|
+
if (this.themePrefetchPromise) {
|
|
140
|
+
await this.themePrefetchPromise;
|
|
141
|
+
}
|
|
142
|
+
this.status = this.themePrefetchError instanceof projectUnavailable_1.ProjectUnavailableError
|
|
143
|
+
? 'unavailable'
|
|
144
|
+
: 'ready';
|
|
123
145
|
if (config.onStatusChange) {
|
|
124
|
-
config.onStatusChange(
|
|
146
|
+
config.onStatusChange(this.status);
|
|
125
147
|
}
|
|
126
148
|
}
|
|
127
149
|
catch (error) {
|
|
@@ -146,8 +168,9 @@ class QafkaSDK {
|
|
|
146
168
|
/**
|
|
147
169
|
* developer-driven locale (BCP 47 — e.g. "tr", "en", "tr-TR").
|
|
148
170
|
* Forwarded to BackendService so it lands in `sdkContext.locale` on every
|
|
149
|
-
* outgoing chat request
|
|
150
|
-
*
|
|
171
|
+
* outgoing chat request — the language the assistant answers in. Explicit
|
|
172
|
+
* only: never derived from the device language, and it leaves the SDK's own
|
|
173
|
+
* UI strings untouched.
|
|
151
174
|
*
|
|
152
175
|
* Pass `null` to clear (e.g. when switching back to instructions-default).
|
|
153
176
|
* Calling before initialize() is a no-op; pass `locale` via SDKConfig instead.
|
|
@@ -444,6 +467,20 @@ class QafkaSDK {
|
|
|
444
467
|
return this.status;
|
|
445
468
|
}
|
|
446
469
|
/**
|
|
470
|
+
* Internal — not part of the documented public surface. Flips the SDK to
|
|
471
|
+
* the 'unavailable' status mid-session (e.g. a chat request comes back
|
|
472
|
+
* with `code === 'PROJECT_UNAVAILABLE'` after the project was already
|
|
473
|
+
* initialized successfully). Notifies the same `onStatusChange` listener
|
|
474
|
+
* `initialize()` uses, so `useSDK` reacts identically regardless of
|
|
475
|
+
* whether unavailability was detected at init time or mid-session.
|
|
476
|
+
*/
|
|
477
|
+
markUnavailable() {
|
|
478
|
+
this.status = 'unavailable';
|
|
479
|
+
if (this.config?.onStatusChange) {
|
|
480
|
+
this.config.onStatusChange('unavailable');
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
447
484
|
* Get SDK configuration
|
|
448
485
|
*/
|
|
449
486
|
getConfig() {
|
|
@@ -480,6 +517,7 @@ class QafkaSDK {
|
|
|
480
517
|
this.subProjectId = null;
|
|
481
518
|
this.config = null;
|
|
482
519
|
this.themePrefetchPromise = null;
|
|
520
|
+
this.themePrefetchError = null;
|
|
483
521
|
this.status = 'uninitialized';
|
|
484
522
|
// Only clear the shared singleton slot when WE are still the current
|
|
485
523
|
// instance. An outgoing component unmounting must not null a newer
|
|
@@ -19,6 +19,12 @@ export interface ChatPageProps {
|
|
|
19
19
|
} | null;
|
|
20
20
|
sdkReady: boolean;
|
|
21
21
|
sdkError: string | null;
|
|
22
|
+
/** True when the project is unavailable (suspended). Replaces the whole
|
|
23
|
+
* chat surface (messages, composer, error banner) with a neutral, centered
|
|
24
|
+
* message instead. */
|
|
25
|
+
unavailable?: boolean;
|
|
26
|
+
/** Forwarded to `unavailableMessage()` for the neutral copy above. */
|
|
27
|
+
locale?: string;
|
|
22
28
|
theme: Theme;
|
|
23
29
|
handleSend: (text: string) => void;
|
|
24
30
|
handleActionClick: (action: any) => void;
|
|
@@ -41,4 +47,4 @@ export interface ChatPageProps {
|
|
|
41
47
|
cardHostCallbacks?: import('../cards/cta/types').CardCTAHostCallbacks;
|
|
42
48
|
cardLifecycle?: import('../cards/cta/types').CardCTALifecycle;
|
|
43
49
|
}
|
|
44
|
-
export declare function ChatPage({ displayMessages, isTyping, isSending, streamingMessage, toolStatus, sdkReady, sdkError, theme, handleSend, handleActionClick, handleExternalSuggestionPress, flatListRef, placeholder, maxMessageLength, showTimestamps, greetingMessage, apiGreeting, isInitialMessageEnabled, initialMessageText, componentRegistry, onBack, onClose, BackComponent, CloseComponent, NavigationButtonComponent, cardHostCallbacks, cardLifecycle, }: ChatPageProps): React.JSX.Element;
|
|
50
|
+
export declare function ChatPage({ displayMessages, isTyping, isSending, streamingMessage, toolStatus, sdkReady, sdkError, unavailable, locale, theme, handleSend, handleActionClick, handleExternalSuggestionPress, flatListRef, placeholder, maxMessageLength, showTimestamps, greetingMessage, apiGreeting, isInitialMessageEnabled, initialMessageText, componentRegistry, onBack, onClose, BackComponent, CloseComponent, NavigationButtonComponent, cardHostCallbacks, cardLifecycle, }: ChatPageProps): React.JSX.Element;
|
|
@@ -11,7 +11,8 @@ const InputArea_1 = require("./InputArea");
|
|
|
11
11
|
const TypingIndicator_1 = require("./TypingIndicator");
|
|
12
12
|
const ToolStatusPill_1 = require("./ToolStatusPill");
|
|
13
13
|
const Qafka_styles_1 = require("./Qafka.styles");
|
|
14
|
-
|
|
14
|
+
const projectUnavailable_1 = require("../services/projectUnavailable");
|
|
15
|
+
function ChatPage({ displayMessages, isTyping, isSending, streamingMessage, toolStatus, sdkReady, sdkError, unavailable, locale, theme, handleSend, handleActionClick, handleExternalSuggestionPress, flatListRef, placeholder, maxMessageLength, showTimestamps, greetingMessage, apiGreeting, isInitialMessageEnabled, initialMessageText, componentRegistry, onBack, onClose, BackComponent, CloseComponent, NavigationButtonComponent, cardHostCallbacks, cardLifecycle, }) {
|
|
15
16
|
const styles = (0, Qafka_styles_1.getWidgetStyles)(theme);
|
|
16
17
|
const renderMessage = ({ item }) => (<MessageBubble_1.MessageBubble message={item.text || item.content} role={item.role === 'system' ? 'assistant' : item.role} timestamp={new Date(item.timestamp)} theme={theme} showTimestamp={showTimestamps} actions={item.actions} onActionPress={handleActionClick} componentRegistry={componentRegistry} toolDefinition={item.toolResponse?.tool} toolResultData={item.toolResponse?.data} NavigationButtonComponent={NavigationButtonComponent} actionResults={item.actionResults} completedSteps={item.completedSteps} externalSuggestions={item.externalSuggestions} onExternalPress={handleExternalSuggestionPress} card={item.card} cardHostCallbacks={cardHostCallbacks} cardLifecycle={cardLifecycle}/>);
|
|
17
18
|
const renderEmpty = () => {
|
|
@@ -54,6 +55,15 @@ function ChatPage({ displayMessages, isTyping, isSending, streamingMessage, tool
|
|
|
54
55
|
</react_native_1.View>
|
|
55
56
|
</react_native_1.View>)}
|
|
56
57
|
|
|
58
|
+
{unavailable ? (
|
|
59
|
+
/* Project unavailable (suspended): replace the whole chat surface —
|
|
60
|
+
messages, composer, error banner — with a neutral, centered message.
|
|
61
|
+
No "Something went wrong" bubble, no input, no typing indicator. */
|
|
62
|
+
<react_native_1.View style={[styles.emptyStateStyle, { flex: 1 }]}>
|
|
63
|
+
<react_native_1.Text style={styles.emptyStateTextStyle}>
|
|
64
|
+
{(0, projectUnavailable_1.unavailableMessage)(locale)}
|
|
65
|
+
</react_native_1.Text>
|
|
66
|
+
</react_native_1.View>) : (<>
|
|
57
67
|
{/* Error banner */}
|
|
58
68
|
{sdkError && (<react_native_1.View style={styles.errorStyle}>
|
|
59
69
|
<react_native_1.Text style={styles.errorTextStyle}>{sdkError}</react_native_1.Text>
|
|
@@ -63,10 +73,10 @@ function ChatPage({ displayMessages, isTyping, isSending, streamingMessage, tool
|
|
|
63
73
|
<react_native_1.FlatList ref={flatListRef} data={displayMessages} renderItem={renderMessage} keyExtractor={(item) => item.id} style={{ flex: 1, flexGrow: 1, flexShrink: 1 }} contentContainerStyle={styles.messageListStyle} ListEmptyComponent={renderEmpty} showsVerticalScrollIndicator={false} keyboardShouldPersistTaps="handled" ListFooterComponent={toolStatus ? (<react_native_1.View style={{ paddingHorizontal: 8, paddingTop: 4, alignItems: 'flex-start' }}>
|
|
64
74
|
<ToolStatusPill_1.ToolStatusPill message={toolStatus.message} theme={theme}/>
|
|
65
75
|
</react_native_1.View>) : null} onContentSizeChange={() => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
if (displayMessages.length > 0) {
|
|
77
|
+
flatListRef.current?.scrollToEnd({ animated: true });
|
|
78
|
+
}
|
|
79
|
+
}}/>
|
|
70
80
|
|
|
71
81
|
{/* Typing Indicator */}
|
|
72
82
|
{isTyping && !streamingMessage && (<react_native_1.View style={styles.typingIndicatorContainer}>
|
|
@@ -74,11 +84,12 @@ function ChatPage({ displayMessages, isTyping, isSending, streamingMessage, tool
|
|
|
74
84
|
</react_native_1.View>)}
|
|
75
85
|
|
|
76
86
|
{/* Tool Status Pill now renders inside the list (ListFooterComponent)
|
|
77
|
-
|
|
87
|
+
so it sits directly under the last message and scrolls with it. */}
|
|
78
88
|
|
|
79
89
|
{/* Input Area — hidden until SDK is ready so users can't type a message
|
|
80
|
-
|
|
81
|
-
|
|
90
|
+
that would be rejected by an un-attested send. TypingIndicator in the
|
|
91
|
+
empty state carries the "still preparing" signal. */}
|
|
82
92
|
{sdkReady && (<InputArea_1.InputArea theme={theme} placeholder={placeholder} onSend={handleSend} isSending={isSending} maxLength={maxMessageLength}/>)}
|
|
93
|
+
</>)}
|
|
83
94
|
</>);
|
|
84
95
|
}
|
package/dist/components/Qafka.js
CHANGED
|
@@ -87,10 +87,10 @@ const { width: SCREEN_WIDTH } = react_native_1.Dimensions.get('window');
|
|
|
87
87
|
* />
|
|
88
88
|
* ```
|
|
89
89
|
*/
|
|
90
|
-
exports.Qafka = (0, react_1.forwardRef)(function Qafka({ style, apiUrl: apiUrlProp, devConfig, subProjectId, projectId, locale, mode = 'fullscreen', theme: themeName = 'light', customTheme, themeOverride, isAuthenticated, endUserId, endUserData, enableStreaming = true, voiceEnabled: voiceEnabledProp = true, context: userContext, contextDescription, components: customComponents, showTimestamps = true, placeholder = 'Type a message...', maxMessageLength = 500, greetingMessage, onReady, onMessageSent, onResponseReceived, onError, onNavigationSuggest, onNavigationAction, onExternalSuggestion, onCardDeepLink, onCardSuggestMessage, onCardExternalNavigation, onCardShare, onCardCopy, onCardToolTrigger, onCardCTAClick, onToolSuggested, onToolDataRequested, onActionResult, onStepCompleted, onFileUploadRequest, onExtractionResult, onClose, onBack, CloseComponent, BackComponent, navigationLabelFormat, NavigationButtonComponent, voiceComponents, voiceTranscript = 'centered', toolRenderMode, }, ref) {
|
|
90
|
+
exports.Qafka = (0, react_1.forwardRef)(function Qafka({ style, apiUrl: apiUrlProp, devConfig, subProjectId, projectId, locale, mode = 'fullscreen', theme: themeName = 'light', customTheme, themeOverride, isAuthenticated, endUserId, endUserData, enableStreaming = true, voiceEnabled: voiceEnabledProp = true, context: userContext, contextDescription, components: customComponents, showTimestamps = true, placeholder = 'Type a message...', maxMessageLength = 500, greetingMessage, onReady, onMessageSent, onResponseReceived, onError, onUnavailable, onNavigationSuggest, onNavigationAction, onExternalSuggestion, onCardDeepLink, onCardSuggestMessage, onCardExternalNavigation, onCardShare, onCardCopy, onCardToolTrigger, onCardCTAClick, onToolSuggested, onToolDataRequested, onActionResult, onStepCompleted, onFileUploadRequest, onExtractionResult, onClose, onBack, CloseComponent, BackComponent, navigationLabelFormat, NavigationButtonComponent, voiceComponents, voiceTranscript = 'centered', toolRenderMode, }, ref) {
|
|
91
91
|
// === ALL HOOKS FIRST (Rules of Hooks compliance) ===
|
|
92
92
|
const insets = (0, react_native_safe_area_context_1.useSafeAreaInsets)();
|
|
93
|
-
const { sdkReady, error: sdkError, resolvedApiKey, resolvedApiUrl } = (0, useSDK_1.useSDK)({
|
|
93
|
+
const { sdkReady, unavailable, error: sdkError, resolvedApiKey, resolvedApiUrl } = (0, useSDK_1.useSDK)({
|
|
94
94
|
apiUrl: apiUrlProp,
|
|
95
95
|
devConfig,
|
|
96
96
|
subProjectId,
|
|
@@ -98,6 +98,7 @@ exports.Qafka = (0, react_1.forwardRef)(function Qafka({ style, apiUrl: apiUrlPr
|
|
|
98
98
|
locale,
|
|
99
99
|
onReady,
|
|
100
100
|
onError,
|
|
101
|
+
onUnavailable,
|
|
101
102
|
});
|
|
102
103
|
const { theme, isLoadingTheme, greeting: apiGreeting, initialMessage: initialMessageText, isInitialMessageEnabled, voiceEnabled, } = (0, useProjectTheme_1.useProjectTheme)({
|
|
103
104
|
sdkReady,
|
|
@@ -432,6 +433,8 @@ exports.Qafka = (0, react_1.forwardRef)(function Qafka({ style, apiUrl: apiUrlPr
|
|
|
432
433
|
toolStatus,
|
|
433
434
|
sdkReady,
|
|
434
435
|
sdkError,
|
|
436
|
+
unavailable,
|
|
437
|
+
locale,
|
|
435
438
|
theme,
|
|
436
439
|
handleSend,
|
|
437
440
|
handleActionClick,
|
|
@@ -283,14 +283,14 @@ export interface QafkaProps {
|
|
|
283
283
|
/**
|
|
284
284
|
* BCP 47 locale (e.g. "tr", "en", "tr-TR").
|
|
285
285
|
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
* another language.
|
|
286
|
+
* The language your app declares the user speaks. Forwarded to the backend
|
|
287
|
+
* as `sdkContext.locale`: when set, the assistant answers in this language
|
|
288
|
+
* unless the project's own instructions demand another one. It does not
|
|
289
|
+
* change the SDK's built-in UI strings.
|
|
291
290
|
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
291
|
+
* Explicit only — the SDK never reads the device language for this. Leave
|
|
292
|
+
* undefined when your app has no language choice of its own, and the
|
|
293
|
+
* project's configured language decides.
|
|
294
294
|
*
|
|
295
295
|
* @example "tr"
|
|
296
296
|
* @example "en-US"
|
|
@@ -364,16 +364,29 @@ export interface QafkaProps {
|
|
|
364
364
|
*/
|
|
365
365
|
endUserData?: Record<string, unknown>;
|
|
366
366
|
/**
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
367
|
+
* Runtime facts the assistant may use to personalize answers: the signed-in
|
|
368
|
+
* user's first name, their selected store or branch, account state, an
|
|
369
|
+
* active promotion's progress — whatever the AI should know about THIS user
|
|
370
|
+
* right now.
|
|
371
|
+
*
|
|
372
|
+
* Every key is visible to the AI (flattened into the prompt as `key: value`
|
|
373
|
+
* lines) and available to tool parameters with `source: 'context'`. Because
|
|
374
|
+
* these keys DO reach the prompt, identity data the AI should not see
|
|
375
|
+
* belongs in `endUserData` instead — that lane is never sent to the LLM.
|
|
376
|
+
*
|
|
377
|
+
* When a value has no meaningful state, send an explicit "empty" shape
|
|
378
|
+
* rather than omitting the key — an absent key gives the AI nothing to
|
|
379
|
+
* reason about, so it answers "I don't know".
|
|
380
|
+
*
|
|
381
|
+
* @example { firstName: 'Ada', selectedStoreId: '123', activeSession: { active: false } }
|
|
382
|
+
*/
|
|
371
383
|
context?: Record<string, any>;
|
|
372
384
|
/**
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
385
|
+
* One human-readable sentence describing the screen the user is currently
|
|
386
|
+
* on. Kept separate from `context`: it describes the user's LOCATION, not a
|
|
387
|
+
* fact about them, and the prompt places it above the context lines.
|
|
388
|
+
* @example "User is on the settings screen"
|
|
389
|
+
*/
|
|
377
390
|
contextDescription?: string;
|
|
378
391
|
/**
|
|
379
392
|
* Custom component registry for Tool Registry responses.
|
|
@@ -422,6 +435,14 @@ export interface QafkaProps {
|
|
|
422
435
|
*/
|
|
423
436
|
onError?: (error: Error) => void;
|
|
424
437
|
/**
|
|
438
|
+
* Callback when the project becomes unavailable (suspended). The chat
|
|
439
|
+
* screen stays mounted and shows a neutral message instead of the normal
|
|
440
|
+
* chat surface; use this to hide your own entry point (launcher button,
|
|
441
|
+
* nav item, ...) if you don't want users opening it in this state. Not
|
|
442
|
+
* treated as an error — `onError` is not also called for this transition.
|
|
443
|
+
*/
|
|
444
|
+
onUnavailable?: () => void;
|
|
445
|
+
/**
|
|
425
446
|
* Callback when navigation is suggested (always triggered)
|
|
426
447
|
*/
|
|
427
448
|
onNavigationSuggest?: (suggestion: NavigationSuggestion) => void;
|
|
@@ -534,6 +534,14 @@ const useChatMessages = ({ enableStreaming = true, context = {}, contextDescript
|
|
|
534
534
|
setIsSending(false);
|
|
535
535
|
setToolStatus(null);
|
|
536
536
|
finalChunkBufferRef.current = '';
|
|
537
|
+
// Project suspended mid-session: flip the SDK to 'unavailable' instead of
|
|
538
|
+
// treating this as a normal error. QafkaSDK.markUnavailable() notifies the
|
|
539
|
+
// same onStatusChange listener useSDK subscribes to, so `onUnavailable`
|
|
540
|
+
// fires there — no onError call and no error bubble here.
|
|
541
|
+
if (err?.code === 'PROJECT_UNAVAILABLE') {
|
|
542
|
+
QafkaSDK_1.QafkaSDK.getInstance().markUnavailable();
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
537
545
|
const errorMessage = err.message || 'Unknown error';
|
|
538
546
|
onError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
539
547
|
// Default fallback bubble. Apps that want a localized or custom error
|
package/dist/hooks/useSDK.d.ts
CHANGED
|
@@ -10,10 +10,18 @@ export interface UseSDKOptions {
|
|
|
10
10
|
locale?: string;
|
|
11
11
|
onReady?: () => void;
|
|
12
12
|
onError?: (error: Error) => void;
|
|
13
|
+
/**
|
|
14
|
+
* Fired when the project is unavailable (suspended). Mutually exclusive
|
|
15
|
+
* with `onError`/`onReady` for this transition — the SDK treats
|
|
16
|
+
* unavailability as an expected, recoverable state rather than a failure.
|
|
17
|
+
*/
|
|
18
|
+
onUnavailable?: () => void;
|
|
13
19
|
devConfig?: unknown;
|
|
14
20
|
}
|
|
15
21
|
export interface UseSDKResult {
|
|
16
22
|
sdkReady: boolean;
|
|
23
|
+
/** True once the SDK has determined the project is unavailable (suspended). */
|
|
24
|
+
unavailable: boolean;
|
|
17
25
|
error: string | null;
|
|
18
26
|
/**
|
|
19
27
|
* Dev-only: the resolved developmentKey from the consumer dev config — the
|
|
@@ -31,4 +39,4 @@ export interface UseSDKResult {
|
|
|
31
39
|
/**
|
|
32
40
|
* Custom hook for managing SDK initialization and lifecycle
|
|
33
41
|
*/
|
|
34
|
-
export declare const useSDK: ({ apiUrl, subProjectId, projectId, locale, onReady, onError, devConfig, }: UseSDKOptions) => UseSDKResult;
|
|
42
|
+
export declare const useSDK: ({ apiUrl, subProjectId, projectId, locale, onReady, onError, onUnavailable, devConfig, }: UseSDKOptions) => UseSDKResult;
|
package/dist/hooks/useSDK.js
CHANGED
|
@@ -9,15 +9,18 @@ const resolve_runtime_config_source_1 = require("../resolve-runtime-config-sourc
|
|
|
9
9
|
/**
|
|
10
10
|
* Custom hook for managing SDK initialization and lifecycle
|
|
11
11
|
*/
|
|
12
|
-
const useSDK = ({ apiUrl, subProjectId, projectId, locale, onReady, onError, devConfig, }) => {
|
|
12
|
+
const useSDK = ({ apiUrl, subProjectId, projectId, locale, onReady, onError, onUnavailable, devConfig, }) => {
|
|
13
13
|
const [sdkReady, setSdkReady] = (0, react_1.useState)(false);
|
|
14
|
+
const [unavailable, setUnavailable] = (0, react_1.useState)(false);
|
|
14
15
|
const [error, setError] = (0, react_1.useState)(null);
|
|
15
16
|
const [resolvedApiKey, setResolvedApiKey] = (0, react_1.useState)(null);
|
|
16
17
|
const [resolvedApiUrl, setResolvedApiUrl] = (0, react_1.useState)(apiUrl);
|
|
17
18
|
const onReadyRef = (0, react_1.useRef)(onReady);
|
|
18
19
|
const onErrorRef = (0, react_1.useRef)(onError);
|
|
20
|
+
const onUnavailableRef = (0, react_1.useRef)(onUnavailable);
|
|
19
21
|
onReadyRef.current = onReady;
|
|
20
22
|
onErrorRef.current = onError;
|
|
23
|
+
onUnavailableRef.current = onUnavailable;
|
|
21
24
|
const initializeSDK = async () => {
|
|
22
25
|
if (!projectId) {
|
|
23
26
|
const err = new Error('Qafka: `projectId` prop is required.');
|
|
@@ -54,9 +57,17 @@ const useSDK = ({ apiUrl, subProjectId, projectId, locale, onReady, onError, dev
|
|
|
54
57
|
locale,
|
|
55
58
|
onStatusChange: (status) => {
|
|
56
59
|
if (status === 'ready') {
|
|
60
|
+
setUnavailable(false);
|
|
57
61
|
setSdkReady(true);
|
|
58
62
|
onReadyRef.current?.();
|
|
59
63
|
}
|
|
64
|
+
else if (status === 'unavailable') {
|
|
65
|
+
// Unavailability is an expected, recoverable state — not a
|
|
66
|
+
// failure — so no onError/sdkError here, only onUnavailable.
|
|
67
|
+
setSdkReady(false);
|
|
68
|
+
setUnavailable(true);
|
|
69
|
+
onUnavailableRef.current?.();
|
|
70
|
+
}
|
|
60
71
|
else if (status === 'error') {
|
|
61
72
|
setError('Failed to initialize SDK');
|
|
62
73
|
onErrorRef.current?.(new Error('SDK initialization failed'));
|
|
@@ -82,10 +93,12 @@ const useSDK = ({ apiUrl, subProjectId, projectId, locale, onReady, onError, dev
|
|
|
82
93
|
(0, react_1.useEffect)(() => {
|
|
83
94
|
const sdk = QafkaSDK_1.QafkaSDK.getInstance();
|
|
84
95
|
setSdkReady(false);
|
|
96
|
+
setUnavailable(false);
|
|
85
97
|
setError(null);
|
|
86
98
|
initializeSDK();
|
|
87
99
|
return () => {
|
|
88
100
|
setSdkReady(false);
|
|
101
|
+
setUnavailable(false);
|
|
89
102
|
sdk.destroy();
|
|
90
103
|
};
|
|
91
104
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -99,6 +112,7 @@ const useSDK = ({ apiUrl, subProjectId, projectId, locale, onReady, onError, dev
|
|
|
99
112
|
}, [locale, sdkReady]);
|
|
100
113
|
return {
|
|
101
114
|
sdkReady,
|
|
115
|
+
unavailable,
|
|
102
116
|
error,
|
|
103
117
|
resolvedApiKey,
|
|
104
118
|
resolvedApiUrl,
|
|
@@ -103,7 +103,14 @@ export declare class BackendService {
|
|
|
103
103
|
*/
|
|
104
104
|
deleteConversation(sessionId: string): Promise<void>;
|
|
105
105
|
/**
|
|
106
|
-
* Get project theme
|
|
106
|
+
* Get project theme.
|
|
107
|
+
*
|
|
108
|
+
* Sends the explicit locale, when the host set one, so the server can answer
|
|
109
|
+
* with its translation of the project's greeting / initial message instead of
|
|
110
|
+
* the language they were authored in. Same source as `sdkContext.locale`: the
|
|
111
|
+
* `<Qafka locale>` prop only, never a device language. Unset ⇒ no param ⇒ the
|
|
112
|
+
* request is identical to the one earlier SDK versions send, and the server
|
|
113
|
+
* reads a missing param as "no signal".
|
|
107
114
|
*/
|
|
108
115
|
getTheme(): Promise<any>;
|
|
109
116
|
/**
|
|
@@ -4,6 +4,7 @@ exports.BackendService = void 0;
|
|
|
4
4
|
const react_native_1 = require("react-native");
|
|
5
5
|
const package_json_1 = require("../../package.json");
|
|
6
6
|
const QafkaAttestation_1 = require("../native/QafkaAttestation");
|
|
7
|
+
const projectUnavailable_1 = require("./projectUnavailable");
|
|
7
8
|
/**
|
|
8
9
|
* Backend API Service using native fetch
|
|
9
10
|
*/
|
|
@@ -25,8 +26,10 @@ class BackendService {
|
|
|
25
26
|
attestationManager = null;
|
|
26
27
|
refreshing = null;
|
|
27
28
|
// locale is only added to sdkContext when explicitly set by the
|
|
28
|
-
// developer via <Qafka locale="..." />.
|
|
29
|
-
//
|
|
29
|
+
// developer via <Qafka locale="..." />. There is no device/system-language
|
|
30
|
+
// fallback on purpose: the server reads this field as the language the USER
|
|
31
|
+
// writes in, and a device default would state that for every host app that
|
|
32
|
+
// never made the choice. The SDK's own UI strings are unaffected by it.
|
|
30
33
|
locale = null;
|
|
31
34
|
// Tool Data Channel
|
|
32
35
|
onToolDataRequested = null;
|
|
@@ -273,6 +276,12 @@ class BackendService {
|
|
|
273
276
|
const errorCode = errorData.error || 'UNKNOWN_ERROR';
|
|
274
277
|
const errorMessage = errorData.message || response.statusText;
|
|
275
278
|
const hint = errorData.hint || '';
|
|
279
|
+
// Project suspended — a distinct signal from the other 403 branches
|
|
280
|
+
// below, callers across the SDK branch on `ProjectUnavailableError` /
|
|
281
|
+
// `code === 'PROJECT_UNAVAILABLE'` rather than parsing message strings.
|
|
282
|
+
if ((0, projectUnavailable_1.isProjectUnavailable)(response.status, errorData)) {
|
|
283
|
+
throw new projectUnavailable_1.ProjectUnavailableError(errorMessage);
|
|
284
|
+
}
|
|
276
285
|
// 401: Unauthorized (Invalid/Missing API Key)
|
|
277
286
|
if (response.status === 401) {
|
|
278
287
|
if (errorCode === 'MISSING_API_KEY') {
|
|
@@ -399,18 +408,35 @@ class BackendService {
|
|
|
399
408
|
}
|
|
400
409
|
}
|
|
401
410
|
/**
|
|
402
|
-
* Get project theme
|
|
411
|
+
* Get project theme.
|
|
412
|
+
*
|
|
413
|
+
* Sends the explicit locale, when the host set one, so the server can answer
|
|
414
|
+
* with its translation of the project's greeting / initial message instead of
|
|
415
|
+
* the language they were authored in. Same source as `sdkContext.locale`: the
|
|
416
|
+
* `<Qafka locale>` prop only, never a device language. Unset ⇒ no param ⇒ the
|
|
417
|
+
* request is identical to the one earlier SDK versions send, and the server
|
|
418
|
+
* reads a missing param as "no signal".
|
|
403
419
|
*/
|
|
404
420
|
async getTheme() {
|
|
405
421
|
try {
|
|
406
422
|
const cleanApiUrl = this.baseURL.replace(/\/api\/v1\/?$/, '');
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
423
|
+
const query = [];
|
|
424
|
+
if (this.subProjectId) {
|
|
425
|
+
query.push(`subProjectId=${encodeURIComponent(this.subProjectId)}`);
|
|
426
|
+
}
|
|
427
|
+
if (this.locale) {
|
|
428
|
+
query.push(`locale=${encodeURIComponent(this.locale)}`);
|
|
429
|
+
}
|
|
430
|
+
const queryParams = query.length > 0 ? `?${query.join('&')}` : '';
|
|
410
431
|
const response = await this.fetchWithTimeout(`${cleanApiUrl}/api/v1/public/projects/chat-theme${queryParams}`, { method: 'GET' });
|
|
411
432
|
return this.handleResponse(response);
|
|
412
433
|
}
|
|
413
434
|
catch (error) {
|
|
435
|
+
// Preserve ProjectUnavailableError as-is — callers (QafkaSDK.initialize)
|
|
436
|
+
// branch on `instanceof`, so re-wrapping here would lose that signal.
|
|
437
|
+
if (error instanceof projectUnavailable_1.ProjectUnavailableError) {
|
|
438
|
+
throw error;
|
|
439
|
+
}
|
|
414
440
|
throw new Error(error.message || 'Failed to fetch theme');
|
|
415
441
|
}
|
|
416
442
|
}
|
|
@@ -645,13 +671,21 @@ class BackendService {
|
|
|
645
671
|
});
|
|
646
672
|
}
|
|
647
673
|
else {
|
|
648
|
-
let
|
|
674
|
+
let parsedBody;
|
|
649
675
|
try {
|
|
650
|
-
|
|
676
|
+
parsedBody = JSON.parse(xhr.responseText);
|
|
651
677
|
}
|
|
652
678
|
catch {
|
|
653
679
|
// non-JSON error body; fall through to a status-based message
|
|
654
680
|
}
|
|
681
|
+
const serverMessage = parsedBody?.message;
|
|
682
|
+
if ((0, projectUnavailable_1.isProjectUnavailable)(xhr.status, parsedBody)) {
|
|
683
|
+
const unavailableErr = new Error(serverMessage || 'The assistant is currently unavailable.');
|
|
684
|
+
unavailableErr.code = 'PROJECT_UNAVAILABLE';
|
|
685
|
+
unavailableErr.status = xhr.status;
|
|
686
|
+
onError(unavailableErr);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
655
689
|
// In dev, a 401 with a test key almost always means the rotated dev
|
|
656
690
|
// key is stale. Surface an actionable hint instead of a bare HTTP error.
|
|
657
691
|
if (__DEV__ && xhr.status === 401 && this.apiKey?.startsWith('qafka_test_')) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the "project unavailable" state: the backend answers
|
|
3
|
+
* with HTTP 403 `{ error: 'project_unavailable' }` when the project (or its
|
|
4
|
+
* parent) is suspended. The chat screen stays mounted but shows a neutral
|
|
5
|
+
* message instead of the normal chat surface or an error bubble.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* True when a response is the specific "project unavailable" signal, as
|
|
9
|
+
* opposed to any other 403 (bundle id, platform, permission, ...).
|
|
10
|
+
*/
|
|
11
|
+
export declare function isProjectUnavailable(status: number, body: unknown): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Thrown by the transport layer when a request fails because the project is
|
|
14
|
+
* unavailable. Callers branch on `code`, not `instanceof`, where they only
|
|
15
|
+
* have a plain error (e.g. across the streaming/XHR boundary).
|
|
16
|
+
*/
|
|
17
|
+
export declare class ProjectUnavailableError extends Error {
|
|
18
|
+
code: string;
|
|
19
|
+
constructor(message?: string);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Neutral, locale-aware copy shown in place of the chat surface when the
|
|
23
|
+
* project is unavailable. Deliberately generic — no mention of payment,
|
|
24
|
+
* plans, or Qafka — so it reads the same regardless of the underlying cause.
|
|
25
|
+
*/
|
|
26
|
+
export declare function unavailableMessage(locale?: string): string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared helpers for the "project unavailable" state: the backend answers
|
|
4
|
+
* with HTTP 403 `{ error: 'project_unavailable' }` when the project (or its
|
|
5
|
+
* parent) is suspended. The chat screen stays mounted but shows a neutral
|
|
6
|
+
* message instead of the normal chat surface or an error bubble.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.ProjectUnavailableError = void 0;
|
|
10
|
+
exports.isProjectUnavailable = isProjectUnavailable;
|
|
11
|
+
exports.unavailableMessage = unavailableMessage;
|
|
12
|
+
/**
|
|
13
|
+
* True when a response is the specific "project unavailable" signal, as
|
|
14
|
+
* opposed to any other 403 (bundle id, platform, permission, ...).
|
|
15
|
+
*/
|
|
16
|
+
function isProjectUnavailable(status, body) {
|
|
17
|
+
if (status !== 403)
|
|
18
|
+
return false;
|
|
19
|
+
if (!body || typeof body !== 'object')
|
|
20
|
+
return false;
|
|
21
|
+
return body.error === 'project_unavailable';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Thrown by the transport layer when a request fails because the project is
|
|
25
|
+
* unavailable. Callers branch on `code`, not `instanceof`, where they only
|
|
26
|
+
* have a plain error (e.g. across the streaming/XHR boundary).
|
|
27
|
+
*/
|
|
28
|
+
class ProjectUnavailableError extends Error {
|
|
29
|
+
code = 'PROJECT_UNAVAILABLE';
|
|
30
|
+
constructor(message = 'The assistant is currently unavailable.') {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = 'ProjectUnavailableError';
|
|
33
|
+
Object.setPrototypeOf(this, ProjectUnavailableError.prototype);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.ProjectUnavailableError = ProjectUnavailableError;
|
|
37
|
+
/**
|
|
38
|
+
* Neutral, locale-aware copy shown in place of the chat surface when the
|
|
39
|
+
* project is unavailable. Deliberately generic — no mention of payment,
|
|
40
|
+
* plans, or Qafka — so it reads the same regardless of the underlying cause.
|
|
41
|
+
*/
|
|
42
|
+
function unavailableMessage(locale) {
|
|
43
|
+
return locale?.toLowerCase().startsWith('tr')
|
|
44
|
+
? 'Asistan şu anda hizmet verememektedir.'
|
|
45
|
+
: 'The assistant is currently unavailable.';
|
|
46
|
+
}
|
package/dist/types/sdk.d.ts
CHANGED
|
@@ -23,9 +23,10 @@ export interface SDKConfig {
|
|
|
23
23
|
/** Request timeout in ms. */
|
|
24
24
|
timeout?: number;
|
|
25
25
|
/**
|
|
26
|
-
* BCP 47 locale (e.g. "tr", "en", "tr-TR"). When set, the SDK
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* BCP 47 locale (e.g. "tr", "en", "tr-TR"). When set, the SDK forwards it
|
|
27
|
+
* as `sdkContext.locale` on every chat request — the language the assistant
|
|
28
|
+
* answers in. Explicit only: never derived from the device language, and it
|
|
29
|
+
* does not change the SDK's own UI strings.
|
|
29
30
|
*/
|
|
30
31
|
locale?: string;
|
|
31
32
|
onStatusChange?: (status: SDKStatusType) => void;
|
|
@@ -35,4 +36,4 @@ export interface SDKConfig {
|
|
|
35
36
|
/**
|
|
36
37
|
* SDK status type
|
|
37
38
|
*/
|
|
38
|
-
export type SDKStatusType = 'uninitialized' | 'initializing' | 'ready' | 'error';
|
|
39
|
+
export type SDKStatusType = 'uninitialized' | 'initializing' | 'ready' | 'unavailable' | 'error';
|
|
@@ -278,11 +278,26 @@ to handle in code. It shows a small set of key/value chips (or routes text to th
|
|
|
278
278
|
You may optionally restyle the chips via `voiceComponents.dataChipList` / `voiceComponents.dataChip`.
|
|
279
279
|
|
|
280
280
|
### Useful context props
|
|
281
|
-
- **`context`**
|
|
282
|
-
|
|
283
|
-
|
|
281
|
+
- **`context`** — runtime facts the AI may use to personalize answers (first name, selected
|
|
282
|
+
store, account state). Every key reaches the AI prompt; tool parameters with
|
|
283
|
+
`source: 'context'` also read from here. For a value with no meaningful state, send an
|
|
284
|
+
explicit "empty" shape rather than omitting the key.
|
|
285
|
+
- **`contextDescription`** — one sentence describing the screen the user is currently on
|
|
286
|
+
(their location, not a fact about them; shown to the AI above the context lines).
|
|
287
|
+
- **`endUserData`** — non-LLM metadata about the user (≤8KB). Never reaches the AI prompt —
|
|
288
|
+
the inverse of `context`.
|
|
284
289
|
- **`subProjectId`** — multi-tenant routing.
|
|
285
290
|
- **`locale`** — BCP-47 locale.
|
|
291
|
+
- **`onUnavailable`** — fires when the project becomes unavailable (suspended). The chat
|
|
292
|
+
screen stays mounted and shows a neutral message in place of the normal chat surface; not
|
|
293
|
+
treated as an error (`onError` is not also called). Use it to hide your own entry point —
|
|
294
|
+
e.g. a launcher button or nav item — so users can't open a chat screen with nothing to say:
|
|
295
|
+
```tsx
|
|
296
|
+
const [chatAvailable, setChatAvailable] = useState(true);
|
|
297
|
+
|
|
298
|
+
<Qafka onUnavailable={() => setChatAvailable(false)} />
|
|
299
|
+
{chatAvailable && <LauncherButton onPress={openChat} />}
|
|
300
|
+
```
|
|
286
301
|
|
|
287
302
|
---
|
|
288
303
|
|
package/package.json
CHANGED