@sagepilot-ai/react-native-sdk 0.2.3 → 0.2.5

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/dist/index.mjs CHANGED
@@ -15,7 +15,7 @@ var SagepilotChatError = class extends Error {
15
15
 
16
16
  // src/core/config/constants.ts
17
17
  var SDK_NAME = "@sagepilot-ai/react-native-sdk";
18
- var SDK_VERSION = "0.2.3";
18
+ var SDK_VERSION = "0.2.5";
19
19
  var DEFAULT_HOST = "https://app.sagepilot.ai";
20
20
  var DEFAULT_WIDGET_HOST = "https://app.sagepilot.ai";
21
21
  var CUSTOMER_API_PREFIX = "/customer-api/v1";
@@ -464,6 +464,18 @@ function normalizeIdentity(identity) {
464
464
  user_hash: identity.userHash ?? identity.user_hash
465
465
  };
466
466
  }
467
+ function readStringField(input, key) {
468
+ const value = input[key];
469
+ return typeof value === "string" && value.trim() ? value : void 0;
470
+ }
471
+ function normalizeOptionalString(value) {
472
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
473
+ }
474
+ function readRecordField(input, key) {
475
+ const value = input[key];
476
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
477
+ return value;
478
+ }
467
479
  function toPublicSessionState(session) {
468
480
  return {
469
481
  session_id: session.session_id,
@@ -497,6 +509,7 @@ var SagepilotReactNativeChat = class {
497
509
  this.unreadPollTimer = null;
498
510
  this.stateCallbacks = /* @__PURE__ */ new Set();
499
511
  this.identifyCallbacks = /* @__PURE__ */ new Set();
512
+ this.conversationCreatedCallbacks = /* @__PURE__ */ new Set();
500
513
  this.unreadCallbacks = /* @__PURE__ */ new Set();
501
514
  this.readyCallbacks = /* @__PURE__ */ new Set();
502
515
  this.presentCallbacks = /* @__PURE__ */ new Set();
@@ -519,6 +532,9 @@ var SagepilotReactNativeChat = class {
519
532
  const { workspaceId, channelId } = parseKey(config.key);
520
533
  this.runtimeOptions = {
521
534
  behavior: config.behavior,
535
+ cacheStorage: config.cacheStorage,
536
+ filePicker: config.filePicker,
537
+ fileStore: config.fileStore,
522
538
  presentation: config.presentation,
523
539
  theme: config.theme,
524
540
  hostedClaimsStorageKeyPrefix: config.hostedClaimsStorageKeyPrefix || DEFAULT_HOSTED_CLAIMS_STORAGE_KEY_PREFIX
@@ -703,7 +719,15 @@ var SagepilotReactNativeChat = class {
703
719
  }
704
720
  presentMessageComposer(message, options) {
705
721
  this.requireConfigured();
706
- this.hostedChatView = { screen: "composer", message, mode: options?.mode ?? "auto" };
722
+ const chatId = normalizeOptionalString(options?.chatId);
723
+ this.hostedChatView = {
724
+ screen: "composer",
725
+ message,
726
+ mode: options?.mode ?? "auto",
727
+ chatId,
728
+ metadata: options?.metadata,
729
+ onConversationCreated: options?.onConversationCreated
730
+ };
707
731
  return this.showHostedChat();
708
732
  }
709
733
  dismiss() {
@@ -752,6 +776,13 @@ var SagepilotReactNativeChat = class {
752
776
  this.dismissCallbacks.delete(callback);
753
777
  };
754
778
  }
779
+ onConversationCreated(callback) {
780
+ if (typeof callback !== "function") return () => void 0;
781
+ this.conversationCreatedCallbacks.add(callback);
782
+ return () => {
783
+ this.conversationCreatedCallbacks.delete(callback);
784
+ };
785
+ }
755
786
  onError(callback) {
756
787
  if (typeof callback !== "function") return () => void 0;
757
788
  this.errorCallbacks.add(callback);
@@ -803,7 +834,10 @@ var SagepilotReactNativeChat = class {
803
834
  url.searchParams.set("jwt", this.legacyWidgetJwt);
804
835
  }
805
836
  if (this.hostedChatView.screen === "composer") {
806
- if (this.hostedChatView.mode === "new") {
837
+ if (this.hostedChatView.chatId) {
838
+ url.searchParams.set("chat_id", this.hostedChatView.chatId);
839
+ }
840
+ if (this.hostedChatView.mode === "new" && !this.hostedChatView.chatId) {
807
841
  url.searchParams.set("new", "true");
808
842
  }
809
843
  if (this.hostedChatView.message) {
@@ -831,13 +865,19 @@ var SagepilotReactNativeChat = class {
831
865
  "})();"
832
866
  ].join("\n");
833
867
  }
868
+ getActiveHostedChatId() {
869
+ if (this.hostedChatView.screen === "composer" && this.hostedChatView.chatId) {
870
+ return this.hostedChatView.chatId;
871
+ }
872
+ return this.session?.conversation_id ?? void 0;
873
+ }
834
874
  getHostedIdentityMessage() {
835
875
  if (!this.legacyWidgetJwt) return null;
836
876
  return {
837
877
  type: "identity_update",
838
878
  data: {
839
879
  jwt: this.legacyWidgetJwt,
840
- chat_id: this.session?.conversation_id ?? void 0
880
+ chat_id: this.getActiveHostedChatId()
841
881
  }
842
882
  };
843
883
  }
@@ -856,17 +896,62 @@ var SagepilotReactNativeChat = class {
856
896
  }
857
897
  return true;
858
898
  }
899
+ if (message.type === "sagepilot:conversation_created") {
900
+ this.handleConversationCreated(message);
901
+ return true;
902
+ }
859
903
  if (message.type === "sagepilot:error") {
860
904
  this.emitError(message);
861
905
  return true;
862
906
  }
863
907
  return true;
864
908
  }
909
+ handleConversationCreated(message) {
910
+ const chatId = readStringField(message, "chat_id") ?? readStringField(message, "conversation_id");
911
+ if (!chatId) return;
912
+ void this.persistConversationId(chatId).catch((error) => this.emitError(error));
913
+ const bridgeMetadata = readRecordField(message, "metadata");
914
+ const composerMetadata = this.hostedChatView.screen === "composer" ? this.hostedChatView.metadata : void 0;
915
+ const metadata = {
916
+ ...composerMetadata ?? {},
917
+ ...bridgeMetadata ?? {}
918
+ };
919
+ const workspaceId = readStringField(message, "workspace_id") ?? this.workspaceId;
920
+ const channelId = readStringField(message, "channel_id") ?? this.channelId;
921
+ const sessionId = readStringField(message, "session_id") ?? this.session?.session_id;
922
+ const customerId = readStringField(message, "customer_id");
923
+ const messageId = readStringField(message, "message_id");
924
+ const createdAt = readStringField(message, "created_at");
925
+ const event = {
926
+ chat_id: chatId,
927
+ ...workspaceId ? { workspace_id: workspaceId } : {},
928
+ ...channelId ? { channel_id: channelId } : {},
929
+ ...sessionId ? { session_id: sessionId } : {},
930
+ ...customerId ? { customer_id: customerId } : {},
931
+ ...messageId ? { message_id: messageId } : {},
932
+ ...createdAt ? { created_at: createdAt } : {},
933
+ ...Object.keys(metadata).length > 0 ? { metadata } : {}
934
+ };
935
+ if (this.hostedChatView.screen === "composer") {
936
+ this.hostedChatView.onConversationCreated?.(event);
937
+ }
938
+ this.conversationCreatedCallbacks.forEach((callback) => callback(event));
939
+ }
940
+ async persistConversationId(conversationId) {
941
+ if (!this.session || !this.sessionManager) return;
942
+ if (this.session.conversation_id === conversationId) return;
943
+ this.session = await this.sessionManager.setSession({
944
+ ...this.session,
945
+ conversation_id: conversationId
946
+ });
947
+ this.emitState();
948
+ }
865
949
  destroy() {
866
950
  this.stopUnreadPolling();
867
951
  this.resetRuntimeState();
868
952
  this.emitState();
869
953
  this.identifyCallbacks.clear();
954
+ this.conversationCreatedCallbacks.clear();
870
955
  this.unreadCallbacks.clear();
871
956
  this.readyCallbacks.clear();
872
957
  this.presentCallbacks.clear();
@@ -1029,6 +1114,9 @@ var SagepilotChat = {
1029
1114
  logout: () => internalSagepilotChat.logout(),
1030
1115
  getIdentityState: () => internalSagepilotChat.getIdentityState(),
1031
1116
  onIdentify: (callback) => internalSagepilotChat.onIdentify(callback),
1117
+ onConversationCreated: (callback) => {
1118
+ return internalSagepilotChat.onConversationCreated(callback);
1119
+ },
1032
1120
  getUnreadCount: () => internalSagepilotChat.getUnreadCount(),
1033
1121
  onUnreadChange: (callback) => internalSagepilotChat.onUnreadChange(callback),
1034
1122
  startUnreadPolling: (intervalMs) => internalSagepilotChat.startUnreadPolling(intervalMs),
@@ -1061,14 +1149,295 @@ function createAsyncStorageCacheStorage(asyncStorage) {
1061
1149
  removeItem: (key) => asyncStorage.removeItem(key)
1062
1150
  };
1063
1151
  }
1152
+ function createJsonCache(storage, namespace) {
1153
+ return {
1154
+ async get(key) {
1155
+ const value = await storage.getItem(`${namespace}:${key}`);
1156
+ if (!value) return null;
1157
+ try {
1158
+ return JSON.parse(value);
1159
+ } catch {
1160
+ return null;
1161
+ }
1162
+ },
1163
+ async set(key, value) {
1164
+ await storage.setItem(`${namespace}:${key}`, JSON.stringify(value));
1165
+ },
1166
+ async remove(key) {
1167
+ await storage.removeItem(`${namespace}:${key}`);
1168
+ }
1169
+ };
1170
+ }
1171
+
1172
+ // src/core/storage/fileStore.ts
1173
+ var DEFAULT_DIRECTORY_NAME = "sagepilot-attachments";
1174
+ function sanitizeKey(key) {
1175
+ return key.replace(/[^a-zA-Z0-9._-]/g, "_").replace(/\.{2,}/g, "_");
1176
+ }
1177
+ function createSagepilotFileStore(blobUtil, options = {}) {
1178
+ const directory = `${blobUtil.fs.dirs.DocumentDir}/${options.directoryName ?? DEFAULT_DIRECTORY_NAME}`;
1179
+ const pathFor = (key) => `${directory}/${sanitizeKey(key)}`;
1180
+ let dirReady = null;
1181
+ function ensureDirectory() {
1182
+ if (!dirReady) {
1183
+ dirReady = (async () => {
1184
+ if (!await blobUtil.fs.exists(directory)) {
1185
+ await blobUtil.fs.mkdir(directory);
1186
+ }
1187
+ })().catch((error) => {
1188
+ dirReady = null;
1189
+ throw error;
1190
+ });
1191
+ }
1192
+ return dirReady;
1193
+ }
1194
+ return {
1195
+ async write(key, base64) {
1196
+ await ensureDirectory();
1197
+ await blobUtil.fs.writeFile(pathFor(key), base64, "base64");
1198
+ },
1199
+ async read(key) {
1200
+ const content = await blobUtil.fs.readFile(pathFor(key), "base64");
1201
+ if (typeof content !== "string" || content.length === 0) {
1202
+ throw new Error("Persisted attachment is empty or unreadable.");
1203
+ }
1204
+ return content;
1205
+ },
1206
+ async remove(key) {
1207
+ try {
1208
+ if (await blobUtil.fs.exists(pathFor(key))) {
1209
+ await blobUtil.fs.unlink(pathFor(key));
1210
+ }
1211
+ } catch {
1212
+ }
1213
+ },
1214
+ async prune(keepKeys) {
1215
+ try {
1216
+ if (!await blobUtil.fs.exists(directory)) return;
1217
+ const keep = new Set(keepKeys.map(sanitizeKey));
1218
+ const entries = await blobUtil.fs.ls(directory);
1219
+ await Promise.all(
1220
+ entries.filter((entry) => !keep.has(entry)).map((entry) => blobUtil.fs.unlink(`${directory}/${entry}`).catch(() => void 0))
1221
+ );
1222
+ } catch {
1223
+ }
1224
+ }
1225
+ };
1226
+ }
1227
+
1228
+ // src/core/native/filePicker.ts
1229
+ import { PermissionsAndroid, Platform } from "react-native";
1230
+ var DEFAULT_IMAGE_MAX_DIMENSION = 1920;
1231
+ var DEFAULT_IMAGE_QUALITY = 0.8;
1232
+ var DEFAULT_IMAGE_MIME_TYPE = "image/jpeg";
1233
+ var DEFAULT_DOCUMENT_MIME_TYPE = "application/octet-stream";
1234
+ var DEFAULT_IMAGE_SELECTION_LIMIT = 5;
1235
+ var DEFAULT_DOCUMENT_MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024;
1236
+ var DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES = 15 * 1024 * 1024;
1237
+ var SagepilotFilePickerError = class extends Error {
1238
+ constructor(code, message) {
1239
+ super(message);
1240
+ this.name = "SagepilotFilePickerError";
1241
+ this.code = code;
1242
+ }
1243
+ };
1244
+ function bytesToMb(bytes) {
1245
+ return Math.round(bytes / (1024 * 1024) * 10) / 10;
1246
+ }
1247
+ function estimateBase64ByteSize(dataBase64) {
1248
+ return Math.floor(dataBase64.length * 3 / 4);
1249
+ }
1250
+ function stripFileScheme(uri) {
1251
+ return uri.startsWith("file://") ? uri.replace("file://", "") : uri;
1252
+ }
1253
+ async function ensureAndroidCameraPermission() {
1254
+ if (Platform.OS !== "android") return;
1255
+ let result;
1256
+ try {
1257
+ result = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA);
1258
+ } catch {
1259
+ return;
1260
+ }
1261
+ if (result === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN) {
1262
+ throw new SagepilotFilePickerError(
1263
+ "permission_denied",
1264
+ "Camera access is turned off. Enable it for this app in Settings to take a photo."
1265
+ );
1266
+ }
1267
+ if (result === PermissionsAndroid.RESULTS.DENIED) {
1268
+ throw new SagepilotFilePickerError(
1269
+ "permission_denied",
1270
+ "Camera permission is required to take a photo."
1271
+ );
1272
+ }
1273
+ }
1274
+ function mapImagePickerErrorCode(errorCode) {
1275
+ switch (errorCode) {
1276
+ case "permission":
1277
+ return "permission_denied";
1278
+ case "camera_unavailable":
1279
+ return "camera_unavailable";
1280
+ default:
1281
+ return "unknown";
1282
+ }
1283
+ }
1284
+ function imageAssetsToPickedFiles(result, maxFileSizeBytes) {
1285
+ if (result.didCancel) return [];
1286
+ if (result.errorCode) {
1287
+ throw new SagepilotFilePickerError(
1288
+ mapImagePickerErrorCode(result.errorCode),
1289
+ result.errorMessage || `Could not capture the photo (${result.errorCode}).`
1290
+ );
1291
+ }
1292
+ const assets = result.assets ?? [];
1293
+ const usable = assets.filter((asset) => typeof asset.base64 === "string" && asset.base64.length > 0);
1294
+ if (assets.length > usable.length) {
1295
+ throw new SagepilotFilePickerError(
1296
+ "encode_failed",
1297
+ assets.length === 1 ? "The photo could not be processed. Please try again." : "Some photos could not be processed. Please try selecting them again."
1298
+ );
1299
+ }
1300
+ return usable.map((asset, index) => {
1301
+ const dataBase64 = asset.base64;
1302
+ const size = asset.fileSize ?? estimateBase64ByteSize(dataBase64);
1303
+ if (maxFileSizeBytes > 0 && size > maxFileSizeBytes) {
1304
+ throw new SagepilotFilePickerError(
1305
+ "file_too_large",
1306
+ `Image is too large (max ${bytesToMb(maxFileSizeBytes)}MB).`
1307
+ );
1308
+ }
1309
+ return {
1310
+ file_name: asset.fileName || `photo-${Date.now()}-${index + 1}.jpg`,
1311
+ mime_type: asset.type || DEFAULT_IMAGE_MIME_TYPE,
1312
+ size,
1313
+ data_base64: dataBase64
1314
+ };
1315
+ });
1316
+ }
1317
+ async function readUriAsBase64(uri, fileReader) {
1318
+ if (fileReader) {
1319
+ const content = await fileReader.fs.readFile(stripFileScheme(uri), "base64");
1320
+ if (typeof content !== "string") {
1321
+ throw new Error("File reader returned unexpected content.");
1322
+ }
1323
+ return content;
1324
+ }
1325
+ const response = await fetch(uri);
1326
+ const blob = await response.blob();
1327
+ return new Promise((resolve, reject) => {
1328
+ const reader = new FileReader();
1329
+ reader.onload = () => {
1330
+ const dataUrl = typeof reader.result === "string" ? reader.result : "";
1331
+ const [, payload = ""] = dataUrl.split(",");
1332
+ if (!payload) {
1333
+ reject(new Error("Could not read the selected file."));
1334
+ return;
1335
+ }
1336
+ resolve(payload);
1337
+ };
1338
+ reader.onerror = () => reject(new Error("Could not read the selected file."));
1339
+ reader.readAsDataURL(blob);
1340
+ });
1341
+ }
1342
+ function createSagepilotFilePicker(options) {
1343
+ const { imagePicker, documentsPicker, fileReader } = options;
1344
+ const imageMaxDimension = options.imageMaxDimension ?? DEFAULT_IMAGE_MAX_DIMENSION;
1345
+ const imageQuality = options.imageQuality ?? DEFAULT_IMAGE_QUALITY;
1346
+ const imageSelectionLimit = options.imageSelectionLimit ?? DEFAULT_IMAGE_SELECTION_LIMIT;
1347
+ const documentMaxFileSizeBytes = options.documentMaxFileSizeBytes ?? DEFAULT_DOCUMENT_MAX_FILE_SIZE_BYTES;
1348
+ const imageMaxFileSizeBytes = options.imageMaxFileSizeBytes ?? DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES;
1349
+ const sources = [];
1350
+ if (imagePicker) sources.push("camera", "library");
1351
+ if (documentsPicker) sources.push("documents");
1352
+ if (sources.length === 0) return void 0;
1353
+ const imageOptions = {
1354
+ mediaType: "photo",
1355
+ includeBase64: true,
1356
+ maxWidth: imageMaxDimension,
1357
+ maxHeight: imageMaxDimension,
1358
+ quality: imageQuality,
1359
+ saveToPhotos: false
1360
+ };
1361
+ async function pickFromCamera() {
1362
+ if (!imagePicker) return [];
1363
+ await ensureAndroidCameraPermission();
1364
+ return imageAssetsToPickedFiles(await imagePicker.launchCamera(imageOptions), imageMaxFileSizeBytes);
1365
+ }
1366
+ async function pickFromLibrary(multiple) {
1367
+ if (!imagePicker) return [];
1368
+ return imageAssetsToPickedFiles(await imagePicker.launchImageLibrary({
1369
+ ...imageOptions,
1370
+ // Bounded multi-select: 0 (unlimited) lets a huge batch OOM the bridge.
1371
+ selectionLimit: multiple ? Math.max(0, imageSelectionLimit) : 1
1372
+ }), imageMaxFileSizeBytes);
1373
+ }
1374
+ async function pickDocuments(multiple) {
1375
+ if (!documentsPicker) return [];
1376
+ let picked;
1377
+ try {
1378
+ picked = await documentsPicker.pick({ allowMultiSelection: multiple });
1379
+ } catch (error) {
1380
+ if (error && typeof error === "object" && error.code === "OPERATION_CANCELED") {
1381
+ return [];
1382
+ }
1383
+ throw error;
1384
+ }
1385
+ const files = [];
1386
+ for (const file of picked) {
1387
+ if (documentMaxFileSizeBytes > 0 && typeof file.size === "number" && file.size > documentMaxFileSizeBytes) {
1388
+ throw new SagepilotFilePickerError(
1389
+ "file_too_large",
1390
+ `"${file.name || "File"}" is too large (max ${bytesToMb(documentMaxFileSizeBytes)}MB).`
1391
+ );
1392
+ }
1393
+ let readableUri = file.uri;
1394
+ if (documentsPicker.keepLocalCopy) {
1395
+ const fileName = file.name || `file-${Date.now()}`;
1396
+ const [copy] = await documentsPicker.keepLocalCopy({
1397
+ files: [{ uri: file.uri, fileName }],
1398
+ destination: "cachesDirectory"
1399
+ });
1400
+ if (copy?.status === "success" && copy.localUri) {
1401
+ readableUri = copy.localUri;
1402
+ }
1403
+ }
1404
+ let dataBase64;
1405
+ try {
1406
+ dataBase64 = await readUriAsBase64(readableUri, fileReader);
1407
+ } catch (error) {
1408
+ throw new SagepilotFilePickerError(
1409
+ "read_failed",
1410
+ error instanceof Error && error.message ? error.message : "Could not read the selected file."
1411
+ );
1412
+ }
1413
+ files.push({
1414
+ file_name: file.name || `file-${Date.now()}`,
1415
+ mime_type: file.type || DEFAULT_DOCUMENT_MIME_TYPE,
1416
+ size: file.size ?? estimateBase64ByteSize(dataBase64),
1417
+ data_base64: dataBase64
1418
+ });
1419
+ }
1420
+ return files;
1421
+ }
1422
+ return {
1423
+ sources,
1424
+ async pickFiles(request) {
1425
+ if (request.source === "camera") return pickFromCamera();
1426
+ if (request.source === "library") return pickFromLibrary(request.multiple);
1427
+ return pickDocuments(request.multiple);
1428
+ }
1429
+ };
1430
+ }
1064
1431
 
1065
1432
  // src/ui/SagepilotChatProvider.ts
1066
1433
  import { createElement, useCallback, useEffect, useRef, useState } from "react";
1067
1434
  import {
1068
1435
  ActivityIndicator,
1436
+ AppState,
1069
1437
  KeyboardAvoidingView,
1438
+ Linking,
1070
1439
  Modal,
1071
- Platform,
1440
+ Platform as Platform2,
1072
1441
  Pressable,
1073
1442
  SafeAreaView,
1074
1443
  StyleSheet,
@@ -1078,6 +1447,37 @@ import {
1078
1447
  import { WebView } from "react-native-webview";
1079
1448
 
1080
1449
  // src/core/webview/mobileBridge.ts
1450
+ var FILE_PICKER_PROTOCOL_VERSION = 2;
1451
+ function buildBridgeCapabilitiesScript(capabilities) {
1452
+ const payload = { ...capabilities, filePickerProtocol: FILE_PICKER_PROTOCOL_VERSION };
1453
+ return [
1454
+ "(function(){",
1455
+ "try {",
1456
+ "if (window.SagepilotMobileBridge) {",
1457
+ `window.SagepilotMobileBridge.capabilities = Object.assign({}, window.SagepilotMobileBridge.capabilities, ${JSON.stringify(payload)});`,
1458
+ "}",
1459
+ "} catch (_) {}",
1460
+ "true;",
1461
+ "})();"
1462
+ ].join("\n");
1463
+ }
1464
+ function buildLivenessPingScript(nonce, staleMs) {
1465
+ return [
1466
+ "(function(){",
1467
+ "try {",
1468
+ "var hb = window.__sagepilotWidgetHeartbeat;",
1469
+ "var supports = typeof hb === 'number';",
1470
+ `var alive = !supports ? true : (Date.now() - hb < ${Math.max(0, Math.floor(staleMs))});`,
1471
+ "var send = function(){",
1472
+ " var msg = JSON.stringify({ type: 'sagepilot:pong', nonce: " + JSON.stringify(nonce) + ", alive: alive, supportsHeartbeat: supports });",
1473
+ " if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) { window.ReactNativeWebView.postMessage(msg); }",
1474
+ "};",
1475
+ "send();",
1476
+ "} catch (_) {}",
1477
+ "true;",
1478
+ "})();"
1479
+ ].join("\n");
1480
+ }
1081
1481
  function isHostedBridgeMessage(value) {
1082
1482
  if (!value || typeof value !== "object") return false;
1083
1483
  const message = value;
@@ -1203,9 +1603,70 @@ function readPresentationState() {
1203
1603
  function getInjectedWebViewScript() {
1204
1604
  return [
1205
1605
  internalSagepilotChat.getHostedAuthScript(),
1206
- mobileWebViewBridgeScript
1606
+ mobileWebViewBridgeScript,
1607
+ buildBridgeCapabilitiesScript({
1608
+ nativeFilePicker: Boolean(internalSagepilotChat.getConfig()?.filePicker)
1609
+ })
1207
1610
  ].filter(Boolean).join("\n");
1208
1611
  }
1612
+ var FILE_PICKER_SOURCE_LABELS = {
1613
+ camera: "Take photo",
1614
+ library: "Choose from gallery",
1615
+ documents: "Browse files"
1616
+ };
1617
+ var DELIVERY_RETRY_MS = 1500;
1618
+ var DELIVERY_LEGACY_FALLBACK_ATTEMPTS = 2;
1619
+ var DELIVERY_MAX_ATTEMPTS = 8;
1620
+ var PERSIST_MAX_TOTAL_BYTES = 2 * 1024 * 1024;
1621
+ var PENDING_FILES_CACHE_NAMESPACE = "sagepilot_rn_picker";
1622
+ var PENDING_FILES_CACHE_KEY = "pending_batch";
1623
+ var LIVENESS_HEARTBEAT_STALE_MS = 4e3;
1624
+ var LIVENESS_PONG_TIMEOUT_MS = 1500;
1625
+ var LIVENESS_MAX_PING_ATTEMPTS = 2;
1626
+ var LIVENESS_MAX_REMOUNTS = 2;
1627
+ var batchSequence = 0;
1628
+ function nextBatchId() {
1629
+ batchSequence += 1;
1630
+ const entropy = Math.random().toString(36).slice(2, 8);
1631
+ return `b_${Date.now().toString(36)}_${batchSequence}_${entropy}`;
1632
+ }
1633
+ function totalBase64Bytes(files) {
1634
+ return files.reduce((sum, file) => sum + (file.data_base64?.length ?? 0), 0);
1635
+ }
1636
+ function storageKeyFor(batchId, index) {
1637
+ return `${batchId}_${index}.bin`;
1638
+ }
1639
+ function buildDispatchScript(messageLiteral) {
1640
+ return [
1641
+ "(function(){",
1642
+ "try {",
1643
+ `var message = ${messageLiteral};`,
1644
+ "window.dispatchEvent(new MessageEvent('message', { data: message, origin: window.location.origin, source: window.parent || window }));",
1645
+ "} catch (_) {}",
1646
+ "true;",
1647
+ "})();"
1648
+ ].join("\n");
1649
+ }
1650
+ function buildFilesPickedScript(files, batchId) {
1651
+ return buildDispatchScript(`{ type: "sagepilot:files_picked", batch_id: ${JSON.stringify(batchId)}, files: ${JSON.stringify(files)} }`);
1652
+ }
1653
+ function buildFilePickerErrorScript(message, code) {
1654
+ return buildDispatchScript(
1655
+ `{ type: "sagepilot:file_picker_error", message: ${JSON.stringify(message)}${code ? `, code: ${JSON.stringify(code)}` : ""} }`
1656
+ );
1657
+ }
1658
+ function buildFilePickerCancelledScript() {
1659
+ return buildDispatchScript(`{ type: "sagepilot:file_picker_cancelled" }`);
1660
+ }
1661
+ function readFilePickerError(error) {
1662
+ if (error && typeof error === "object") {
1663
+ const candidate = error;
1664
+ const message = typeof candidate.message === "string" && candidate.message ? candidate.message : "Could not attach the selected file.";
1665
+ const code = typeof candidate.code === "string" ? candidate.code : void 0;
1666
+ return { message, code };
1667
+ }
1668
+ return { message: "Could not attach the selected file." };
1669
+ }
1209
1670
  function getHostedIdentityDispatchScript() {
1210
1671
  const message = internalSagepilotChat.getHostedIdentityMessage();
1211
1672
  if (!message) return "";
@@ -1228,19 +1689,28 @@ function readUrlOrigin(url) {
1228
1689
  return null;
1229
1690
  }
1230
1691
  }
1692
+ function isInternalWebViewScheme(url) {
1693
+ return url.startsWith("about:") || url.startsWith("data:") || url.startsWith("blob:") || url.startsWith("javascript:") || url.startsWith("file:");
1694
+ }
1231
1695
  var hostedChatWebViewProps = {
1232
1696
  allowFileAccess: true,
1233
1697
  allowFileAccessFromFileURLs: true,
1698
+ ...Platform2.OS === "ios" ? {
1699
+ automaticallyAdjustContentInsets: false,
1700
+ contentInsetAdjustmentBehavior: "never",
1701
+ hideKeyboardAccessoryView: true
1702
+ } : null,
1234
1703
  bounces: false,
1235
1704
  domStorageEnabled: true,
1236
1705
  javaScriptEnabled: true,
1237
1706
  overScrollMode: "never",
1238
- scrollEnabled: Platform.OS !== "android",
1707
+ // The hosted widget owns feed scrolling internally; outer WebView scrolling lets iOS focus-scroll the page over the keyboard.
1708
+ scrollEnabled: false,
1239
1709
  setSupportMultipleWindows: false,
1240
1710
  sharedCookiesEnabled: true,
1241
1711
  thirdPartyCookiesEnabled: true
1242
1712
  };
1243
- var AndroidInsetsView = Platform.OS === "android" ? SagepilotInsetsViewNativeComponent_default : View;
1713
+ var AndroidInsetsView = Platform2.OS === "android" ? SagepilotInsetsViewNativeComponent_default : View;
1244
1714
  var emptyAndroidInsets = { top: 0, bottom: 0 };
1245
1715
  function SagepilotChatProvider({
1246
1716
  children,
@@ -1249,13 +1719,292 @@ function SagepilotChatProvider({
1249
1719
  }) {
1250
1720
  const [state, setState] = useState(readPresentationState);
1251
1721
  const [androidModalInsets, setAndroidModalInsets] = useState(emptyAndroidInsets);
1722
+ const [nativeWebViewKey, setNativeWebViewKey] = useState(0);
1723
+ const [preloadWebViewKey, setPreloadWebViewKey] = useState(0);
1252
1724
  const webFrameRef = useRef(null);
1253
1725
  const nativeWebViewRef = useRef(null);
1726
+ const pendingBatchesRef = useRef([]);
1727
+ const widgetReadyRef = useRef(false);
1728
+ const deliveryTimerRef = useRef(null);
1729
+ const deliveryAttemptsRef = useRef(0);
1730
+ const pendingPingRef = useRef(null);
1731
+ const pingTimeoutRef = useRef(null);
1732
+ const livenessRemountCountRef = useRef(0);
1733
+ const appStateRef = useRef(AppState.currentState);
1734
+ const didReconcileRef = useRef(false);
1735
+ const [androidRepaintTick, setAndroidRepaintTick] = useState(0);
1736
+ const [sourceChooser, setSourceChooser] = useState(null);
1737
+ const getPendingCache = useCallback(() => {
1738
+ const cacheStorage = internalSagepilotChat.getConfig()?.cacheStorage;
1739
+ if (!cacheStorage) return null;
1740
+ return createJsonCache(cacheStorage, PENDING_FILES_CACHE_NAMESPACE);
1741
+ }, []);
1742
+ const writeManifest = useCallback(() => {
1743
+ const cache = getPendingCache();
1744
+ if (!cache) return;
1745
+ const queue = pendingBatchesRef.current;
1746
+ if (queue.length === 0) {
1747
+ void cache.remove(PENDING_FILES_CACHE_KEY).catch(() => void 0);
1748
+ return;
1749
+ }
1750
+ const hasFileStore = Boolean(internalSagepilotChat.getConfig()?.fileStore);
1751
+ if (hasFileStore) {
1752
+ const manifest = queue.filter((batch) => batch.storageKeys && batch.storageKeys.length === batch.files.length).map((batch) => ({
1753
+ batchId: batch.batchId,
1754
+ files: batch.files.map((file, index) => ({
1755
+ file_name: file.file_name,
1756
+ mime_type: file.mime_type,
1757
+ size: file.size,
1758
+ storageKey: batch.storageKeys[index]
1759
+ }))
1760
+ }));
1761
+ if (manifest.length === 0) {
1762
+ void cache.remove(PENDING_FILES_CACHE_KEY).catch(() => void 0);
1763
+ } else {
1764
+ void cache.set(PENDING_FILES_CACHE_KEY, manifest).catch(() => void 0);
1765
+ }
1766
+ return;
1767
+ }
1768
+ const totalBytes = queue.reduce((sum, batch) => sum + totalBase64Bytes(batch.files), 0);
1769
+ if (totalBytes <= PERSIST_MAX_TOTAL_BYTES) {
1770
+ const manifest = queue.map((batch) => ({
1771
+ batchId: batch.batchId,
1772
+ files: batch.files.map((file) => ({
1773
+ file_name: file.file_name,
1774
+ mime_type: file.mime_type,
1775
+ size: file.size,
1776
+ data_base64: file.data_base64
1777
+ }))
1778
+ }));
1779
+ void cache.set(PENDING_FILES_CACHE_KEY, manifest).catch(() => void 0);
1780
+ } else {
1781
+ void cache.remove(PENDING_FILES_CACHE_KEY).catch(() => void 0);
1782
+ }
1783
+ }, [getPendingCache]);
1784
+ const writeBatchBytes = useCallback(async (batch) => {
1785
+ const fileStore = internalSagepilotChat.getConfig()?.fileStore;
1786
+ if (!fileStore) return;
1787
+ const entries = batch.files.map((file, index) => ({
1788
+ key: storageKeyFor(batch.batchId, index),
1789
+ base64: file.data_base64
1790
+ }));
1791
+ try {
1792
+ await Promise.all(entries.map((entry) => fileStore.write(entry.key, entry.base64)));
1793
+ } catch {
1794
+ return;
1795
+ }
1796
+ if (!pendingBatchesRef.current.some((queued) => queued.batchId === batch.batchId)) {
1797
+ void Promise.all(entries.map((entry) => fileStore.remove(entry.key).catch(() => void 0)));
1798
+ }
1799
+ }, []);
1800
+ const discardBatchFiles = useCallback((batch) => {
1801
+ if (!batch?.storageKeys) return;
1802
+ const fileStore = internalSagepilotChat.getConfig()?.fileStore;
1803
+ if (!fileStore) return;
1804
+ void Promise.all(batch.storageKeys.map((key) => fileStore.remove(key).catch(() => void 0)));
1805
+ }, []);
1806
+ const pumpDelivery = useCallback(() => {
1807
+ if (deliveryTimerRef.current) {
1808
+ clearTimeout(deliveryTimerRef.current);
1809
+ deliveryTimerRef.current = null;
1810
+ }
1811
+ const batch = pendingBatchesRef.current[0];
1812
+ if (!batch) return;
1813
+ deliveryAttemptsRef.current += 1;
1814
+ const attempts = deliveryAttemptsRef.current;
1815
+ const ref = nativeWebViewRef.current;
1816
+ const shouldDeliver = ref && (widgetReadyRef.current || attempts >= DELIVERY_LEGACY_FALLBACK_ATTEMPTS);
1817
+ if (shouldDeliver && ref) {
1818
+ ref.injectJavaScript(buildFilesPickedScript(batch.files, batch.batchId));
1819
+ }
1820
+ if (attempts < DELIVERY_MAX_ATTEMPTS) {
1821
+ deliveryTimerRef.current = setTimeout(() => pumpDelivery(), DELIVERY_RETRY_MS);
1822
+ }
1823
+ }, []);
1824
+ const startDelivery = useCallback(() => {
1825
+ deliveryAttemptsRef.current = 0;
1826
+ pumpDelivery();
1827
+ }, [pumpDelivery]);
1828
+ const ensureDelivery = useCallback(() => {
1829
+ if (pendingBatchesRef.current.length === 0) return;
1830
+ if (deliveryTimerRef.current) return;
1831
+ startDelivery();
1832
+ }, [startDelivery]);
1833
+ const acknowledgeHeadBatch = useCallback(() => {
1834
+ const head = pendingBatchesRef.current[0];
1835
+ pendingBatchesRef.current = pendingBatchesRef.current.slice(1);
1836
+ deliveryAttemptsRef.current = 0;
1837
+ if (deliveryTimerRef.current) {
1838
+ clearTimeout(deliveryTimerRef.current);
1839
+ deliveryTimerRef.current = null;
1840
+ }
1841
+ discardBatchFiles(head);
1842
+ writeManifest();
1843
+ if (pendingBatchesRef.current.length > 0) startDelivery();
1844
+ }, [discardBatchFiles, writeManifest, startDelivery]);
1845
+ const queuePickedFiles = useCallback((files) => {
1846
+ const batchId = nextBatchId();
1847
+ const hasFileStore = Boolean(internalSagepilotChat.getConfig()?.fileStore);
1848
+ const storageKeys = hasFileStore ? files.map((_, index) => storageKeyFor(batchId, index)) : void 0;
1849
+ const batch = { batchId, files, storageKeys };
1850
+ pendingBatchesRef.current = [...pendingBatchesRef.current, batch];
1851
+ ensureDelivery();
1852
+ writeManifest();
1853
+ void writeBatchBytes(batch);
1854
+ }, [ensureDelivery, writeManifest, writeBatchBytes]);
1855
+ const recoverNativeWebView = useCallback(() => {
1856
+ widgetReadyRef.current = false;
1857
+ setNativeWebViewKey((key) => key + 1);
1858
+ }, []);
1859
+ const recoverPreloadWebView = useCallback(() => {
1860
+ setPreloadWebViewKey((key) => key + 1);
1861
+ }, []);
1862
+ const runNativeFilePicker = useCallback((source, multiple) => {
1863
+ const filePicker = internalSagepilotChat.getConfig()?.filePicker;
1864
+ if (!filePicker) return;
1865
+ filePicker.pickFiles({ source, multiple }).then((files) => {
1866
+ if (files.length === 0) {
1867
+ nativeWebViewRef.current?.injectJavaScript(buildFilePickerCancelledScript());
1868
+ return;
1869
+ }
1870
+ queuePickedFiles(files);
1871
+ }).catch((error) => {
1872
+ const { message, code } = readFilePickerError(error);
1873
+ nativeWebViewRef.current?.injectJavaScript(buildFilePickerErrorScript(message, code));
1874
+ });
1875
+ }, [queuePickedFiles]);
1876
+ const openNativeFilePicker = useCallback((multiple) => {
1877
+ const filePicker = internalSagepilotChat.getConfig()?.filePicker;
1878
+ if (!filePicker || filePicker.sources.length === 0) return;
1879
+ const [onlySource] = filePicker.sources;
1880
+ if (filePicker.sources.length === 1 && onlySource) {
1881
+ runNativeFilePicker(onlySource, multiple);
1882
+ return;
1883
+ }
1884
+ setSourceChooser({ multiple });
1885
+ }, [runNativeFilePicker]);
1886
+ const handleSourceChoice = useCallback((source) => {
1887
+ const chooser = sourceChooser;
1888
+ setSourceChooser(null);
1889
+ if (chooser) runNativeFilePicker(source, chooser.multiple);
1890
+ }, [sourceChooser, runNativeFilePicker]);
1254
1891
  useEffect(() => {
1255
1892
  return internalSagepilotChat.onStateChange(() => {
1256
1893
  setState(readPresentationState());
1257
1894
  });
1258
1895
  }, []);
1896
+ useEffect(() => {
1897
+ let cancelled = false;
1898
+ const cache = getPendingCache();
1899
+ if (!cache) return;
1900
+ const reconcile = async () => {
1901
+ if (didReconcileRef.current) return;
1902
+ const manifest = await cache.get(PENDING_FILES_CACHE_KEY).catch(() => null);
1903
+ if (cancelled || didReconcileRef.current || !Array.isArray(manifest) || manifest.length === 0) return;
1904
+ const fileStore = internalSagepilotChat.getConfig()?.fileStore;
1905
+ const restored = [];
1906
+ for (const batch of manifest) {
1907
+ if (!batch || typeof batch.batchId !== "string" || !Array.isArray(batch.files) || batch.files.length === 0) continue;
1908
+ const files = [];
1909
+ const storageKeys = [];
1910
+ let intact = true;
1911
+ for (const file of batch.files) {
1912
+ let dataBase64 = null;
1913
+ if (typeof file.storageKey === "string" && fileStore) {
1914
+ try {
1915
+ dataBase64 = await fileStore.read(file.storageKey);
1916
+ storageKeys.push(file.storageKey);
1917
+ } catch {
1918
+ intact = false;
1919
+ }
1920
+ } else if (typeof file.data_base64 === "string" && file.data_base64) {
1921
+ dataBase64 = file.data_base64;
1922
+ }
1923
+ if (!dataBase64) {
1924
+ intact = false;
1925
+ break;
1926
+ }
1927
+ files.push({ file_name: file.file_name, mime_type: file.mime_type, size: file.size, data_base64: dataBase64 });
1928
+ }
1929
+ if (intact && files.length === batch.files.length) {
1930
+ restored.push({
1931
+ batchId: batch.batchId,
1932
+ files,
1933
+ storageKeys: storageKeys.length === files.length ? storageKeys : void 0
1934
+ });
1935
+ }
1936
+ }
1937
+ if (cancelled || didReconcileRef.current) return;
1938
+ didReconcileRef.current = true;
1939
+ pendingBatchesRef.current = [...restored, ...pendingBatchesRef.current];
1940
+ writeManifest();
1941
+ if (fileStore) {
1942
+ const keep = pendingBatchesRef.current.flatMap(
1943
+ (batch) => batch.files.map((_, index) => storageKeyFor(batch.batchId, index))
1944
+ );
1945
+ void fileStore.prune(keep).catch(() => void 0);
1946
+ }
1947
+ startDelivery();
1948
+ };
1949
+ void reconcile();
1950
+ return () => {
1951
+ cancelled = true;
1952
+ };
1953
+ }, [getPendingCache, startDelivery, writeManifest]);
1954
+ useEffect(() => {
1955
+ return () => {
1956
+ if (deliveryTimerRef.current) {
1957
+ clearTimeout(deliveryTimerRef.current);
1958
+ deliveryTimerRef.current = null;
1959
+ }
1960
+ if (pingTimeoutRef.current) {
1961
+ clearTimeout(pingTimeoutRef.current);
1962
+ pingTimeoutRef.current = null;
1963
+ }
1964
+ };
1965
+ }, []);
1966
+ const clearPing = useCallback(() => {
1967
+ pendingPingRef.current = null;
1968
+ if (pingTimeoutRef.current) {
1969
+ clearTimeout(pingTimeoutRef.current);
1970
+ pingTimeoutRef.current = null;
1971
+ }
1972
+ }, []);
1973
+ const runLivenessProbe = useCallback(() => {
1974
+ if (Platform2.OS !== "android") return;
1975
+ const ref = nativeWebViewRef.current;
1976
+ if (!ref || !internalSagepilotChat.isPresented()) return;
1977
+ setAndroidRepaintTick((tick) => tick + 1);
1978
+ const attempts = (pendingPingRef.current?.attempts ?? 0) + 1;
1979
+ const nonce = `${Date.now().toString(36)}_${attempts}`;
1980
+ pendingPingRef.current = { nonce, attempts };
1981
+ ref.injectJavaScript(buildLivenessPingScript(nonce, LIVENESS_HEARTBEAT_STALE_MS));
1982
+ if (pingTimeoutRef.current) clearTimeout(pingTimeoutRef.current);
1983
+ pingTimeoutRef.current = setTimeout(() => {
1984
+ if (attempts >= LIVENESS_MAX_PING_ATTEMPTS) {
1985
+ clearPing();
1986
+ recoverNativeWebView();
1987
+ return;
1988
+ }
1989
+ runLivenessProbe();
1990
+ }, LIVENESS_PONG_TIMEOUT_MS);
1991
+ }, [clearPing, recoverNativeWebView]);
1992
+ useEffect(() => {
1993
+ const subscription = AppState.addEventListener("change", (nextState) => {
1994
+ const prev = appStateRef.current;
1995
+ appStateRef.current = nextState;
1996
+ if (nextState === "active" && (prev === "background" || prev === "inactive")) {
1997
+ clearPing();
1998
+ livenessRemountCountRef.current = 0;
1999
+ runLivenessProbe();
2000
+ ensureDelivery();
2001
+ }
2002
+ });
2003
+ return () => {
2004
+ subscription.remove();
2005
+ clearPing();
2006
+ };
2007
+ }, [clearPing, runLivenessProbe, ensureDelivery]);
1259
2008
  const handleAndroidInsetsChange = useCallback((event) => {
1260
2009
  const nextBottomInset = event.nativeEvent?.bottom;
1261
2010
  const nextTopInset = event.nativeEvent?.top;
@@ -1269,11 +2018,11 @@ function SagepilotChatProvider({
1269
2018
  const presentationStyle = state.presentation?.style ?? "sheet";
1270
2019
  const isFullScreenModal = presentationStyle === "fullScreen";
1271
2020
  const animationType = presentationStyle === "fullScreen" || presentationStyle === "push" ? "slide" : "fade";
1272
- const ModalContainer = Platform.OS === "ios" && isFullScreenModal ? SafeAreaView : View;
1273
- const NativeModalContainer = Platform.OS === "android" ? AndroidInsetsView : ModalContainer;
1274
- const ChatContentContainer = Platform.OS === "ios" ? KeyboardAvoidingView : View;
1275
- const nativeModalContainerProps = Platform.OS === "android" ? { style: styles.container, onInsetsChange: handleAndroidInsetsChange } : { style: styles.container };
1276
- const chatContentContainerProps = Platform.OS === "ios" ? {
2021
+ const ModalContainer = Platform2.OS === "ios" && isFullScreenModal ? SafeAreaView : View;
2022
+ const NativeModalContainer = Platform2.OS === "android" ? AndroidInsetsView : ModalContainer;
2023
+ const ChatContentContainer = Platform2.OS === "ios" ? KeyboardAvoidingView : View;
2024
+ const nativeModalContainerProps = Platform2.OS === "android" ? { style: styles.container, onInsetsChange: handleAndroidInsetsChange } : { style: styles.container };
2025
+ const chatContentContainerProps = Platform2.OS === "ios" ? {
1277
2026
  behavior: "padding",
1278
2027
  enabled: true,
1279
2028
  keyboardVerticalOffset: 0,
@@ -1281,14 +2030,48 @@ function SagepilotChatProvider({
1281
2030
  } : {
1282
2031
  style: [
1283
2032
  styles.modalContent,
1284
- Platform.OS === "android" ? {
2033
+ Platform2.OS === "android" ? {
1285
2034
  paddingTop: androidModalInsets.top,
1286
2035
  paddingBottom: androidModalInsets.bottom
1287
2036
  } : null
1288
2037
  ].filter(Boolean)
1289
2038
  };
1290
2039
  const handleWebViewMessage = (event) => {
1291
- internalSagepilotChat.handleHostedBridgeMessage(parseHostedBridgeMessage(event.nativeEvent?.data));
2040
+ const message = parseHostedBridgeMessage(event.nativeEvent?.data);
2041
+ if (message?.type === "sagepilot:open_file_picker") {
2042
+ openNativeFilePicker(message.multiple ?? true);
2043
+ return;
2044
+ }
2045
+ if (message?.type === "sagepilot:widget_listener_ready") {
2046
+ widgetReadyRef.current = true;
2047
+ startDelivery();
2048
+ return;
2049
+ }
2050
+ if (message?.type === "sagepilot:files_received") {
2051
+ const ackBatchId = message.batch_id;
2052
+ const head = pendingBatchesRef.current[0];
2053
+ if (head && (!ackBatchId || ackBatchId === head.batchId)) {
2054
+ acknowledgeHeadBatch();
2055
+ }
2056
+ return;
2057
+ }
2058
+ if (message?.type === "sagepilot:pong") {
2059
+ const pending = pendingPingRef.current;
2060
+ if (pending && (!message.nonce || message.nonce === pending.nonce)) {
2061
+ if (message.alive === false) {
2062
+ clearPing();
2063
+ if (livenessRemountCountRef.current < LIVENESS_MAX_REMOUNTS) {
2064
+ livenessRemountCountRef.current += 1;
2065
+ recoverNativeWebView();
2066
+ }
2067
+ } else {
2068
+ livenessRemountCountRef.current = 0;
2069
+ clearPing();
2070
+ }
2071
+ }
2072
+ return;
2073
+ }
2074
+ internalSagepilotChat.handleHostedBridgeMessage(message);
1292
2075
  };
1293
2076
  const postIdentityToWebFrame = () => {
1294
2077
  const message = internalSagepilotChat.getHostedIdentityMessage();
@@ -1301,8 +2084,25 @@ function SagepilotChatProvider({
1301
2084
  if (!script || !nativeWebViewRef.current) return;
1302
2085
  nativeWebViewRef.current.injectJavaScript(script);
1303
2086
  };
2087
+ const handleNativeWebViewLoadEnd = () => {
2088
+ postIdentityToNativeWebView();
2089
+ widgetReadyRef.current = false;
2090
+ startDelivery();
2091
+ };
2092
+ const handleShouldStartLoadWithRequest = useCallback((request) => {
2093
+ const url = request?.url ?? "";
2094
+ if (!url || request?.isTopFrame === false || isInternalWebViewScheme(url)) {
2095
+ return true;
2096
+ }
2097
+ const widgetOrigin = readUrlOrigin(state.conversationUrl ?? state.preloadUrl);
2098
+ if (widgetOrigin && readUrlOrigin(url) === widgetOrigin) {
2099
+ return true;
2100
+ }
2101
+ Linking.openURL(url).catch(() => void 0);
2102
+ return false;
2103
+ }, [state.conversationUrl, state.preloadUrl]);
1304
2104
  useEffect(() => {
1305
- if (Platform.OS !== "web" || typeof window === "undefined") return;
2105
+ if (Platform2.OS !== "web" || typeof window === "undefined") return;
1306
2106
  const trustedWidgetOrigin = readUrlOrigin(state.conversationUrl);
1307
2107
  if (!trustedWidgetOrigin) return;
1308
2108
  const handleWindowMessage = (event) => {
@@ -1313,14 +2113,14 @@ function SagepilotChatProvider({
1313
2113
  return () => window.removeEventListener("message", handleWindowMessage);
1314
2114
  }, [state.conversationUrl]);
1315
2115
  useEffect(() => {
1316
- if (Platform.OS !== "web" || !state.isPresented) return;
2116
+ if (Platform2.OS !== "web" || !state.isPresented) return;
1317
2117
  postIdentityToWebFrame();
1318
2118
  }, [state.isPresented, state.conversationUrl, state.configured]);
1319
2119
  useEffect(() => {
1320
- if (Platform.OS === "web" || !state.isPresented) return;
2120
+ if (Platform2.OS === "web" || !state.isPresented) return;
1321
2121
  postIdentityToNativeWebView();
1322
2122
  }, [state.isPresented, state.conversationUrl, state.configured]);
1323
- if (Platform.OS === "web") {
2123
+ if (Platform2.OS === "web") {
1324
2124
  return createElement(
1325
2125
  View,
1326
2126
  { style: styles.root },
@@ -1363,10 +2163,18 @@ function SagepilotChatProvider({
1363
2163
  children,
1364
2164
  state.configured && !state.isPresented && state.shouldPreload && state.preloadUrl ? createElement(WebView, {
1365
2165
  ...hostedChatWebViewProps,
2166
+ // Separate key from the hosted WebView: a hidden-preload renderer crash
2167
+ // must not bump the hosted key (which would reset widgetReadyRef and
2168
+ // disrupt an in-flight delivery). onRenderProcessGone must still be
2169
+ // handled here or an unhandled renderer kill crashes the whole app.
2170
+ key: `sagepilot-preload-webview-${preloadWebViewKey}`,
1366
2171
  source: { uri: state.preloadUrl },
1367
2172
  style: styles.preloadWebview,
1368
2173
  injectedJavaScriptBeforeContentLoaded: getInjectedWebViewScript(),
1369
- onMessage: handleWebViewMessage
2174
+ onMessage: handleWebViewMessage,
2175
+ onShouldStartLoadWithRequest: handleShouldStartLoadWithRequest,
2176
+ onRenderProcessGone: recoverPreloadWebView,
2177
+ onContentProcessDidTerminate: recoverPreloadWebView
1370
2178
  }) : null,
1371
2179
  createElement(
1372
2180
  Modal,
@@ -1374,8 +2182,8 @@ function SagepilotChatProvider({
1374
2182
  visible: state.configured && state.isPresented,
1375
2183
  animationType,
1376
2184
  presentationStyle: isFullScreenModal ? "fullScreen" : "pageSheet",
1377
- statusBarTranslucent: Platform.OS === "android",
1378
- navigationBarTranslucent: Platform.OS === "android",
2185
+ statusBarTranslucent: Platform2.OS === "android",
2186
+ navigationBarTranslucent: Platform2.OS === "android",
1379
2187
  onRequestClose: () => internalSagepilotChat.dismiss()
1380
2188
  },
1381
2189
  createElement(
@@ -1400,13 +2208,23 @@ function SagepilotChatProvider({
1400
2208
  ) : null,
1401
2209
  state.conversationUrl ? createElement(WebView, {
1402
2210
  ...hostedChatWebViewProps,
2211
+ key: `sagepilot-hosted-webview-${nativeWebViewKey}`,
1403
2212
  ref: nativeWebViewRef,
1404
2213
  source: { uri: state.conversationUrl },
1405
- style: styles.webview,
2214
+ // The imperceptible opacity toggle forces the Android WebView
2215
+ // surface to re-composite on resume, clearing the blank-but-alive
2216
+ // surface bug without a reload (see runLivenessProbe).
2217
+ style: Platform2.OS === "android" ? [styles.webview, { opacity: 1 - androidRepaintTick % 2 * 1e-3 }] : styles.webview,
1406
2218
  startInLoadingState: true,
1407
2219
  injectedJavaScriptBeforeContentLoaded: getInjectedWebViewScript(),
1408
2220
  onMessage: handleWebViewMessage,
1409
- onLoadEnd: postIdentityToNativeWebView,
2221
+ onLoadEnd: handleNativeWebViewLoadEnd,
2222
+ onShouldStartLoadWithRequest: handleShouldStartLoadWithRequest,
2223
+ // Android: render process killed (commonly while the camera/file
2224
+ // chooser activity is foregrounded). Remount to recover.
2225
+ onRenderProcessGone: recoverNativeWebView,
2226
+ // iOS equivalent: WKWebView content process terminated.
2227
+ onContentProcessDidTerminate: recoverNativeWebView,
1410
2228
  renderLoading: () => createElement(
1411
2229
  View,
1412
2230
  { style: styles.loading },
@@ -1416,6 +2234,48 @@ function SagepilotChatProvider({
1416
2234
  }) : null
1417
2235
  )
1418
2236
  )
2237
+ ),
2238
+ // Native attachment-source chooser. Replaces the Android Alert (capped at
2239
+ // 3 buttons) so camera/library/documents all show on every platform.
2240
+ createElement(
2241
+ Modal,
2242
+ {
2243
+ visible: sourceChooser !== null,
2244
+ transparent: true,
2245
+ animationType: "fade",
2246
+ statusBarTranslucent: true,
2247
+ onRequestClose: () => setSourceChooser(null)
2248
+ },
2249
+ createElement(
2250
+ Pressable,
2251
+ { style: styles.sheetBackdrop, onPress: () => setSourceChooser(null) },
2252
+ createElement(
2253
+ View,
2254
+ { style: styles.sheetCard },
2255
+ createElement(Text, { style: styles.sheetTitle }, "Add attachment"),
2256
+ ...(internalSagepilotChat.getConfig()?.filePicker?.sources ?? []).map(
2257
+ (source) => createElement(
2258
+ Pressable,
2259
+ {
2260
+ key: source,
2261
+ accessibilityRole: "button",
2262
+ style: styles.sheetButton,
2263
+ onPress: () => handleSourceChoice(source)
2264
+ },
2265
+ createElement(Text, { style: styles.sheetButtonText }, FILE_PICKER_SOURCE_LABELS[source])
2266
+ )
2267
+ ),
2268
+ createElement(
2269
+ Pressable,
2270
+ {
2271
+ accessibilityRole: "button",
2272
+ style: [styles.sheetButton, styles.sheetCancelButton],
2273
+ onPress: () => setSourceChooser(null)
2274
+ },
2275
+ createElement(Text, { style: styles.sheetCancelText }, "Cancel")
2276
+ )
2277
+ )
2278
+ )
1419
2279
  )
1420
2280
  );
1421
2281
  }
@@ -1514,6 +2374,46 @@ var styles = StyleSheet.create({
1514
2374
  marginTop: 12,
1515
2375
  color: "#4b5563",
1516
2376
  fontSize: 14
2377
+ },
2378
+ sheetBackdrop: {
2379
+ flex: 1,
2380
+ justifyContent: "flex-end",
2381
+ backgroundColor: "rgba(17, 24, 39, 0.36)"
2382
+ },
2383
+ sheetCard: {
2384
+ backgroundColor: "#ffffff",
2385
+ borderTopLeftRadius: 16,
2386
+ borderTopRightRadius: 16,
2387
+ paddingTop: 8,
2388
+ paddingBottom: 24,
2389
+ paddingHorizontal: 8
2390
+ },
2391
+ sheetTitle: {
2392
+ textAlign: "center",
2393
+ color: "#6b7280",
2394
+ fontSize: 13,
2395
+ fontWeight: "600",
2396
+ paddingVertical: 10
2397
+ },
2398
+ sheetButton: {
2399
+ minHeight: 52,
2400
+ alignItems: "center",
2401
+ justifyContent: "center",
2402
+ borderRadius: 12
2403
+ },
2404
+ sheetButtonText: {
2405
+ color: "#111827",
2406
+ fontSize: 16,
2407
+ fontWeight: "500"
2408
+ },
2409
+ sheetCancelButton: {
2410
+ marginTop: 6,
2411
+ backgroundColor: "#f3f4f6"
2412
+ },
2413
+ sheetCancelText: {
2414
+ color: "#111827",
2415
+ fontSize: 16,
2416
+ fontWeight: "600"
1517
2417
  }
1518
2418
  });
1519
2419
 
@@ -1549,6 +2449,9 @@ function useSagepilotChat() {
1549
2449
  }, []);
1550
2450
  const logout = useCallback2(() => SagepilotChat.logout(), []);
1551
2451
  const getUnreadCount = useCallback2(() => SagepilotChat.getUnreadCount(), []);
2452
+ const onConversationCreated = useCallback2((callback) => {
2453
+ return SagepilotChat.onConversationCreated(callback);
2454
+ }, []);
1552
2455
  return {
1553
2456
  ...state,
1554
2457
  present,
@@ -1559,14 +2462,18 @@ function useSagepilotChat() {
1559
2462
  toggle,
1560
2463
  identify,
1561
2464
  logout,
1562
- getUnreadCount
2465
+ getUnreadCount,
2466
+ onConversationCreated
1563
2467
  };
1564
2468
  }
1565
2469
  export {
1566
2470
  SagepilotChat,
1567
2471
  SagepilotChatError,
1568
2472
  SagepilotChatProvider,
2473
+ SagepilotFilePickerError,
1569
2474
  createAsyncStorageCacheStorage,
1570
2475
  createKeychainTokenStorage,
2476
+ createSagepilotFilePicker,
2477
+ createSagepilotFileStore,
1571
2478
  useSagepilotChat
1572
2479
  };