@paymanai/payman-ask-sdk 4.0.19 → 4.0.21

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.
@@ -731,7 +731,8 @@ function buildRequestBody(config, userMessage, sessionId, options) {
731
731
  sessionAttributes,
732
732
  analysisMode: options?.analysisMode,
733
733
  locale: resolveLocale(config.locale),
734
- timezone: resolveTimezone(config.timezone)
734
+ timezone: resolveTimezone(config.timezone),
735
+ ...options?.attachments?.length ? { attachments: options.attachments } : {}
735
736
  };
736
737
  }
737
738
  function resolveLocale(configured) {
@@ -776,6 +777,7 @@ function buildResolveImagesUrl(config) {
776
777
  const basePath = normalizedEndpointPath.endsWith("/stream") ? normalizedEndpointPath.slice(0, -"/stream".length) : normalizedEndpointPath;
777
778
  return `${config.api.baseUrl}${basePath}/resolve-image-urls`;
778
779
  }
780
+ var NGROK_SKIP_BROWSER_WARNING = "ngrok-skip-browser-warning";
779
781
  function buildRequestHeaders(config) {
780
782
  const headers = {
781
783
  ...config.api.headers
@@ -783,6 +785,9 @@ function buildRequestHeaders(config) {
783
785
  if (config.api.authToken) {
784
786
  headers.Authorization = `Bearer ${config.api.authToken}`;
785
787
  }
788
+ if (!headers[NGROK_SKIP_BROWSER_WARNING]) {
789
+ headers[NGROK_SKIP_BROWSER_WARNING] = "true";
790
+ }
786
791
  return headers;
787
792
  }
788
793
  var RAG_IMAGE_REGEX = /\/api\/rag\/chunks\/[^"'\s]+\/image/;
@@ -1063,6 +1068,81 @@ function createCancelledMessageUpdate(steps, currentMessage) {
1063
1068
  currentMessage: currentMessage || "Thinking..."
1064
1069
  };
1065
1070
  }
1071
+ var DEFAULT_SIGNED_URL_ENDPOINT = "/api/files/signed-url";
1072
+ function fileExtension(filename) {
1073
+ const ext = filename.split(".").pop()?.toLowerCase().replace(/^\./, "");
1074
+ return ext && ext.length > 0 ? ext : "bin";
1075
+ }
1076
+ function resolveMimeType(file) {
1077
+ if (file.type && file.type.trim().length > 0) return file.type;
1078
+ const ext = fileExtension(file.name);
1079
+ const byExt = {
1080
+ pdf: "application/pdf",
1081
+ png: "image/png",
1082
+ jpg: "image/jpeg",
1083
+ jpeg: "image/jpeg",
1084
+ gif: "image/gif",
1085
+ webp: "image/webp"
1086
+ };
1087
+ return byExt[ext] ?? "application/octet-stream";
1088
+ }
1089
+ function buildSignedUrlEndpoint(config, ext) {
1090
+ const endpoint = config.api.signedUrlEndpoint || DEFAULT_SIGNED_URL_ENDPOINT;
1091
+ const queryParams = new URLSearchParams({ extn: ext.replace(/^\./, "") });
1092
+ return `${config.api.baseUrl}${endpoint}?${queryParams.toString()}`;
1093
+ }
1094
+ async function requestSignedUrl(config, ext, signal) {
1095
+ const url = buildSignedUrlEndpoint(config, ext);
1096
+ const headers = buildRequestHeaders(config);
1097
+ const response = await fetch(url, {
1098
+ method: "GET",
1099
+ headers,
1100
+ signal
1101
+ });
1102
+ if (!response.ok) {
1103
+ const errorText = await response.text().catch(() => "");
1104
+ throw new Error(
1105
+ errorText ? `Failed to get upload URL (${response.status}): ${errorText}` : `Failed to get upload URL (${response.status})`
1106
+ );
1107
+ }
1108
+ const data = await response.json();
1109
+ if (!data.key || !data.url) {
1110
+ throw new Error("Signed URL response missing key or url");
1111
+ }
1112
+ return { key: data.key, url: data.url };
1113
+ }
1114
+ async function uploadToSignedUrl(signedUrl, file, mimeType, signal) {
1115
+ const response = await fetch(signedUrl, {
1116
+ method: "PUT",
1117
+ headers: {
1118
+ "Content-Type": mimeType,
1119
+ "x-ms-blob-type": "BlockBlob"
1120
+ },
1121
+ body: file,
1122
+ signal
1123
+ });
1124
+ if (!response.ok) {
1125
+ const errorText = await response.text().catch(() => "");
1126
+ throw new Error(
1127
+ errorText ? `Failed to upload file (${response.status}): ${errorText}` : `Failed to upload file (${response.status})`
1128
+ );
1129
+ }
1130
+ }
1131
+ async function uploadAttachment(config, file, signal) {
1132
+ const ext = fileExtension(file.name);
1133
+ const mimeType = resolveMimeType(file);
1134
+ const { key, url } = await requestSignedUrl(config, ext, signal);
1135
+ await uploadToSignedUrl(url, file, mimeType, signal);
1136
+ return {
1137
+ tempKey: key,
1138
+ filename: file.name,
1139
+ mimeType
1140
+ };
1141
+ }
1142
+ async function uploadAttachments(config, files, signal) {
1143
+ const uploads = files.map((file) => uploadAttachment(config, file, signal));
1144
+ return Promise.all(uploads);
1145
+ }
1066
1146
  var UserActionStaleError = class extends Error {
1067
1147
  constructor(userActionId, message = "User action is no longer actionable") {
1068
1148
  super(message);
@@ -1099,9 +1179,6 @@ async function cancelUserAction(config, userActionId) {
1099
1179
  async function resendUserAction(config, userActionId) {
1100
1180
  return sendUserActionRequest(config, userActionId, "resend");
1101
1181
  }
1102
- async function expireUserAction(config, userActionId) {
1103
- return sendUserActionRequest(config, userActionId, "expired");
1104
- }
1105
1182
  var EMPTY_USER_ACTION_STATE = { prompts: [], notifications: [] };
1106
1183
  function upsertPrompt(prompts, req) {
1107
1184
  const active = { ...req, status: "pending" };
@@ -1130,12 +1207,32 @@ function getStoredOrInitialMessages(config) {
1130
1207
  function getSessionIdFromMessages(messages) {
1131
1208
  return messages.find((message) => message.sessionId)?.sessionId;
1132
1209
  }
1210
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set(["jpg", "jpeg", "png", "gif", "webp", "avif"]);
1211
+ function attachmentKindFromFile(file) {
1212
+ const ext = file.name.split(".").pop()?.toLowerCase() ?? "";
1213
+ if (IMAGE_EXTENSIONS.has(ext) || file.type.startsWith("image/")) return "image";
1214
+ return "file";
1215
+ }
1216
+ function buildMessageAttachments(files) {
1217
+ return files.map((file, index) => {
1218
+ const kind = attachmentKindFromFile(file);
1219
+ return {
1220
+ id: `att-${Date.now()}-${index}`,
1221
+ filename: file.name,
1222
+ mimeType: file.type || "application/octet-stream",
1223
+ // Blob URL for all local files so PDFs/docs stay previewable after send.
1224
+ previewUrl: URL.createObjectURL(file),
1225
+ kind
1226
+ };
1227
+ });
1228
+ }
1133
1229
  function useChatV2(config, callbacks = {}) {
1134
1230
  const [messages, setMessages] = react.useState(() => getStoredOrInitialMessages(config));
1135
1231
  const [isWaitingForResponse, setIsWaitingForResponse] = react.useState(() => {
1136
1232
  if (!config.userId) return false;
1137
1233
  return activeStreamStore.get(config.userId)?.isWaiting ?? false;
1138
1234
  });
1235
+ const [isUploadingAttachments, setIsUploadingAttachments] = react.useState(false);
1139
1236
  const sessionIdRef = react.useRef(
1140
1237
  getSessionIdFromMessages(getStoredOrInitialMessages(config)) ?? config.initialSessionId ?? void 0
1141
1238
  );
@@ -1210,21 +1307,45 @@ function useChatV2(config, callbacks = {}) {
1210
1307
  );
1211
1308
  const sendMessage = react.useCallback(
1212
1309
  async (userMessage, options) => {
1213
- if (!userMessage.trim()) return;
1310
+ const trimmedMessage = userMessage.trim();
1311
+ const files = options?.files ?? [];
1312
+ const hasPreuploadedAttachments = (options?.attachments?.length ?? 0) > 0;
1313
+ if (!trimmedMessage && files.length === 0 && !hasPreuploadedAttachments) return;
1314
+ let streamAttachments = options?.attachments;
1315
+ if (!streamAttachments && files.length > 0) {
1316
+ setIsUploadingAttachments(true);
1317
+ try {
1318
+ streamAttachments = await uploadAttachments(configRef.current, files);
1319
+ } catch (error) {
1320
+ if (error.name !== "AbortError") {
1321
+ callbacksRef.current.onError?.(error);
1322
+ }
1323
+ throw error;
1324
+ } finally {
1325
+ setIsUploadingAttachments(false);
1326
+ }
1327
+ }
1214
1328
  if (!sessionIdRef.current && configRef.current.autoGenerateSessionId !== false) {
1215
1329
  sessionIdRef.current = generateId();
1216
1330
  callbacksRef.current.onSessionIdChange?.(sessionIdRef.current);
1217
1331
  }
1332
+ const messageAttachments = files.length > 0 ? buildMessageAttachments(files) : streamAttachments?.length ? streamAttachments.map((attachment, index) => ({
1333
+ id: `att-${Date.now()}-${index}`,
1334
+ filename: attachment.filename,
1335
+ mimeType: attachment.mimeType,
1336
+ kind: attachment.mimeType.startsWith("image/") ? "image" : "file"
1337
+ })) : void 0;
1218
1338
  const userMessageId = `user-${Date.now()}`;
1219
1339
  const userMsg = {
1220
1340
  id: userMessageId,
1221
1341
  sessionId: sessionIdRef.current,
1222
1342
  role: "user",
1223
- content: userMessage,
1224
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1343
+ content: trimmedMessage,
1344
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1345
+ attachments: messageAttachments
1225
1346
  };
1226
1347
  setMessages((prev) => [...prev, userMsg]);
1227
- callbacksRef.current.onMessageSent?.(userMessage);
1348
+ callbacksRef.current.onMessageSent?.(trimmedMessage);
1228
1349
  setIsWaitingForResponse(true);
1229
1350
  callbacksRef.current.onStreamStart?.();
1230
1351
  const streamingId = `assistant-${Date.now()}`;
@@ -1251,11 +1372,14 @@ function useChatV2(config, callbacks = {}) {
1251
1372
  activeStreamStore.start(userId, abortController, initialMessages);
1252
1373
  }
1253
1374
  const newSessionId = await startStream(
1254
- userMessage,
1375
+ trimmedMessage,
1255
1376
  streamingId,
1256
1377
  sessionIdRef.current,
1257
1378
  abortController,
1258
- options
1379
+ {
1380
+ analysisMode: options?.analysisMode,
1381
+ attachments: streamAttachments
1382
+ }
1259
1383
  );
1260
1384
  const finalStreamUserId = streamUserIdRef.current ?? userId;
1261
1385
  if (finalStreamUserId) {
@@ -1393,19 +1517,6 @@ function useChatV2(config, callbacks = {}) {
1393
1517
  },
1394
1518
  [setPromptStatus]
1395
1519
  );
1396
- const expireUserAction2 = react.useCallback(
1397
- async (userActionId) => {
1398
- setPromptStatus(userActionId, "expired");
1399
- try {
1400
- await expireUserAction(configRef.current, userActionId);
1401
- } catch (error) {
1402
- if (error instanceof UserActionStaleError) return;
1403
- callbacksRef.current.onError?.(error);
1404
- throw error;
1405
- }
1406
- },
1407
- [setPromptStatus]
1408
- );
1409
1520
  const dismissNotification = react.useCallback((id) => {
1410
1521
  setUserActionState((prev) => ({
1411
1522
  ...prev,
@@ -1446,6 +1557,18 @@ function useChatV2(config, callbacks = {}) {
1446
1557
  setMessages(config.initialMessages);
1447
1558
  sessionIdRef.current = getSessionIdFromMessages(config.initialMessages) ?? config.initialSessionId;
1448
1559
  }, [config.initialMessages, config.initialSessionId, config.userId]);
1560
+ const hydratedSessionIdRef = react.useRef(void 0);
1561
+ react.useEffect(() => {
1562
+ if (config.userId) return;
1563
+ if (!config.initialMessages?.length) return;
1564
+ const sessionId = config.initialSessionId;
1565
+ if (hydratedSessionIdRef.current === sessionId && messagesRef.current.length > 0) {
1566
+ return;
1567
+ }
1568
+ hydratedSessionIdRef.current = sessionId;
1569
+ setMessages(config.initialMessages);
1570
+ sessionIdRef.current = getSessionIdFromMessages(config.initialMessages) ?? sessionId;
1571
+ }, [config.initialMessages, config.initialSessionId, config.userId]);
1449
1572
  react.useEffect(() => {
1450
1573
  const prevUserId = prevUserIdRef.current;
1451
1574
  prevUserIdRef.current = config.userId;
@@ -1480,12 +1603,12 @@ function useChatV2(config, callbacks = {}) {
1480
1603
  getSessionId,
1481
1604
  getMessages,
1482
1605
  isWaitingForResponse,
1606
+ isUploadingAttachments,
1483
1607
  sessionId: sessionIdRef.current,
1484
1608
  userActionState,
1485
1609
  submitUserAction: submitUserAction2,
1486
1610
  cancelUserAction: cancelUserAction2,
1487
1611
  resendUserAction: resendUserAction2,
1488
- expireUserAction: expireUserAction2,
1489
1612
  dismissNotification
1490
1613
  };
1491
1614
  }
@@ -2803,6 +2926,7 @@ function UserActionSheet({
2803
2926
  react.useEffect(() => {
2804
2927
  minimizeRef.current = onMinimize;
2805
2928
  });
2929
+ const [mounted, setMounted] = react.useState(open);
2806
2930
  const [kbHeight, setKbHeight] = react.useState(0);
2807
2931
  react.useEffect(() => {
2808
2932
  const showEvt = reactNative.Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow";
@@ -2816,16 +2940,24 @@ function UserActionSheet({
2816
2940
  }, []);
2817
2941
  react.useEffect(() => {
2818
2942
  if (open) {
2819
- reactNative.Animated.parallel([
2820
- reactNative.Animated.spring(translateY, { toValue: 0, useNativeDriver: true, damping: 30, stiffness: 360, mass: 0.85 }),
2821
- reactNative.Animated.timing(backdrop, { toValue: 1, duration: 190, useNativeDriver: true })
2822
- ]).start();
2823
- } else {
2824
- reactNative.Animated.parallel([
2825
- reactNative.Animated.timing(translateY, { toValue: screenH, duration: 210, easing: reactNative.Easing.in(reactNative.Easing.cubic), useNativeDriver: true }),
2826
- reactNative.Animated.timing(backdrop, { toValue: 0, duration: 170, useNativeDriver: true })
2827
- ]).start();
2943
+ setMounted(true);
2944
+ translateY.setValue(screenH);
2945
+ backdrop.setValue(0);
2946
+ const id = requestAnimationFrame(() => {
2947
+ reactNative.Animated.parallel([
2948
+ reactNative.Animated.spring(translateY, { toValue: 0, useNativeDriver: true, damping: 30, stiffness: 360, mass: 0.85 }),
2949
+ reactNative.Animated.timing(backdrop, { toValue: 1, duration: 190, useNativeDriver: true })
2950
+ ]).start();
2951
+ });
2952
+ return () => cancelAnimationFrame(id);
2828
2953
  }
2954
+ reactNative.Animated.parallel([
2955
+ reactNative.Animated.timing(translateY, { toValue: screenH, duration: 230, easing: reactNative.Easing.in(reactNative.Easing.cubic), useNativeDriver: true }),
2956
+ reactNative.Animated.timing(backdrop, { toValue: 0, duration: 180, useNativeDriver: true })
2957
+ ]).start(({ finished }) => {
2958
+ if (finished) setMounted(false);
2959
+ });
2960
+ return void 0;
2829
2961
  }, [open, translateY, backdrop, screenH]);
2830
2962
  const pan = react.useRef(
2831
2963
  reactNative.PanResponder.create({
@@ -2843,66 +2975,77 @@ function UserActionSheet({
2843
2975
  })
2844
2976
  ).current;
2845
2977
  const a = active ?? shown;
2846
- if (!a) return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { pointerEvents: "none" });
2978
+ if (!mounted || !a) return null;
2847
2979
  const isVerification = a.kind === "verification";
2848
2980
  const title = isVerification ? "Verification required" : "Action required";
2849
2981
  const subtitle = isVerification ? "Confirm the one-time code to continue" : "Review the details and confirm to continue";
2850
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [reactNative.StyleSheet.absoluteFill, sht.overlay], pointerEvents: open ? "auto" : "none", children: [
2851
- /* @__PURE__ */ jsxRuntime.jsx(
2852
- reactNative.Animated.View,
2853
- {
2854
- pointerEvents: "none",
2855
- style: [sht.backdrop, { backgroundColor: pal.overlay, opacity: backdrop }]
2856
- }
2857
- ),
2858
- open ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { style: reactNative.StyleSheet.absoluteFill, onPress: () => minimizeRef.current(), accessibilityLabel: "Dismiss" }) : null,
2859
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [sht.kav, { paddingBottom: kbHeight }], pointerEvents: "box-none", children: /* @__PURE__ */ jsxRuntime.jsxs(
2860
- reactNative.Animated.View,
2861
- {
2862
- style: [
2863
- sht.sheet,
2982
+ return /* @__PURE__ */ jsxRuntime.jsx(
2983
+ reactNative.Modal,
2984
+ {
2985
+ visible: mounted,
2986
+ transparent: true,
2987
+ animationType: "none",
2988
+ statusBarTranslucent: true,
2989
+ onRequestClose: () => minimizeRef.current(),
2990
+ children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: reactNative.StyleSheet.absoluteFill, pointerEvents: open ? "auto" : "none", children: [
2991
+ /* @__PURE__ */ jsxRuntime.jsx(
2992
+ reactNative.Animated.View,
2864
2993
  {
2865
- backgroundColor: pal.sheetBg,
2866
- borderColor: pal.border,
2867
- // Cap to the space above the keyboard (and a top margin) so the
2868
- // header stays visible and the footer sits above the keyboard;
2869
- // the field area scrolls within whatever's left.
2870
- maxHeight: Math.min(screenH * 0.9, screenH - kbHeight - 72),
2871
- paddingBottom: kbHeight > 0 ? 16 : Math.max(insetBottom, 16) + 6,
2872
- transform: [{ translateY }]
2994
+ pointerEvents: "none",
2995
+ style: [sht.backdrop, { backgroundColor: pal.overlay, opacity: backdrop }]
2873
2996
  }
2874
- ],
2875
- children: [
2876
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ...pan.panHandlers, style: sht.handleZone, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [sht.grabber, { backgroundColor: pal.grabber }] }) }),
2877
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: sht.header, children: [
2878
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [sht.iconCircle, { backgroundColor: accent + (isDark ? "26" : "14") }], children: isVerification ? /* @__PURE__ */ jsxRuntime.jsx(lucideReactNative.ShieldCheck, { size: 20, color: accent, strokeWidth: 2 }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReactNative.Pencil, { size: 18, color: accent, strokeWidth: 2 }) }),
2879
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: sht.headerText, children: [
2880
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [sht.title, { color: pal.text }], children: title }),
2881
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [sht.subtitle, { color: pal.muted }], numberOfLines: 1, children: subtitle })
2882
- ] }),
2883
- /* @__PURE__ */ jsxRuntime.jsx(SheetTimerPill, { prompt: a, accent, isDark }),
2884
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: () => minimizeRef.current(), hitSlop: 10, style: [sht.closeBtn, { backgroundColor: pal.fieldBg }], children: /* @__PURE__ */ jsxRuntime.jsx(lucideReactNative.X, { size: 16, color: pal.muted, strokeWidth: 2.2 }) })
2885
- ] }),
2886
- a.status === "stale" ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: sht.simpleBody, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [ub.desc, { color: pal.muted }], children: "This request is no longer available." }) }) : expired ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: sht.simpleBody, children: [
2887
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [ub.desc, { color: pal.muted }], children: isVerification ? "This verification request expired." : "This request expired." }),
2888
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: ub.actions, children: /* @__PURE__ */ jsxRuntime.jsx(PrimaryButton, { label: "Close", onPress: () => void onCancel(a.userActionId), accent }) })
2889
- ] }) : isVerification ? /* @__PURE__ */ jsxRuntime.jsx(
2890
- VerificationBody,
2891
- {
2892
- prompt: a,
2893
- expired,
2894
- pal,
2895
- accent,
2896
- onSubmit,
2897
- onCancel,
2898
- onResend
2899
- },
2900
- a.userActionId
2901
- ) : /* @__PURE__ */ jsxRuntime.jsx(FormBody, { prompt: a, pal, accent, isDark, onSubmit, onCancel }, a.userActionId)
2902
- ]
2903
- }
2904
- ) })
2905
- ] });
2997
+ ),
2998
+ open ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { style: reactNative.StyleSheet.absoluteFill, onPress: () => minimizeRef.current(), accessibilityLabel: "Dismiss" }) : null,
2999
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [sht.kav, { paddingBottom: kbHeight }], pointerEvents: "box-none", children: /* @__PURE__ */ jsxRuntime.jsxs(
3000
+ reactNative.Animated.View,
3001
+ {
3002
+ style: [
3003
+ sht.sheet,
3004
+ {
3005
+ backgroundColor: pal.sheetBg,
3006
+ borderColor: pal.border,
3007
+ // Cap to the space above the keyboard (and a top margin) so the
3008
+ // header stays visible and the footer sits above the keyboard;
3009
+ // the field area scrolls within whatever's left.
3010
+ maxHeight: Math.min(screenH * 0.9, screenH - kbHeight - 72),
3011
+ paddingBottom: kbHeight > 0 ? 16 : Math.max(insetBottom, 16) + 6,
3012
+ transform: [{ translateY }]
3013
+ }
3014
+ ],
3015
+ children: [
3016
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ...pan.panHandlers, style: sht.handleZone, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [sht.grabber, { backgroundColor: pal.grabber }] }) }),
3017
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: sht.header, children: [
3018
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [sht.iconCircle, { backgroundColor: accent + (isDark ? "26" : "14") }], children: isVerification ? /* @__PURE__ */ jsxRuntime.jsx(lucideReactNative.ShieldCheck, { size: 19, color: accent, strokeWidth: 2.1 }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReactNative.Pencil, { size: 17, color: accent, strokeWidth: 2.1 }) }),
3019
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: sht.headerText, children: [
3020
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [sht.title, { color: pal.text }], numberOfLines: 1, children: title }),
3021
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [sht.subtitle, { color: pal.muted }], numberOfLines: 1, children: subtitle })
3022
+ ] }),
3023
+ /* @__PURE__ */ jsxRuntime.jsx(SheetTimerPill, { prompt: a, accent, isDark }),
3024
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: () => minimizeRef.current(), hitSlop: 10, style: [sht.closeBtn, { backgroundColor: pal.fieldBg }], children: /* @__PURE__ */ jsxRuntime.jsx(lucideReactNative.X, { size: 15, color: pal.muted, strokeWidth: 2.3 }) })
3025
+ ] }),
3026
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [sht.headerRule, { backgroundColor: pal.border }] }),
3027
+ a.status === "stale" ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: sht.simpleBody, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [ub.desc, { color: pal.muted }], children: "This request is no longer available." }) }) : expired ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: sht.simpleBody, children: [
3028
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [ub.desc, { color: pal.muted }], children: isVerification ? "This verification request expired." : "This request expired." }),
3029
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: ub.actions, children: /* @__PURE__ */ jsxRuntime.jsx(PrimaryButton, { label: "Close", onPress: () => void onCancel(a.userActionId), accent }) })
3030
+ ] }) : isVerification ? /* @__PURE__ */ jsxRuntime.jsx(
3031
+ VerificationBody,
3032
+ {
3033
+ prompt: a,
3034
+ expired,
3035
+ pal,
3036
+ accent,
3037
+ onSubmit,
3038
+ onCancel,
3039
+ onResend
3040
+ },
3041
+ a.userActionId
3042
+ ) : /* @__PURE__ */ jsxRuntime.jsx(FormBody, { prompt: a, pal, accent, isDark, onSubmit, onCancel }, a.userActionId)
3043
+ ]
3044
+ }
3045
+ ) })
3046
+ ] })
3047
+ }
3048
+ );
2906
3049
  }
