@qafka/react-native 2.1.0 → 2.2.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 CHANGED
@@ -7,6 +7,12 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.1.1] — 2026-06-03
11
+
12
+ ### Fixed
13
+
14
+ - Chat surface could get stuck on the loading indicator (greeting + typing dots, no input) after navigating away from the chat screen and returning to it. Re-entering an already-initialized SDK now reports the ready state to the new mount, and tearing down a superseded instance no longer clears a newer live one.
15
+
10
16
  ## [2.0.0] — 2026-05-17
11
17
 
12
18
  Initial public release. See [README.md](./README.md) for the full feature list and installation instructions.
package/dist/QafkaSDK.js CHANGED
@@ -47,6 +47,14 @@ class QafkaSDK {
47
47
  if (currentApiKey === newApiKey &&
48
48
  currentSubProjectId === newSubProjectId &&
49
49
  currentProjectId === newProjectId) {
50
+ // Already initialized with an identical config. A second component
51
+ // mount shares this singleton — the host can re-enter or re-push the
52
+ // chat screen via forward navigation before the previous instance has
53
+ // finished tearing down. Re-running init is unnecessary, but we MUST
54
+ // still notify this caller's status listener; otherwise its local
55
+ // `sdkReady` never flips true and the UI stalls on the loading gate
56
+ // with the input hidden.
57
+ config.onStatusChange?.('ready');
50
58
  return;
51
59
  }
52
60
  await this.destroy();
@@ -457,7 +465,12 @@ class QafkaSDK {
457
465
  this.config = null;
458
466
  this.themePrefetchPromise = null;
459
467
  this.status = 'uninitialized';
460
- QafkaSDK.instance = null;
468
+ // Only clear the shared singleton slot when WE are still the current
469
+ // instance. An outgoing component unmounting must not null a newer
470
+ // instance that a freshly mounted component already created/initialized.
471
+ if (QafkaSDK.instance === this) {
472
+ QafkaSDK.instance = null;
473
+ }
461
474
  }
462
475
  }
463
476
  exports.QafkaSDK = QafkaSDK;
@@ -86,11 +86,12 @@ const { width: SCREEN_WIDTH } = react_native_1.Dimensions.get('window');
86
86
  * />
