From 519de821e2896604abc9418e43699ad1d5f9ac7a Mon Sep 17 00:00:00 2001 From: EthanShoeDev <13422990+EthanShoeDev@users.noreply.github.com> Date: Thu, 18 Sep 2025 21:36:40 -0400 Subject: [PATCH] Better xtermjs api --- apps/mobile/package.json | 18 +- apps/mobile/src/app/(tabs)/shell/detail.tsx | 144 +++--- .../src-internal/main.tsx | 163 ++----- .../src/bridge.ts | 23 +- .../src/index.tsx | 412 ++++++++++++------ pnpm-lock.yaml | 82 ++-- 6 files changed, 439 insertions(+), 403 deletions(-) diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 060b845..ccc1475 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -49,7 +49,7 @@ "expo-haptics": "~15.0.7", "expo-image": "~3.0.8", "expo-linking": "~8.0.8", - "expo-router": "6.0.6", + "expo-router": "6.0.7", "expo-secure-store": "~15.0.7", "expo-splash-screen": "~31.0.10", "expo-status-bar": "~3.0.8", @@ -72,28 +72,28 @@ }, "devDependencies": { "@epic-web/config": "^1.21.3", - "@types/react": "~19.1.12", - "cmd-ts": "^0.14.1", - "eslint": "^9.35.0", - "@eslint/js": "^9.35.0", "@eslint-community/eslint-plugin-eslint-comments": "^4.5.0", "@eslint-react/eslint-plugin": "^1.53.0", + "@eslint/js": "^9.35.0", "@tanstack/eslint-plugin-query": "^5.86.0", + "@types/react": "~19.1.12", "@typescript-eslint/parser": "^8.44.0", "@typescript-eslint/utils": "^8.43.0", + "cmd-ts": "^0.14.1", + "eslint": "^9.35.0", + "eslint-config-expo": "~10.0.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-compiler": "19.1.0-rc.2", "eslint-plugin-react-hooks": "^5.2.0", - "globals": "^16.4.0", "eslint-plugin-react-refresh": "^0.4.20", - "typescript-eslint": "^8.44.0", - "eslint-config-expo": "~10.0.0", + "globals": "^16.4.0", "jiti": "^2.5.1", "npm-run-all": "^4.1.5", "prettier": "^3.6.2", "prettier-plugin-organize-imports": "^4.2.0", "tsx": "^4.20.5", - "typescript": "~5.9.2" + "typescript": "~5.9.2", + "typescript-eslint": "^8.44.0" }, "expo": { "doctor": { diff --git a/apps/mobile/src/app/(tabs)/shell/detail.tsx b/apps/mobile/src/app/(tabs)/shell/detail.tsx index b542fd1..fa6ff3b 100644 --- a/apps/mobile/src/app/(tabs)/shell/detail.tsx +++ b/apps/mobile/src/app/(tabs)/shell/detail.tsx @@ -50,6 +50,8 @@ function RouteSkeleton() { ); } +const encoder = new TextEncoder(); + function ShellDetail() { const xtermRef = useRef(null); const terminalReadyRef = useRef(false); @@ -151,100 +153,72 @@ function ShellDetail() { { - console.log('WebView render process gone -> clear()'); - const xr = xtermRef.current; - if (xr) xr.clear(); - }} - onContentProcessDidTerminate={() => { - console.log('WKWebView content process terminated -> clear()'); - const xr = xtermRef.current; - if (xr) xr.clear(); - }} - onLoadEnd={() => { - console.log('WebView onLoadEnd'); - }} - onMessage={(m) => { - console.log('received msg', m); - if (m.type === 'initialized') { - if (terminalReadyRef.current) return; - terminalReadyRef.current = true; + onInitialized={() => { + if (terminalReadyRef.current) return; + terminalReadyRef.current = true; - // Replay from head, then attach live listener - if (shell) { - void (async () => { - const res = await shell.readBuffer({ mode: 'head' }); - console.log('readBuffer(head)', { - chunks: res.chunks.length, - nextSeq: res.nextSeq, - dropped: res.dropped, - }); - if (res.chunks.length) { - const chunks = res.chunks.map((c) => c.bytes); - const xr = xtermRef.current; - if (xr) { - xr.writeMany(chunks); - xr.flush(); - } - } - const id = shell.addListener( - (ev: ListenerEvent) => { - if ('kind' in ev) { - console.log('listener.dropped', ev); - return; - } - const chunk = ev; - const xr3 = xtermRef.current; - if (xr3) xr3.write(chunk.bytes); - }, - { cursor: { mode: 'seq', seq: res.nextSeq } }, - ); - console.log('shell listener attached', id.toString()); - listenerIdRef.current = id; - })(); - } - - // Focus to pop the keyboard (iOS needs the prop we set) - const xr2 = xtermRef.current; - if (xr2) xr2.focus(); - return; - } - if (m.type === 'data') { - console.log('xterm->SSH', { len: m.data.length }); - const { buffer, byteOffset, byteLength } = m.data; - const ab = buffer.slice(byteOffset, byteOffset + byteLength); - if (shell) { - shell.sendData(ab as ArrayBuffer).catch((e: unknown) => { - console.warn('sendData failed', e); - router.back(); + // Replay from head, then attach live listener + if (shell) { + void (async () => { + const res = await shell.readBuffer({ mode: 'head' }); + console.log('readBuffer(head)', { + chunks: res.chunks.length, + nextSeq: res.nextSeq, + dropped: res.dropped, }); - } - return; - } else { - console.log('xterm.debug', m.message); + if (res.chunks.length) { + const chunks = res.chunks.map((c) => c.bytes); + const xr = xtermRef.current; + if (xr) { + xr.writeMany(chunks); + xr.flush(); + } + } + const id = shell.addListener( + (ev: ListenerEvent) => { + if ('kind' in ev) { + console.log('listener.dropped', ev); + return; + } + const chunk = ev; + const xr3 = xtermRef.current; + if (xr3) xr3.write(chunk.bytes); + }, + { cursor: { mode: 'seq', seq: res.nextSeq } }, + ); + console.log('shell listener attached', id.toString()); + listenerIdRef.current = id; + })(); } + + // Focus to pop the keyboard (iOS needs the prop we set) + const xr2 = xtermRef.current; + if (xr2) xr2.focus(); + return; + }} + onData={(terminalMessage) => { + if (!shell) return; + const bytes = encoder.encode(terminalMessage); + if (shell) { + shell.sendData(bytes.buffer).catch((e: unknown) => { + console.warn('sendData failed', e); + router.back(); + }); + } + return; }} /> diff --git a/packages/react-native-xtermjs-webview/src-internal/main.tsx b/packages/react-native-xtermjs-webview/src-internal/main.tsx index a9543b0..3be38fb 100644 --- a/packages/react-native-xtermjs-webview/src-internal/main.tsx +++ b/packages/react-native-xtermjs-webview/src-internal/main.tsx @@ -1,8 +1,8 @@ import { FitAddon } from '@xterm/addon-fit'; -import { Terminal, type ITerminalOptions, type ITheme } from '@xterm/xterm'; -import { Base64 } from 'js-base64'; +import { Terminal } from '@xterm/xterm'; import '@xterm/xterm/css/xterm.css'; import { + bStrToBinary, type BridgeInboundMessage, type BridgeOutboundMessage, } from '../src/bridge'; @@ -14,14 +14,13 @@ declare global { terminalWriteBase64?: (data: string) => void; ReactNativeWebView?: { postMessage?: (data: string) => void }; __FRESSH_XTERM_BRIDGE__?: boolean; - __FRESSH_XTERM_MSG_HANDLER__?: (e: MessageEvent) => void; + __FRESSH_XTERM_MSG_HANDLER__?: ( + e: MessageEvent, + ) => void; } } -/** - * Post typed messages to React Native - */ -const post = (msg: BridgeInboundMessage) => +const sendToRn = (msg: BridgeInboundMessage) => window.ReactNativeWebView?.postMessage?.(JSON.stringify(msg)); /** @@ -29,7 +28,7 @@ const post = (msg: BridgeInboundMessage) => * If the script happens to run twice (dev reloads, double-mounts), we bail out early. */ if (window.__FRESSH_XTERM_BRIDGE__) { - post({ + sendToRn({ type: 'debug', message: 'bridge already installed; ignoring duplicate boot', }); @@ -55,152 +54,84 @@ if (window.__FRESSH_XTERM_BRIDGE__) { window.fitAddon = fitAddon; // Encode helper - const enc = new TextEncoder(); - - // Initial handshake (send once) - setTimeout(() => post({ type: 'initialized' }), 500); + // const enc = new TextEncoder(); // User input from xterm -> RN (SSH) as UTF-8 bytes (Base64) - term.onData((data /* string */) => { - const bytes = enc.encode(data); - const b64 = Base64.fromUint8Array(bytes); - post({ type: 'input', b64 }); + term.onData((data) => { + // const bytes = enc.encode(data); + // const bStr = binaryToBStr(bytes); + sendToRn({ type: 'input', str: data }); }); // Remove old handler if any (just in case) - if (window.__FRESSH_XTERM_MSG_HANDLER__) { + if (window.__FRESSH_XTERM_MSG_HANDLER__) window.removeEventListener('message', window.__FRESSH_XTERM_MSG_HANDLER__!); - } // RN -> WebView handler (write, resize, setFont, setTheme, setOptions, clear, focus) - const handler = (e: MessageEvent) => { + const handler = (e: MessageEvent) => { try { - const msg = JSON.parse(e.data) as BridgeOutboundMessage; + const msg = e.data; if (!msg || typeof msg.type !== 'string') return; + // TODO: https://xtermjs.org/docs/guides/flowcontrol/#ideas-for-a-better-mechanism + const termWrite = (bStr: string) => { + const bytes = bStrToBinary(bStr); + term.write(bytes); + }; + switch (msg.type) { case 'write': { - if ('b64' in msg) { - const bytes = Base64.toUint8Array(msg.b64); - term.write(bytes); - post({ type: 'debug', message: `write(bytes=${bytes.length})` }); - } else if ('chunks' in msg && Array.isArray(msg.chunks)) { - for (const b64 of msg.chunks) { - const bytes = Base64.toUint8Array(b64); - term.write(bytes); - } - post({ - type: 'debug', - message: `write(chunks=${msg.chunks.length})`, - }); + termWrite(msg.bStr); + break; + } + case 'writeMany': { + for (const bStr of msg.chunks) { + termWrite(bStr); } break; } - case 'resize': { - if (typeof msg.cols === 'number' && typeof msg.rows === 'number') { - term.resize(msg.cols, msg.rows); - post({ type: 'debug', message: `resize(${msg.cols}x${msg.rows})` }); - } + term.resize(msg.cols, msg.rows); + break; + } + case 'fit': { fitAddon.fit(); break; } - - case 'setFont': { - const { family, size } = msg; - const patch: Partial = {}; - if (family) patch.fontFamily = family; - if (typeof size === 'number') patch.fontSize = size; - if (Object.keys(patch).length) { - term.options = patch; // never spread existing options (avoids cols/rows setters) - post({ - type: 'debug', - message: `setFont(${family ?? ''}, ${size ?? ''})`, - }); - fitAddon.fit(); - } - break; - } - - case 'setTheme': { - const { background, foreground } = msg; - const theme: Partial = {}; - if (background) { - theme.background = background; - document.body.style.backgroundColor = background; - } - if (foreground) theme.foreground = foreground; - if (Object.keys(theme).length) { - term.options = { theme }; // set only theme - post({ - type: 'debug', - message: `setTheme(bg=${background ?? ''}, fg=${foreground ?? ''})`, - }); - } - break; - } - case 'setOptions': { - const incoming = (msg.opts ?? {}) as Record; - type PatchRecord = Partial< - Record< - keyof ITerminalOptions, - ITerminalOptions[keyof ITerminalOptions] - > - >; - const patch: PatchRecord = {}; - for (const [k, v] of Object.entries(incoming)) { - // Avoid touching cols/rows via options setters here - if (k === 'cols' || k === 'rows') continue; - // Theme: also mirror background to page for seamless visuals - if (k === 'theme' && v && typeof v === 'object') { - const theme = v as ITheme; - if (theme.background) { - document.body.style.backgroundColor = theme.background; - } - patch.theme = theme; - continue; - } - const key = k as keyof ITerminalOptions; - patch[key] = v as ITerminalOptions[keyof ITerminalOptions]; - } - if (Object.keys(patch).length) { - term.options = patch; - post({ - type: 'debug', - message: `setOptions(${Object.keys(patch).join(',')})`, - }); - // If dimensions-affecting options changed, refit - if ( - patch.fontFamily !== undefined || - patch.fontSize !== undefined || - patch.letterSpacing !== undefined || - patch.lineHeight !== undefined - ) { - fitAddon.fit(); - } + const newOpts = msg.opts; + term.options = newOpts; + if ( + 'theme' in newOpts && + newOpts.theme && + 'background' in newOpts.theme && + newOpts.theme.background + ) { + document.body.style.backgroundColor = newOpts.theme.background; } break; } - case 'clear': { term.clear(); - post({ type: 'debug', message: 'clear()' }); break; } - case 'focus': { term.focus(); - post({ type: 'debug', message: 'focus()' }); break; } } } catch (err) { - post({ type: 'debug', message: `message handler error: ${String(err)}` }); + sendToRn({ + type: 'debug', + message: `message handler error: ${String(err)}`, + }); } }; window.__FRESSH_XTERM_MSG_HANDLER__ = handler; window.addEventListener('message', handler); + + // Initial handshake (send once) + setTimeout(() => sendToRn({ type: 'initialized' }), 50); } diff --git a/packages/react-native-xtermjs-webview/src/bridge.ts b/packages/react-native-xtermjs-webview/src/bridge.ts index fe0af9d..7b913d1 100644 --- a/packages/react-native-xtermjs-webview/src/bridge.ts +++ b/packages/react-native-xtermjs-webview/src/bridge.ts @@ -1,25 +1,22 @@ +import { Base64 } from 'js-base64'; type ITerminalOptions = import('@xterm/xterm').ITerminalOptions; - // Messages posted from the WebView (xterm page) to React Native export type BridgeInboundMessage = | { type: 'initialized' } - | { type: 'input'; b64: string } + | { type: 'input'; str: string } | { type: 'debug'; message: string }; // Messages injected from React Native into the WebView (xterm page) export type BridgeOutboundMessage = - | { type: 'write'; b64: string } - | { type: 'write'; chunks: string[] } - | { type: 'resize'; cols?: number; rows?: number } - | { type: 'setFont'; family?: string; size?: number } - | { type: 'setTheme'; background?: string; foreground?: string } + | { type: 'write'; bStr: string } + | { type: 'writeMany'; chunks: string[] } + | { type: 'resize'; cols: number; rows: number } + | { type: 'fit' } | { type: 'setOptions'; opts: Partial } | { type: 'clear' } | { type: 'focus' }; -export type TerminalOptionsPatch = BridgeOutboundMessage extends { - type: 'setOptions'; - opts: infer O; -} - ? O - : never; +export const binaryToBStr = (binary: Uint8Array): string => + Base64.fromUint8Array(binary); +export const bStrToBinary = (bStr: string): Uint8Array => + Base64.toUint8Array(bStr); diff --git a/packages/react-native-xtermjs-webview/src/index.tsx b/packages/react-native-xtermjs-webview/src/index.tsx index ca7a014..ff1fbab 100644 --- a/packages/react-native-xtermjs-webview/src/index.tsx +++ b/packages/react-native-xtermjs-webview/src/index.tsx @@ -1,27 +1,26 @@ -type ITerminalOptions = import('@xterm/xterm').ITerminalOptions; -import { Base64 } from 'js-base64'; -import React, { useEffect, useImperativeHandle, useRef } from 'react'; -import { WebView } from 'react-native-webview'; +import React, { + useEffect, + useImperativeHandle, + useMemo, + useRef, + useCallback, +} from 'react'; +import { WebView, type WebViewMessageEvent } from 'react-native-webview'; import htmlString from '../dist-internal/index.html?raw'; import { + binaryToBStr, + bStrToBinary, type BridgeInboundMessage, type BridgeOutboundMessage, - type TerminalOptionsPatch, } from './bridge'; -// Re-exported shared types live in src/bridge.ts for library build -// Internal page imports the same file via ../src/bridge + +export { bStrToBinary, binaryToBStr }; type StrictOmit = Omit; +type ITerminalOptions = import('@xterm/xterm').ITerminalOptions; +type WebViewOptions = React.ComponentProps; -/** - * Message from the webview to RN - */ -type InboundMessage = BridgeInboundMessage; - -/** - * Message from RN to the webview - */ -type OutboundMessage = BridgeOutboundMessage; +const defaultCoalescingThreshold = 8 * 1024; /** * Message from this pkg to calling RN @@ -36,105 +35,160 @@ export type XtermWebViewHandle = { // Efficiently write many chunks in one postMessage (for initial replay) writeMany: (chunks: Uint8Array[]) => void; flush: () => void; // force-flush outgoing writes - resize: (cols?: number, rows?: number) => void; - setFont: (family?: string, size?: number) => void; - setTheme: (background?: string, foreground?: string) => void; - setOptions: (opts: TerminalOptionsPatch) => void; clear: () => void; focus: () => void; + resize: (size: { cols: number; rows: number }) => void; + fit: () => void; }; -export interface XtermJsWebViewProps - extends StrictOmit< - React.ComponentProps, - 'source' | 'originWhitelist' | 'onMessage' - > { - ref: React.RefObject; - onMessage?: (msg: XtermInbound) => void; +const defaultWebViewProps: WebViewOptions = { + // WebView behavior that suits terminals + // ios + keyboardDisplayRequiresUserAction: false, + pullToRefreshEnabled: false, + bounces: false, + textInteractionEnabled: false, + allowsLinkPreview: false, + // android + setSupportMultipleWindows: false, + overScrollMode: 'never', + setBuiltInZoomControls: false, + setDisplayZoomControls: false, + textZoom: 100, + // both + originWhitelist: ['*'], + scalesPageToFit: false, + contentMode: 'mobile', +}; - // xterm Terminal.setOptions props (typed from @xterm/xterm) - options?: Partial; +const defaultXtermOptions: Partial = { + fontFamily: 'Menlo, ui-monospace, monospace', + fontSize: 80, + cursorBlink: true, + scrollback: 10000, +}; + +type UserControllableWebViewProps = StrictOmit< + WebViewOptions, + 'source' | 'style' +>; + +export type XtermJsWebViewProps = { + ref: React.RefObject; + style?: WebViewOptions['style']; + webViewOptions?: UserControllableWebViewProps; + xtermOptions?: Partial; + onInitialized?: () => void; + onData?: (data: string) => void; + logger?: { + debug?: (...args: unknown[]) => void; + log?: (...args: unknown[]) => void; + warn?: (...args: unknown[]) => void; + error?: (...args: unknown[]) => void; + }; + coalescingThreshold?: number; + size?: { + cols: number; + rows: number; + }; + autoFit?: boolean; +}; + +function xTermOptionsEquals( + a: Partial | null, + b: Partial | null, +): boolean { + if (a == b) return true; + if (a == null && b == null) return true; + if (a == null || b == null) return false; + const keys = new Set([ + ...Object.keys(a as object), + ...Object.keys(b as object), + ]); + for (const k of keys) { + const key = k as keyof ITerminalOptions; + if (a[key] !== b[key]) return false; + } + return true; } export function XtermJsWebView({ ref, - onMessage, - options, - ...props + style, + webViewOptions = defaultWebViewProps, + xtermOptions = defaultXtermOptions, + onInitialized, + onData, + coalescingThreshold = defaultCoalescingThreshold, + logger, + size, + autoFit = true, }: XtermJsWebViewProps) { const webRef = useRef(null); // ---- RN -> WebView message sender - const send = (obj: OutboundMessage) => { - const payload = JSON.stringify(obj); - console.log('sending msg', payload); - const js = `window.dispatchEvent(new MessageEvent('message',{data:${JSON.stringify( - payload, - )}})); true;`; - webRef.current?.injectJavaScript(js); - }; + const sendToWebView = useCallback( + (obj: BridgeOutboundMessage) => { + const webViewRef = webRef.current; + if (!webViewRef) return; + const payload = JSON.stringify(obj); + logger?.log?.(`sending msg to webview: ${payload}`); + const js = `window.dispatchEvent(new MessageEvent('message',{data:${payload}})); true;`; + webViewRef.injectJavaScript(js); + }, + [logger], + ); // ---- rAF + 8KB coalescer for writes const bufRef = useRef(null); const rafRef = useRef(null); - const THRESHOLD = 8 * 1024; - const flush = () => { + const flush = useCallback(() => { if (!bufRef.current) return; - const b64 = Base64.fromUint8Array(bufRef.current); + const bStr = binaryToBStr(bufRef.current); bufRef.current = null; if (rafRef.current != null) { cancelAnimationFrame(rafRef.current); rafRef.current = null; } - send({ type: 'write', b64 }); - }; + sendToWebView({ type: 'write', bStr }); + }, [sendToWebView]); - const schedule = () => { + const schedule = useCallback(() => { if (rafRef.current != null) return; rafRef.current = requestAnimationFrame(() => { rafRef.current = null; flush(); }); - }; + }, [flush]); - const write = (data: Uint8Array) => { - if (!data || data.length === 0) return; - if (!bufRef.current) { - bufRef.current = data; - } else { - const a = bufRef.current; - const merged = new Uint8Array(a.length + data.length); - merged.set(a, 0); - merged.set(data, a.length); - bufRef.current = merged; - } - if ((bufRef.current?.length ?? 0) >= THRESHOLD) flush(); - else schedule(); - }; + const write = useCallback( + (data: Uint8Array) => { + if (!data || data.length === 0) return; + if (!bufRef.current) { + bufRef.current = data; + } else { + const a = bufRef.current; + const merged = new Uint8Array(a.length + data.length); + merged.set(a, 0); + merged.set(data, a.length); + bufRef.current = merged; + } + if ((bufRef.current?.length ?? 0) >= coalescingThreshold) flush(); + else schedule(); + }, + [coalescingThreshold, flush, schedule], + ); - const writeMany = (chunks: Uint8Array[]) => { - if (!chunks || chunks.length === 0) return; - // Ensure any pending small buffered write is flushed before bulk write - flush(); - const b64s = chunks.map((c) => Base64.fromUint8Array(c)); - send({ type: 'write', chunks: b64s }); - }; - - useImperativeHandle(ref, () => ({ - write, - writeMany, - flush, - resize: (cols?: number, rows?: number) => - send({ type: 'resize', cols, rows }), - setFont: (family?: string, size?: number) => - send({ type: 'setFont', family, size }), - setTheme: (background?: string, foreground?: string) => - send({ type: 'setTheme', background, foreground }), - setOptions: (opts) => send({ type: 'setOptions', opts }), - clear: () => send({ type: 'clear' }), - focus: () => send({ type: 'focus' }), - })); + const writeMany = useCallback( + (chunks: Uint8Array[]) => { + if (!chunks || chunks.length === 0) return; + flush(); // Ensure any pending small buffered write is flushed before bulk write + const bStrs = chunks.map(binaryToBStr); + sendToWebView({ type: 'writeMany', chunks: bStrs }); + }, + [flush, sendToWebView], + ); // Cleanup pending rAF on unmount useEffect(() => { @@ -145,69 +199,149 @@ export function XtermJsWebView({ }; }, []); - // Apply options changes via setOptions without remounting - const prevOptsRef = useRef | null>(null); - useEffect(() => { - const merged: Partial = { - ...(options ?? {}), - }; + const fit = useCallback(() => { + sendToWebView({ type: 'fit' }); + }, [sendToWebView]); - // Compute shallow patch of changed keys to reduce noise - const prev: Partial = (prevOptsRef.current ?? - {}) as Partial; - type PatchRecord = Partial< - Record - >; - const patch: PatchRecord = {}; - const keys = new Set([ - ...Object.keys(prev as object), - ...Object.keys(merged as object), - ]); - let changed = false; - for (const k of keys) { - const key = k as keyof ITerminalOptions; - const prevVal = prev[key]; - const nextVal = merged[key]; - if (prevVal !== nextVal) { - patch[key] = nextVal as ITerminalOptions[keyof ITerminalOptions]; - changed = true; + const autoFitFn = useCallback(() => { + if (!autoFit) return; + fit(); + }, [autoFit, fit]); + + const appliedSizeRef = useRef<{ cols: number; rows: number } | null>(null); + + useEffect(() => { + const appliedSize = appliedSizeRef.current; + if (!size) return; + if (appliedSize?.cols === size.cols && appliedSize?.rows === size.rows) + return; + + logger?.log?.(`calling resize`, size); + sendToWebView({ type: 'resize', cols: size.cols, rows: size.rows }); + autoFitFn(); + + appliedSizeRef.current = size; + }, [size, sendToWebView, logger, autoFitFn]); + + useImperativeHandle(ref, () => ({ + write, + writeMany, + flush, + clear: () => sendToWebView({ type: 'clear' }), + focus: () => { + sendToWebView({ type: 'focus' }); + webRef.current?.requestFocus(); + }, + resize: (size: { cols: number; rows: number }) => { + sendToWebView({ type: 'resize', cols: size.cols, rows: size.rows }); + autoFitFn(); + appliedSizeRef.current = size; + }, + fit, + })); + + const mergedXTermOptions = useMemo( + () => ({ + ...defaultXtermOptions, + ...xtermOptions, + }), + [xtermOptions], + ); + + const appliedXtermOptionsRef = useRef | null>(null); + + useEffect(() => { + const appliedXtermOptions = appliedXtermOptionsRef.current; + if (xTermOptionsEquals(appliedXtermOptions, mergedXTermOptions)) return; + logger?.log?.(`setting options: `, mergedXTermOptions); + sendToWebView({ type: 'setOptions', opts: mergedXTermOptions }); + + appliedXtermOptionsRef.current = mergedXTermOptions; + }, [mergedXTermOptions, sendToWebView, logger]); + + const onMessage = useCallback( + (e: WebViewMessageEvent) => { + try { + const msg: BridgeInboundMessage = JSON.parse(e.nativeEvent.data); + logger?.log?.(`received msg from webview: `, msg); + if (msg.type === 'initialized') { + onInitialized?.(); + autoFitFn(); + return; + } + if (msg.type === 'input') { + // const bytes = bStrToBinary(msg.bStr); + // onData?.(bytes); + onData?.(msg.str); + return; + } + if (msg.type === 'debug') { + logger?.log?.(`received debug msg from webview: `, msg.message); + return; + } + webViewOptions?.onMessage?.(e); + } catch (error) { + logger?.warn?.( + `received unknown msg from webview: `, + e.nativeEvent.data, + error, + ); } - } - if (changed) { - send({ type: 'setOptions', opts: patch }); - prevOptsRef.current = merged; - } - }, [options]); + }, + [logger, webViewOptions, onInitialized, autoFitFn, onData], + ); + + const onContentProcessDidTerminate = useCallback< + NonNullable + >( + (e) => { + logger?.warn?.('WebView Crashed on iOS! onContentProcessDidTerminate'); + webViewOptions?.onContentProcessDidTerminate?.(e); + }, + [logger, webViewOptions], + ); + + const onRenderProcessGone = useCallback< + NonNullable + >( + (e) => { + logger?.warn?.('WebView Crashed on Android! onRenderProcessGone'); + webViewOptions?.onRenderProcessGone?.(e); + }, + [logger, webViewOptions], + ); + + const onLoadEnd = useCallback>( + (e) => { + logger?.log?.('WebView onLoadEnd'); + webViewOptions?.onLoadEnd?.(e); + }, + [logger, webViewOptions], + ); + + const mergedWebViewOptions = useMemo( + () => ({ + ...defaultWebViewProps, + ...webViewOptions, + onContentProcessDidTerminate, + onRenderProcessGone, + onLoadEnd, + }), + [ + webViewOptions, + onContentProcessDidTerminate, + onRenderProcessGone, + onLoadEnd, + ], + ); return ( { - try { - const msg: InboundMessage = JSON.parse(e.nativeEvent.data); - console.log('received msg', msg); - if (msg.type === 'initialized') { - onMessage?.({ type: 'initialized' }); - return; - } - if (msg.type === 'input') { - const bytes = Base64.toUint8Array(msg.b64); - onMessage?.({ type: 'data', data: bytes }); - return; - } - if (msg.type === 'debug') { - onMessage?.({ type: 'debug', message: msg.message }); - return; - } - } catch { - // ignore unknown payloads - } - }} - {...props} + onMessage={onMessage} + style={style} + {...mergedWebViewOptions} /> ); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec2a466..a5b60b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,7 +78,7 @@ importers: version: 4.1.0 expo: specifier: 54.0.8 - version: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + version: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-clipboard: specifier: ~8.0.7 version: 8.0.7(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) @@ -113,8 +113,8 @@ importers: specifier: ~8.0.8 version: 8.0.8(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-router: - specifier: 6.0.6 - version: 6.0.6(795f20997b5e74199ec1f471a585ccc7) + specifier: 6.0.7 + version: 6.0.7(795f20997b5e74199ec1f471a585ccc7) expo-secure-store: specifier: ~15.0.7 version: 15.0.7(expo@54.0.8) @@ -5261,8 +5261,8 @@ packages: react: '*' react-native: '*' - expo-router@6.0.6: - resolution: {integrity: sha512-uSuKQanivBI9RtwmAznLI7It5aPwQLVL2tVBPAOJ70tv6BzP62SpVCf0I8o0j9PmEzORPRLrU2LbQOL962yBHg==} + expo-router@6.0.7: + resolution: {integrity: sha512-dP/35aQadCuplEP99CZ0sLrVpnCFCQGnCBtFlI0Tph75PbepdWhI7XC0Vzt7MoNBLF9NW80q5CeZdXTvybc+4w==} peerDependencies: '@expo/metro-runtime': ^6.1.2 '@react-navigation/drawer': ^7.5.0 @@ -10646,7 +10646,7 @@ snapshots: '@eslint/core': 0.15.2 levn: 0.4.1 - '@expo/cli@54.0.6(expo-router@6.0.6)(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))': + '@expo/cli@54.0.6(expo-router@6.0.7)(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))': dependencies: '@0no-co/graphql.web': 1.2.0 '@expo/code-signing-certificates': 0.0.5 @@ -10682,7 +10682,7 @@ snapshots: connect: 3.7.0 debug: 4.4.1 env-editor: 0.4.2 - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) freeport-async: 2.0.0 getenv: 2.0.0 glob: 10.4.5 @@ -10714,7 +10714,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.6(795f20997b5e74199ec1f471a585ccc7) + expo-router: 6.0.7(795f20997b5e74199ec1f471a585ccc7) react-native: 0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -10903,7 +10903,7 @@ snapshots: postcss: 8.4.49 resolve-from: 5.0.0 optionalDependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) transitivePeerDependencies: - bufferutil - supports-color @@ -10912,7 +10912,7 @@ snapshots: '@expo/metro-runtime@6.1.1(expo@54.0.8)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0)': dependencies: anser: 1.4.10 - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) pretty-format: 29.7.0 react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0) @@ -10975,7 +10975,7 @@ snapshots: '@expo/json-file': 10.0.7 '@react-native/normalize-colors': 0.81.4 debug: 4.4.3 - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) resolve-from: 5.0.0 semver: 7.7.2 xml2js: 0.6.0 @@ -13827,7 +13827,7 @@ snapshots: resolve-from: 5.0.0 optionalDependencies: '@babel/runtime': 7.28.3 - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) transitivePeerDependencies: - '@babel/core' - supports-color @@ -14808,7 +14808,7 @@ snapshots: '@typescript-eslint/eslint-plugin': 8.41.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) '@typescript-eslint/parser': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) eslint: 9.35.0(jiti@2.5.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1)))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1)))(eslint-plugin-import@2.32.0)(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-expo: 1.0.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-react: 7.37.5(eslint@9.35.0(jiti@2.5.1)) @@ -14839,7 +14839,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1)))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1)))(eslint-plugin-import@2.32.0)(eslint@9.35.0(jiti@2.5.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1 @@ -14862,7 +14862,7 @@ snapshots: '@typescript-eslint/parser': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) eslint: 9.35.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1)))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1)))(eslint-plugin-import@2.32.0)(eslint@9.35.0(jiti@2.5.1)) transitivePeerDependencies: - supports-color @@ -15316,7 +15316,7 @@ snapshots: expo-asset@12.0.8(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0): dependencies: '@expo/image-utils': 0.8.7 - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-constants: 18.0.9(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0)) react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0) @@ -15325,7 +15325,7 @@ snapshots: expo-clipboard@8.0.7(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0) @@ -15333,7 +15333,7 @@ snapshots: dependencies: '@expo/config': 12.0.9 '@expo/env': 2.0.7 - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) react-native: 0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0) transitivePeerDependencies: - supports-color @@ -15341,11 +15341,11 @@ snapshots: expo-crypto@15.0.7(expo@54.0.8): dependencies: base64-js: 1.5.1 - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-dev-client@6.0.12(expo@54.0.8): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-dev-launcher: 6.0.11(expo@54.0.8) expo-dev-menu: 7.0.11(expo@54.0.8) expo-dev-menu-interface: 2.0.0(expo@54.0.8) @@ -15356,7 +15356,7 @@ snapshots: expo-dev-launcher@6.0.11(expo@54.0.8): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-dev-menu: 7.0.11(expo@54.0.8) expo-manifests: 1.0.8(expo@54.0.8) transitivePeerDependencies: @@ -15364,42 +15364,42 @@ snapshots: expo-dev-menu-interface@2.0.0(expo@54.0.8): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-dev-menu@7.0.11(expo@54.0.8): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-dev-menu-interface: 2.0.0(expo@54.0.8) expo-document-picker@14.0.7(expo@54.0.8): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-file-system@19.0.14(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0)): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) react-native: 0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0) expo-font@14.0.8(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) fontfaceobserver: 2.3.0 react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0) expo-glass-effect@0.1.4(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0) expo-haptics@15.0.7(expo@54.0.8): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-image@3.0.8(expo@54.0.8)(react-native-web@0.21.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0) optionalDependencies: @@ -15409,7 +15409,7 @@ snapshots: expo-keep-awake@15.0.7(expo@54.0.8)(react@19.1.0): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) react: 19.1.0 expo-linking@8.0.8(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0): @@ -15425,7 +15425,7 @@ snapshots: expo-manifests@1.0.8(expo@54.0.8): dependencies: '@expo/config': 12.0.8 - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-json-utils: 0.15.0 transitivePeerDependencies: - supports-color @@ -15445,7 +15445,7 @@ snapshots: react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0) - expo-router@6.0.6(795f20997b5e74199ec1f471a585ccc7): + expo-router@6.0.7(795f20997b5e74199ec1f471a585ccc7): dependencies: '@expo/metro-runtime': 6.1.1(expo@54.0.8)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) '@expo/schema-utils': 0.1.7 @@ -15456,9 +15456,9 @@ snapshots: '@react-navigation/native': 7.1.17(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) '@react-navigation/native-stack': 7.3.26(@react-navigation/native@7.1.17(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.1(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) client-only: 0.0.1 - debug: 4.4.1 + debug: 4.4.3 escape-string-regexp: 4.0.0 - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-constants: 18.0.9(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0)) expo-linking: 8.0.8(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) fast-deep-equal: 3.1.3 @@ -15490,12 +15490,12 @@ snapshots: expo-secure-store@15.0.7(expo@54.0.8): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-splash-screen@31.0.10(expo@54.0.8): dependencies: '@expo/prebuild-config': 54.0.3(expo@54.0.8) - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) transitivePeerDependencies: - supports-color @@ -15507,7 +15507,7 @@ snapshots: expo-symbols@1.0.7(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0)): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) react-native: 0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0) sf-symbols-typescript: 2.1.0 @@ -15515,7 +15515,7 @@ snapshots: dependencies: '@react-native/normalize-colors': 0.81.4 debug: 4.4.1 - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) react-native: 0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0) optionalDependencies: react-native-web: 0.21.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -15524,12 +15524,12 @@ snapshots: expo-updates-interface@2.0.0(expo@54.0.8): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) - expo@54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0): + expo@54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.7)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0): dependencies: '@babel/runtime': 7.28.3 - '@expo/cli': 54.0.6(expo-router@6.0.6)(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0)) + '@expo/cli': 54.0.6(expo-router@6.0.7)(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0)) '@expo/config': 12.0.9 '@expo/config-plugins': 54.0.1 '@expo/devtools': 0.1.7(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0)