2907
3050
  function UserActionReopenCard({
2908
3051
  active,
@@ -2962,35 +3105,35 @@ var ub = reactNative.StyleSheet.create({
2962
3105
  // forms (no scroll), shrinks and scrolls only when the form exceeds the sheet.
2963
3106
  fieldsScroll: { flexShrink: 1 },
2964
3107
  fieldsContent: { paddingBottom: 6 },
2965
- messageBlock: { marginBottom: 18, marginTop: 2 },
2966
- desc: { fontSize: 14.5, lineHeight: 21, marginBottom: 16 },
2967
- form: { gap: 18 },
2968
- field: { gap: 7 },
2969
- label: { fontSize: 14, fontWeight: "600" },
2970
- hint: { fontSize: 12.5, lineHeight: 17 },
3108
+ messageBlock: { marginBottom: 14, marginTop: 0 },
3109
+ desc: { fontSize: 14, lineHeight: 20, marginBottom: 14 },
3110
+ form: { gap: 14 },
3111
+ field: { gap: 6 },
3112
+ label: { fontSize: 13.5, fontWeight: "600", letterSpacing: -0.1 },
3113
+ hint: { fontSize: 12, lineHeight: 16 },
2971
3114
  input: {
2972
3115
  borderWidth: 1,
2973
3116
  borderRadius: 12,
2974
3117
  paddingHorizontal: 14,
2975
- paddingVertical: 13,
3118
+ paddingVertical: 12,
2976
3119
  fontSize: 16
2977
3120
  },
2978
- error: { color: "#ef4444", fontSize: 12.5, marginTop: 2 },
3121
+ error: { color: "#ef4444", fontSize: 12, marginTop: 2 },
2979
3122
  switchRow: { flexDirection: "row", alignItems: "center", gap: 12 },
2980
3123
  // Selectable option "boxes" for oneOf / enum single-select fields.
2981
- optionList: { gap: 10 },
3124
+ optionList: { gap: 9 },
2982
3125
  optionRow: {
2983
3126
  flexDirection: "row",
2984
3127
  alignItems: "center",
2985
3128
  gap: 12,
2986
3129
  borderWidth: 1.5,
2987
- borderRadius: 14,
3130
+ borderRadius: 13,
2988
3131
  paddingHorizontal: 14,
2989
- paddingVertical: 14
3132
+ paddingVertical: 13
2990
3133
  },
2991
3134
  radio: {
2992
- width: 20,
2993
- height: 20,
3135
+ width: 19,
3136
+ height: 19,
2994
3137
  borderRadius: 10,
2995
3138
  borderWidth: 2,
2996
3139
  alignItems: "center",
@@ -2999,10 +3142,10 @@ var ub = reactNative.StyleSheet.create({
2999
3142
  radioDot: { width: 10, height: 10, borderRadius: 5 },
3000
3143
  optionLabel: { flex: 1, fontSize: 15, letterSpacing: -0.1 },
3001
3144
  // Pinned footer (outside the fields scroll) — always visible.
3002
- actions: { paddingTop: 16, gap: 14 },
3003
- primaryBtn: { minHeight: 52, borderRadius: 14, borderWidth: 1, paddingVertical: 15, alignItems: "center", justifyContent: "center" },
3145
+ actions: { paddingTop: 14, gap: 12 },
3146
+ primaryBtn: { minHeight: 50, borderRadius: 14, borderWidth: 1, paddingVertical: 14, alignItems: "center", justifyContent: "center" },
3004
3147
  primaryBtnText: { color: "#fff", fontSize: 15.5, fontWeight: "600", letterSpacing: -0.1 },
3005
- linkRow: { flexDirection: "row", justifyContent: "center", gap: 24 },
3148
+ linkRow: { flexDirection: "row", justifyContent: "center", gap: 24, paddingTop: 2 },
3006
3149
  link: { fontSize: 14, fontWeight: "500" },
3007
3150
  linkDanger: { color: "#dc2626" },
3008
3151
  codeRow: { flexDirection: "row", justifyContent: "center", gap: 9, position: "relative" },
@@ -3035,34 +3178,36 @@ var sht = reactNative.StyleSheet.create({
3035
3178
  backdrop: { ...reactNative.StyleSheet.absoluteFillObject },
3036
3179
  kav: { flex: 1, justifyContent: "flex-end" },
3037
3180
  sheet: {
3038
- borderTopLeftRadius: 28,
3039
- borderTopRightRadius: 28,
3181
+ borderTopLeftRadius: 26,
3182
+ borderTopRightRadius: 26,
3040
3183
  borderWidth: reactNative.StyleSheet.hairlineWidth,
3041
3184
  borderBottomWidth: 0,
3042
- paddingHorizontal: 22,
3043
- paddingTop: 6,
3185
+ paddingHorizontal: 20,
3186
+ paddingTop: 4,
3044
3187
  maxWidth: 640,
3045
3188
  width: "100%",
3046
3189
  alignSelf: "center",
3047
3190
  ...reactNative.Platform.select({
3048
3191
  ios: {
3049
3192
  shadowColor: "#000",
3050
- shadowOffset: { width: 0, height: -6 },
3051
- shadowOpacity: 0.18,
3052
- shadowRadius: 24
3193
+ shadowOffset: { width: 0, height: -8 },
3194
+ shadowOpacity: 0.22,
3195
+ shadowRadius: 28
3053
3196
  },
3054
3197
  android: { elevation: 24 }
3055
3198
  })
3056
3199
  },
3057
- handleZone: { alignItems: "center", paddingVertical: 10 },
3058
- grabber: { width: 40, height: 5, borderRadius: 3 },
3059
- header: { flexDirection: "row", alignItems: "center", gap: 12, marginTop: 2, marginBottom: 18 },
3060
- iconCircle: { width: 40, height: 40, borderRadius: 20, alignItems: "center", justifyContent: "center" },
3061
- headerText: { flex: 1, gap: 2 },
3062
- title: { fontSize: 18, fontWeight: "700", letterSpacing: -0.3 },
3063
- subtitle: { fontSize: 13, letterSpacing: -0.1 },
3064
- timer: { borderWidth: 1, borderRadius: 999, paddingHorizontal: 11, paddingVertical: 5, minWidth: 46, alignItems: "center" },
3065
- closeBtn: { width: 30, height: 30, borderRadius: 15, alignItems: "center", justifyContent: "center" },
3200
+ handleZone: { alignItems: "center", paddingTop: 9, paddingBottom: 8 },
3201
+ grabber: { width: 38, height: 5, borderRadius: 3 },
3202
+ header: { flexDirection: "row", alignItems: "center", gap: 11, marginTop: 0, marginBottom: 14 },
3203
+ iconCircle: { width: 36, height: 36, borderRadius: 12, alignItems: "center", justifyContent: "center" },
3204
+ headerText: { flex: 1, gap: 1 },
3205
+ title: { fontSize: 17, fontWeight: "700", letterSpacing: -0.3 },
3206
+ subtitle: { fontSize: 12.5, letterSpacing: -0.1 },
3207
+ // Hairline rule under the header so the title cluster reads as its own zone.
3208
+ headerRule: { height: reactNative.StyleSheet.hairlineWidth, marginBottom: 16, marginHorizontal: -20 },
3209
+ timer: { borderWidth: 1, borderRadius: 999, paddingHorizontal: 10, paddingVertical: 4.5, minWidth: 44, alignItems: "center" },
3210
+ closeBtn: { width: 29, height: 29, borderRadius: 14.5, alignItems: "center", justifyContent: "center" },
3066
3211
  // Short, non-scrolling bodies (stale / expired notices).
3067
3212
  simpleBody: { paddingBottom: 8 }
3068
3213
  });