@qafka/react-native 2.0.0 → 2.1.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.
- package/CHANGELOG.md +6 -0
- package/dist/QafkaSDK.js +20 -2
- package/dist/components/Qafka.types.d.ts +9 -9
- package/dist/components/QafkaProvider.d.ts +1 -1
- package/dist/hooks/useSDK.js +7 -0
- package/dist/services/AttestationManager.d.ts +3 -0
- package/dist/services/AttestationManager.js +27 -10
- package/dist/types/sdk.d.ts +2 -0
- package/package.json +1 -1
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
|
@@ -40,9 +40,21 @@ class QafkaSDK {
|
|
|
40
40
|
if (this.status === 'ready') {
|
|
41
41
|
const currentApiKey = this.config?.apiKey ?? null;
|
|
42
42
|
const currentSubProjectId = this.subProjectId;
|
|
43
|
+
const currentProjectId = this.config?.projectId ?? null;
|
|
43
44
|
const newApiKey = config.apiKey ?? null;
|
|
44
45
|
const newSubProjectId = config.subProjectId ?? null;
|
|
45
|
-
|
|
46
|
+
const newProjectId = config.projectId ?? null;
|
|
47
|
+
if (currentApiKey === newApiKey &&
|
|
48
|
+
currentSubProjectId === newSubProjectId &&
|
|
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');
|
|
46
58
|
return;
|
|
47
59
|
}
|
|
48
60
|
await this.destroy();
|
|
@@ -78,6 +90,7 @@ class QafkaSDK {
|
|
|
78
90
|
this.attestationManager = new AttestationManager_1.AttestationManager({
|
|
79
91
|
apiUrl: attestApiUrl,
|
|
80
92
|
apiKey: apiKey ?? null,
|
|
93
|
+
projectId: config.projectId ?? null,
|
|
81
94
|
debug: config.debug,
|
|
82
95
|
});
|
|
83
96
|
try {
|
|
@@ -452,7 +465,12 @@ class QafkaSDK {
|
|
|
452
465
|
this.config = null;
|
|
453
466
|
this.themePrefetchPromise = null;
|
|
454
467
|
this.status = 'uninitialized';
|
|
455
|
-
|
|
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
|
+
}
|
|
456
474
|
}
|
|
457
475
|
}
|
|
458
476
|
exports.QafkaSDK = QafkaSDK;
|
|
@@ -233,16 +233,16 @@ export interface QafkaHandle {
|
|
|
233
233
|
export interface QafkaProps {
|
|
234
234
|
style?: ViewStyle;
|
|
235
235
|
/**
|
|
236
|
-
*
|
|
237
|
-
* project. Selects which project's development key the SDK uses in
|
|
238
|
-
* development builds (loaded from `qafka.config.js`).
|
|
236
|
+
* Target Qafka project identifier. Required.
|
|
239
237
|
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
238
|
+
* Identifies which project this SDK instance talks to. It is sent with
|
|
239
|
+
* device attestation in keyless (production) builds, so it cannot be
|
|
240
|
+
* omitted — a missing value throws on mount.
|
|
243
241
|
*
|
|
244
|
-
* In
|
|
245
|
-
*
|
|
242
|
+
* In development builds it also selects which project's development key
|
|
243
|
+
* the SDK uses (loaded from `qafka.config.js`). Pass a project id that is
|
|
244
|
+
* unknown to the runtime config and the SDK throws on mount — silent
|
|
245
|
+
* fallback would mask a typo.
|
|
246
246
|
*
|
|
247
247
|
* When the Qafka CLI has been run (`qafka init` / `qafka project` /
|
|
248
248
|
* `qafka sync`), this prop is narrowed to the registered project ids —
|
|
@@ -251,7 +251,7 @@ export interface QafkaProps {
|
|
|
251
251
|
*
|
|
252
252
|
* @example "proj_abc123"
|
|
253
253
|
*/
|
|
254
|
-
projectId
|
|
254
|
+
projectId: ProjectIdOf;
|
|
255
255
|
/**
|
|
256
256
|
* Backend API URL (OPTIONAL — advanced, leave unset for production).
|
|
257
257
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { QafkaProps } from './Qafka.types';
|
|
3
|
-
type QafkaProviderProps = Pick<QafkaProps, 'style' | 'apiUrl' | 'theme' | 'themeOverride' | 'customTheme' | 'enableStreaming' | 'isAuthenticated' | 'endUserId' | 'endUserData' | 'context' | 'contextDescription' | 'components' | 'showTimestamps' | 'placeholder' | 'maxMessageLength' | 'greetingMessage' | 'title' | 'showHeader' | 'onReady' | 'onMessageSent' | 'onResponseReceived' | 'onError' | 'onNavigationSuggest' | 'onNavigationAction' | 'onToolSuggested' | 'onActionResult' | 'onStepCompleted' | 'onClose' | 'onBack' | 'CloseComponent' | 'BackComponent' | 'navigationLabelFormat' | 'NavigationButtonComponent'> & {
|
|
3
|
+
type QafkaProviderProps = Pick<QafkaProps, 'style' | 'projectId' | 'apiUrl' | 'theme' | 'themeOverride' | 'customTheme' | 'enableStreaming' | 'isAuthenticated' | 'endUserId' | 'endUserData' | 'context' | 'contextDescription' | 'components' | 'showTimestamps' | 'placeholder' | 'maxMessageLength' | 'greetingMessage' | 'title' | 'showHeader' | 'onReady' | 'onMessageSent' | 'onResponseReceived' | 'onError' | 'onNavigationSuggest' | 'onNavigationAction' | 'onToolSuggested' | 'onActionResult' | 'onStepCompleted' | 'onClose' | 'onBack' | 'CloseComponent' | 'BackComponent' | 'navigationLabelFormat' | 'NavigationButtonComponent'> & {
|
|
4
4
|
mode?: 'floating' | 'fullscreen' | 'inline';
|
|
5
5
|
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
|
|
6
6
|
};
|
package/dist/hooks/useSDK.js
CHANGED
|
@@ -18,6 +18,12 @@ const useSDK = ({ apiUrl, subProjectId, projectId, locale, onReady, onError, })
|
|
|
18
18
|
onReadyRef.current = onReady;
|
|
19
19
|
onErrorRef.current = onError;
|
|
20
20
|
const initializeSDK = async () => {
|
|
21
|
+
if (!projectId) {
|
|
22
|
+
const err = new Error('Qafka: `projectId` prop is required.');
|
|
23
|
+
setError(err.message);
|
|
24
|
+
onErrorRef.current?.(err);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
21
27
|
let apiKey = null;
|
|
22
28
|
let resolvedUrl = apiUrl;
|
|
23
29
|
if (__DEV__) {
|
|
@@ -45,6 +51,7 @@ const useSDK = ({ apiUrl, subProjectId, projectId, locale, onReady, onError, })
|
|
|
45
51
|
apiKey, // null in prod; developmentKey in dev
|
|
46
52
|
apiUrl: resolvedUrl,
|
|
47
53
|
subProjectId,
|
|
54
|
+
projectId,
|
|
48
55
|
locale,
|
|
49
56
|
onStatusChange: (status) => {
|
|
50
57
|
if (status === 'ready') {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
interface AttestationConfig {
|
|
2
2
|
apiUrl: string;
|
|
3
3
|
apiKey: string | null;
|
|
4
|
+
projectId?: string | null;
|
|
4
5
|
debug?: boolean;
|
|
5
6
|
}
|
|
6
7
|
export declare class AttestationManager {
|
|
@@ -9,6 +10,8 @@ export declare class AttestationManager {
|
|
|
9
10
|
private keyId;
|
|
10
11
|
constructor(config: AttestationConfig);
|
|
11
12
|
private log;
|
|
13
|
+
private storageKeyIdName;
|
|
14
|
+
private androidAlias;
|
|
12
15
|
initialize(): Promise<void>;
|
|
13
16
|
private initializeIos;
|
|
14
17
|
private performIosAttestation;
|
|
@@ -38,7 +38,6 @@ const react_native_1 = require("react-native");
|
|
|
38
38
|
const storage_1 = require("./storage");
|
|
39
39
|
const NativeAttestation = __importStar(require("../native/QafkaAttestation"));
|
|
40
40
|
const QafkaAttestation_1 = require("../native/QafkaAttestation");
|
|
41
|
-
const STORAGE_KEY_ID = '@qafka/attestation_key_id';
|
|
42
41
|
class AttestationManager {
|
|
43
42
|
config;
|
|
44
43
|
sessionToken = null;
|
|
@@ -53,6 +52,20 @@ class AttestationManager {
|
|
|
53
52
|
const verbose = this.config.debug || __DEV__;
|
|
54
53
|
console.warn(verbose && verboseMessage ? verboseMessage : message);
|
|
55
54
|
}
|
|
55
|
+
// Namespace the persisted key identifier per project so each project gets its
|
|
56
|
+
// own device-bound key instead of sharing one across projects.
|
|
57
|
+
storageKeyIdName() {
|
|
58
|
+
return this.config.projectId
|
|
59
|
+
? `@qafka/attestation_key_id:${this.config.projectId}`
|
|
60
|
+
: '@qafka/attestation_key_id';
|
|
61
|
+
}
|
|
62
|
+
// Namespace the native key alias per project, sanitised to characters that
|
|
63
|
+
// are safe for a keystore alias.
|
|
64
|
+
androidAlias() {
|
|
65
|
+
return this.config.projectId
|
|
66
|
+
? `qafka_attest_${String(this.config.projectId).replace(/[^a-zA-Z0-9_]/g, '_')}`
|
|
67
|
+
: 'qafka_attest';
|
|
68
|
+
}
|
|
56
69
|
async initialize() {
|
|
57
70
|
const moduleLinked = (0, QafkaAttestation_1.isAttestationAvailable)();
|
|
58
71
|
// Diagnostic logs are intentionally NOT __DEV__-gated. Init is a once-per-
|
|
@@ -90,10 +103,10 @@ class AttestationManager {
|
|
|
90
103
|
}
|
|
91
104
|
}
|
|
92
105
|
async initializeIos() {
|
|
93
|
-
this.keyId = await storage_1.storage.getItem(
|
|
106
|
+
this.keyId = await storage_1.storage.getItem(this.storageKeyIdName());
|
|
94
107
|
if (!this.keyId) {
|
|
95
108
|
this.keyId = await NativeAttestation.generateKey();
|
|
96
|
-
await storage_1.storage.setItem(
|
|
109
|
+
await storage_1.storage.setItem(this.storageKeyIdName(), this.keyId);
|
|
97
110
|
}
|
|
98
111
|
// Always perform a fresh attestation at session start. Lighter
|
|
99
112
|
// assertions (`performIosAssertion`) are used later to refresh expired
|
|
@@ -122,9 +135,9 @@ class AttestationManager {
|
|
|
122
135
|
throw err;
|
|
123
136
|
}
|
|
124
137
|
this.log(`[Qafka:init] initial attestation failed (${msg}), regenerating key and retrying once`);
|
|
125
|
-
await storage_1.storage.removeItem(
|
|
138
|
+
await storage_1.storage.removeItem(this.storageKeyIdName());
|
|
126
139
|
this.keyId = await NativeAttestation.generateKey();
|
|
127
|
-
await storage_1.storage.setItem(
|
|
140
|
+
await storage_1.storage.setItem(this.storageKeyIdName(), this.keyId);
|
|
128
141
|
await this.performIosAttestation();
|
|
129
142
|
}
|
|
130
143
|
}
|
|
@@ -162,8 +175,9 @@ class AttestationManager {
|
|
|
162
175
|
}
|
|
163
176
|
async performAndroidAttestation() {
|
|
164
177
|
const challenge = await this.requestChallenge('android');
|
|
165
|
-
|
|
166
|
-
|
|
178
|
+
const alias = this.androidAlias();
|
|
179
|
+
await NativeAttestation.generateKeyPair(alias, challenge);
|
|
180
|
+
const certChain = await NativeAttestation.getAttestationCertChain(alias);
|
|
167
181
|
const response = await this.submitAttestation({
|
|
168
182
|
platform: 'android',
|
|
169
183
|
certChain,
|
|
@@ -249,7 +263,7 @@ class AttestationManager {
|
|
|
249
263
|
response = await fetch(url, {
|
|
250
264
|
method: 'POST',
|
|
251
265
|
headers: this.getHeaders(),
|
|
252
|
-
body: JSON.stringify({ platform }),
|
|
266
|
+
body: JSON.stringify({ platform, ...(this.config.projectId ? { projectId: this.config.projectId } : {}) }),
|
|
253
267
|
});
|
|
254
268
|
}
|
|
255
269
|
catch (err) {
|
|
@@ -272,7 +286,10 @@ class AttestationManager {
|
|
|
272
286
|
response = await fetch(url, {
|
|
273
287
|
method: 'POST',
|
|
274
288
|
headers: this.getHeaders(),
|
|
275
|
-
body: JSON.stringify(
|
|
289
|
+
body: JSON.stringify({
|
|
290
|
+
...body,
|
|
291
|
+
...(this.config.projectId ? { projectId: this.config.projectId } : {}),
|
|
292
|
+
}),
|
|
276
293
|
});
|
|
277
294
|
}
|
|
278
295
|
catch (err) {
|
|
@@ -290,7 +307,7 @@ class AttestationManager {
|
|
|
290
307
|
async reset() {
|
|
291
308
|
this.sessionToken = null;
|
|
292
309
|
this.keyId = null;
|
|
293
|
-
await storage_1.storage.removeItem(
|
|
310
|
+
await storage_1.storage.removeItem(this.storageKeyIdName());
|
|
294
311
|
}
|
|
295
312
|
}
|
|
296
313
|
exports.AttestationManager = AttestationManager;
|
package/dist/types/sdk.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface SDKConfig {
|
|
|
12
12
|
apiKey?: string | null;
|
|
13
13
|
/** Sub-project identifier (same apiKey, different sub-project). */
|
|
14
14
|
subProjectId?: string;
|
|
15
|
+
/** Target Qafka project id. Required in keyless (production) builds. */
|
|
16
|
+
projectId?: string;
|
|
15
17
|
/** Advanced — leave unset for production. */
|
|
16
18
|
apiUrl?: string;
|
|
17
19
|
/** React Navigation ref, used when the SDK navigates on your behalf. */
|
package/package.json
CHANGED