87
87
  * ```
88
88
  */
89
- exports.Qafka = (0, react_1.forwardRef)(function Qafka({ style, apiUrl: apiUrlProp, 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) {
89
+ 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
90
  // === ALL HOOKS FIRST (Rules of Hooks compliance) ===
91
91
  const insets = (0, react_native_safe_area_context_1.useSafeAreaInsets)();
92
92
  const { sdkReady, error: sdkError, resolvedApiKey, resolvedApiUrl } = (0, useSDK_1.useSDK)({
93
93
  apiUrl: apiUrlProp,
94
+ devConfig,
94
95
  subProjectId,
95
96
  projectId,
96
97
  locale,
@@ -4,6 +4,7 @@ import { ComponentRegistry } from '../types/components';
4
4
  import { ExternalSuggestion } from '../types/external-navigation';
5
5
  import { NavigationSuggestion } from '../types/navigation';
6
6
  import { VoiceChatState } from './VoicePage';
7
+ import type { RuntimeConfig } from '../runtime-config-loader';
7
8
  /**
8
9
  * Augmentation hook for consumer-side projectId literal types.
9
10
  *
@@ -256,6 +257,16 @@ export interface QafkaProps {
256
257
  * Backend API URL (OPTIONAL — advanced, leave unset for production).
257
258
  */
258
259
  apiUrl?: string;
260
+ /**
261
+ * Dev-only runtime config the SDK reads for the simulator/emulator
262
+ * `__DEV__` auth path. Pass the CLI-managed file:
263
+ * `devConfig={__DEV__ ? require('../.qafka/qafka-runtime') : undefined}`
264
+ * Omit on real devices / production — attestation handles auth there, and
265
+ * `require` under `__DEV__` is dead-code-eliminated from release bundles so
266
+ * no key reaches the prod build. When omitted, the SDK falls back to the
267
+ * legacy `node_modules` config (backward compatible).
268
+ */
269
+ devConfig?: RuntimeConfig | null;
259
270
  /**
260
271
  * Sub-Project Identifier (OPTIONAL)
261
272
  *
@@ -569,7 +569,9 @@ function VoicePage({ state, transcript, userTranscript, transcriptHistory, trans
569
569
 
570
570
  {showMute && !hasToolUI ? (<react_native_1.View style={{
571
571
  position: 'absolute',
572
- top: 12,
572
+ // Absolute children aren't pushed by the parent's safe-area paddingTop,
573
+ // so add the inset here to keep the button clear of the status bar / notch.
574
+ top: insets.top + 12,
573
575
  right: 16,
574
576
  }}>
575
577
  <MuteButton isMuted={isMuted} onToggle={onToggleMute} theme={theme} state={state}/>
@@ -10,12 +10,15 @@ export interface UseSDKOptions {
10
10
  locale?: string;
11
11
  onReady?: () => void;
12
12
  onError?: (error: Error) => void;
13
+ devConfig?: unknown;
13
14
  }
14
15
  export interface UseSDKResult {
15
16
  sdkReady: boolean;
16
17
  error: string | null;
17
18
  /**
18
- * Dev-only: the resolved developmentKey from `qafka.config.js`. Null in
19
+ * Dev-only: the resolved developmentKey from the consumer dev config the
20
+ * `devConfig` prop (`.qafka/qafka-runtime.js`) or the legacy node_modules
21
+ * fallback. Null in
19
22
  * production builds (Metro dead-code-eliminates the dev config branch).
20
23
  * Used by `useVoiceChat` whose WebSocket auth still relies on apiKey.
21
24
  */
@@ -28,4 +31,4 @@ export interface UseSDKResult {
28
31
  /**
29
32
  * Custom hook for managing SDK initialization and lifecycle
30
33
  */
31
- export declare const useSDK: ({ apiUrl, subProjectId, projectId, locale, onReady, onError, }: UseSDKOptions) => UseSDKResult;
34
+ export declare const useSDK: ({ apiUrl, subProjectId, projectId, locale, onReady, onError, devConfig, }: UseSDKOptions) => UseSDKResult;
@@ -5,10 +5,11 @@ const react_1 = require("react");
5
5
  const QafkaSDK_1 = require("../QafkaSDK");
6
6
  const runtime_config_loader_1 = require("../runtime-config-loader");
7
7
  const resolve_project_config_1 = require("../resolve-project-config");
8
+ const resolve_runtime_config_source_1 = require("../resolve-runtime-config-source");
8
9
  /**
9
10
  * Custom hook for managing SDK initialization and lifecycle
10
11
  */
11
- const useSDK = ({ apiUrl, subProjectId, projectId, locale, onReady, onError, }) => {
12
+ const useSDK = ({ apiUrl, subProjectId, projectId, locale, onReady, onError, devConfig, }) => {
12
13
  const [sdkReady, setSdkReady] = (0, react_1.useState)(false);
13
14
  const [error, setError] = (0, react_1.useState)(null);
14
15
  const [resolvedApiKey, setResolvedApiKey] = (0, react_1.useState)(null);
@@ -27,18 +28,16 @@ const useSDK = ({ apiUrl, subProjectId, projectId, locale, onReady, onError, })
27
28
  let apiKey = null;
28
29
  let resolvedUrl = apiUrl;
29
30
  if (__DEV__) {
30
- // Dev path: load qafka.config.js, pick developmentKey for the current project.
31
- // Metro dead-code elimination removes this branch entirely in production
32
- // builds, so qafka.config.js never lands in the prod bundle.
31
+ // Dev path: prefer the consumer-supplied .qafka/ config, else the legacy
32
+ // node_modules config. Metro dead-code elimination removes this whole
33
+ // branch (and both requires) from production builds.
33
34
  try {
34
- const cfg = (0, resolve_project_config_1.resolveProjectConfig)({ apiUrl, projectId }, (0, runtime_config_loader_1.loadRuntimeConfig)());
35
+ const cfg = (0, resolve_project_config_1.resolveProjectConfig)({ apiUrl, projectId }, (0, resolve_runtime_config_source_1.resolveRuntimeConfigSource)(devConfig, (0, runtime_config_loader_1.loadRuntimeConfig)()));
35
36
  apiKey = cfg.apiKey;
36
37
  resolvedUrl = cfg.apiUrl;
37
38
  }
38
39
  catch (err) {
39
- if (__DEV__) {
40
- console.warn('[Qafka] Dev config missing or invalid:', err instanceof Error ? err.message : err);
41
- }
40
+ console.warn('[Qafka] Dev config missing or invalid:', err instanceof Error ? err.message : err);
42
41
  }
43
42
  }
44
43
  // Expose resolved values so `useVoiceChat` (WebSocket auth still
@@ -0,0 +1,9 @@
1
+ import { type RuntimeConfig } from './runtime-config-loader';
2
+ /**
3
+ * Pick the runtime config the SDK should use in dev builds.
4
+ *
5
+ * Precedence: a valid `devConfig` (the consumer's `require('../.qafka/qafka-runtime')`,
6
+ * shape-validated) wins; otherwise the value loaded from the SDK package
7
+ * (`loadRuntimeConfig()`). Returns null when neither yields a usable config.
8
+ */
9
+ export declare function resolveRuntimeConfigSource(devConfig: unknown, loaded: RuntimeConfig | null): RuntimeConfig | null;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveRuntimeConfigSource = resolveRuntimeConfigSource;
4
+ const runtime_config_loader_1 = require("./runtime-config-loader");
5
+ /**
6
+ * Pick the runtime config the SDK should use in dev builds.
7
+ *
8
+ * Precedence: a valid `devConfig` (the consumer's `require('../.qafka/qafka-runtime')`,
9
+ * shape-validated) wins; otherwise the value loaded from the SDK package
10
+ * (`loadRuntimeConfig()`). Returns null when neither yields a usable config.
11
+ */
12
+ function resolveRuntimeConfigSource(devConfig, loaded) {
13
+ const provided = devConfig != null ? (0, runtime_config_loader_1.normalizeRuntimeConfig)(devConfig) : null;
14
+ return provided ?? loaded;
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qafka/react-native",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Drop-in AI assistant for React Native: chat, voice, and tool execution with screen-aware navigation.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",