diff --git a/docs/accessibility.md b/docs/accessibility.md
index 6006d1a..2f54556 100644
--- a/docs/accessibility.md
+++ b/docs/accessibility.md
@@ -5,6 +5,7 @@ Switchify Remote is designed for VoiceOver, TalkBack, iOS Switch Control, and An
- Every interactive target is at least 48 by 48 logical points and has a concise accessible name.
- First-run setup explains the Remote before asking for Bluetooth. Its two steps expose headings and "Step 1 of 2"/"Step 2 of 2" announcements in logical reading order, remain scrollable at large text sizes, and never move focus to a permission prompt until Allow Bluetooth is selected.
- Surface selection is one button that announces its current value and opens a modal option list. Options expose selected state, scanning stays inside the modal, and focus returns to the selector after selection or dismissal. Toggles expose selected or disabled state.
+- On Remote screens that expose controls, the Surface selector stays at the top of the scroll area after the title scrolls away. Its backdrop never becomes a scanning stop, and pinning does not move focus or trigger an announcement.
- Remote keeps a quick PC switcher immediately above the tab bar in every connection state. It announces the active connection, exposes selected state for the current saved PC, and restores focus after its modal closes.
- Headings, connection changes, failures, repeat state, pairing approval, and pointer-profile recovery transitions are announced once without moving focus unexpectedly.
- The pairing code is announced one digit at a time.
@@ -24,4 +25,4 @@ Switchify Remote is designed for VoiceOver, TalkBack, iOS Switch Control, and An
## Manual smoke test
-On both platforms, clear the app data before starting. Enable the screen reader and then the platform switch-access feature. Traverse both setup steps and confirm the headings, step positions, explanations, desktop download, Continue, Back, Not now, and Allow Bluetooth are announced in logical order. Confirm no Bluetooth prompt appears before Allow Bluetooth, and that the layout remains usable in both orientations and at 200% text. Then navigate PCs, pairing, every available remote screen, Settings, and Diagnostics in logical order. Confirm every action can be selected, read-only text does not become a switch-scan stop, pointer repeat exposes Stop movement, held modifiers and drag expose state, disabled controls are announced, and backgrounding returns the desktop to a neutral input state. On Android, confirm Forwarding keeps its profile choices available before Start but adds mapped external switches to scanning only while forwarding is active. The mapped switches and overflow notice must disappear after manual or safety stops. In Settings, edit the Remote name and confirm validation and save status are announced without adding read-only scan stops. Confirm Use device model restores the model-based name. Confirm Privacy policy is one scan stop, announces that it opens in a browser, opens the Remote-specific policy, and returns safely if the browser cannot open. During a delayed pointer-profile response, confirm "Restoring controls" is announced once, followed by either restored controls or "Controls unavailable" after all retries. Open the Surface selector and confirm the underlying screen and bottom tabs are not scannable, options scan linearly, Close works, Android back and VoiceOver escape dismiss the modal, and focus returns to the selector after selection or dismissal. Repeat those checks with the Remote PC switcher, confirm it stays above the tabs while content scrolls, and switch between two saved PCs. Press Unpair for a saved computer and confirm the native alert reads the full computer name. Verify Cancel, Android Back, and outside dismissal preserve the pairing, then confirm Unpair removes it. Repeat in light and dark appearance, at 200% text scaling, and with reduced motion enabled.
+On both platforms, clear the app data before starting. Enable the screen reader and then the platform switch-access feature. Traverse both setup steps and confirm the headings, step positions, explanations, desktop download, Continue, Back, Not now, and Allow Bluetooth are announced in logical order. Confirm no Bluetooth prompt appears before Allow Bluetooth, and that the layout remains usable in both orientations and at 200% text. Then navigate PCs, pairing, every available remote screen, Settings, and Diagnostics in logical order. Confirm every action can be selected, read-only text does not become a switch-scan stop, pointer repeat exposes Stop movement, held modifiers and drag expose state, disabled controls are announced, and backgrounding returns the desktop to a neutral input state. On each Remote control screen, scroll past the title and confirm the Surface selector stays visible, gains its backdrop only while pinned, remains one scan stop, and causes no focus move or announcement. On Android, confirm Forwarding keeps its profile choices available before Start but adds mapped external switches to scanning only while forwarding is active. The mapped switches and overflow notice must disappear after manual or safety stops. In Settings, edit the Remote name and confirm validation and save status are announced without adding read-only scan stops. Confirm Use device model restores the model-based name. Confirm Privacy policy is one scan stop, announces that it opens in a browser, opens the Remote-specific policy, and returns safely if the browser cannot open. During a delayed pointer-profile response, confirm "Restoring controls" is announced once, followed by either restored controls or "Controls unavailable" after all retries. Open the Surface selector while it is pinned and confirm the underlying screen and bottom tabs are not scannable, options scan linearly, Close works, Android back and VoiceOver escape dismiss the modal, and focus returns to the selector after selection or dismissal. Repeat those checks with the Remote PC switcher, confirm it stays above the tabs while content scrolls, and switch between two saved PCs. Press Unpair for a saved computer and confirm the native alert reads the full computer name. Verify Cancel, Android Back, and outside dismissal preserve the pairing, then confirm Unpair removes it. Repeat in light and dark appearance, at 200% text scaling, with reduced motion enabled, and with iOS Reduce Transparency enabled.
diff --git a/docs/physical-smoke-test.md b/docs/physical-smoke-test.md
index 537789b..2277e68 100644
--- a/docs/physical-smoke-test.md
+++ b/docs/physical-smoke-test.md
@@ -20,6 +20,8 @@ Run the matrix on a physical Android phone and iPhone against current Switchify
- Test a 320–428 point phone at 100%, 150%, and 200% text in light and dark appearance. Confirm headers stack, labels remain complete, action grids reduce columns, and the last control scrolls clear of the tab bar and system gesture area.
- Rename a paired PC to a long fixture name. Confirm the Remote status, PC card, and Unpair control wrap without truncation.
- Open each Surface, Opening surface, and Hold to stop selector. Confirm only the selector is a scan stop while closed; while open, only the modal options and Close are scanned. Confirm selection, Close, Android back, scrim dismissal, and VoiceOver escape restore focus to the selector.
+ - Scroll Mouse, Typing, Window, and Android Forwarding past the Remote title. Confirm the Surface selector pins below the top safe area, the controls blur only while passing behind it, and the fixed PC switcher remains above the tab bar. Confirm pinning adds no scan stop, focus move, or announcement.
+ - On Android 12 or later, confirm the pinned backdrop visibly blurs the moving controls without slowing pointer repeat, typing, or Forwarding. On Android 11, confirm the themed fallback remains legible and responsive. On iOS, enable Reduce Transparency and confirm the pinned backdrop becomes opaque without changing its size or focus order.
- Repeat on a tablet at normal and maximum text size. Confirm two-column and two-pane layouts collapse when enlarged text needs the width.
9. While repeat, drag, modifiers, and a text stream are active, background and terminate the app, disconnect Bluetooth, and quit Switchify PC. Confirm the desktop returns to neutral input state each time.
10. Export diagnostics and verify that no typed content, token, authentication proof, nonce, or verification code appears.
diff --git a/jest.setup.js b/jest.setup.js
index 47aa05b..da4e565 100644
--- a/jest.setup.js
+++ b/jest.setup.js
@@ -1,3 +1,7 @@
/* global jest */
jest.mock('@react-native-async-storage/async-storage', () => require('@react-native-async-storage/async-storage/jest/async-storage-mock'));
+jest.mock('expo-blur', () => {
+ const { View } = require('react-native');
+ return { BlurTargetView: View, BlurView: View };
+});
jest.mock('react-native-safe-area-context', () => require('react-native-safe-area-context/jest/mock').default);
diff --git a/package-lock.json b/package-lock.json
index de28f8a..9e72a53 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,28 +8,29 @@
"name": "switchify-remote",
"version": "1.0.0-beta.13",
"dependencies": {
- "@expo/ui": "~57.0.12",
+ "@expo/ui": "~57.0.13",
"@expo/vector-icons": "^15.0.2",
"@noble/hashes": "^2.3.0",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-navigation/native": "^7.3.16",
"base64-js": "^1.5.1",
- "expo": "~57.0.15",
- "expo-build-properties": "~57.0.13",
+ "expo": "~57.0.16",
+ "expo-blur": "~57.0.2",
+ "expo-build-properties": "~57.0.14",
"expo-clipboard": "~57.0.1",
"expo-constants": "~57.0.12",
- "expo-crypto": "~57.0.1",
- "expo-dev-client": "~57.0.14",
+ "expo-crypto": "~57.0.2",
+ "expo-dev-client": "~57.0.15",
"expo-device": "~57.0.1",
"expo-file-system": "~57.0.4",
"expo-font": "~57.0.1",
"expo-glass-effect": "~57.0.1",
"expo-image": "~57.0.3",
"expo-linking": "~57.0.7",
- "expo-router": "~57.0.15",
+ "expo-router": "~57.0.16",
"expo-secure-store": "~57.0.1",
- "expo-sharing": "~57.0.14",
- "expo-splash-screen": "~57.0.7",
+ "expo-sharing": "~57.0.15",
+ "expo-splash-screen": "~57.0.8",
"expo-status-bar": "~57.0.1",
"expo-symbols": "~57.0.2",
"expo-system-ui": "~57.0.2",
@@ -46,6 +47,8 @@
"react-native-worklets": "0.10.1"
},
"devDependencies": {
+ "@emnapi/core": "^1.11.3",
+ "@emnapi/runtime": "^1.11.3",
"@testing-library/react-native": "^14.0.1",
"@types/jest": "^29.5.14",
"@types/react": "~19.2.2",
@@ -85,6 +88,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
@@ -1150,7 +1154,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
"integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
},
@@ -1166,7 +1169,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
"integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
},
@@ -1198,7 +1200,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.8.tgz",
"integrity": "sha512-0UpIXPtdDtMXfnV2OJAVMLpj3H/92vmkA6lpSRakmycJvj3VUy6Xs1dM8tXRugupykr5WB+LpiVl0J8LMVg2mg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
},
@@ -1401,7 +1402,6 @@
"integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==",
"dev": true,
"license": "MIT",
- "optional": true,
"peer": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.3",
@@ -1414,7 +1414,6 @@
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
"dev": true,
"license": "MIT",
- "optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
@@ -1426,8 +1425,6 @@
"integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==",
"dev": true,
"license": "MIT",
- "optional": true,
- "peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
@@ -1667,15 +1664,15 @@
}
},
"node_modules/@expo/config": {
- "version": "57.0.8",
- "resolved": "https://registry.npmjs.org/@expo/config/-/config-57.0.8.tgz",
- "integrity": "sha512-7VpAu2ZMXNZI0+Kn+nD3vQBIyST89LATNkZpnp/cXh8/aj7zUXO8d/T+hOxsvhOFwR8eQO+jKXEw8tQbidiMxA==",
+ "version": "57.0.9",
+ "resolved": "https://registry.npmjs.org/@expo/config/-/config-57.0.9.tgz",
+ "integrity": "sha512-dmzlKraIFxa7wLwV6K7WzI8jp6QZpW6Mc5mGjLimJUFjzh4uQdYaT3m3plEutM5yxBoBEwqzks7l+I/ljCbxAQ==",
"license": "MIT",
"dependencies": {
- "@expo/config-plugins": "~57.0.8",
+ "@expo/config-plugins": "~57.0.9",
"@expo/config-types": "^57.0.2",
"@expo/json-file": "^11.0.1",
- "@expo/require-utils": "^57.0.4",
+ "@expo/require-utils": "^57.0.5",
"deepmerge": "^4.3.1",
"getenv": "^2.0.0",
"glob": "^13.0.0",
@@ -1685,15 +1682,15 @@
}
},
"node_modules/@expo/config-plugins": {
- "version": "57.0.8",
- "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-57.0.8.tgz",
- "integrity": "sha512-x6lx4s/19i39/+1dMPwb9tFCc4WR843dG5Yi+C64lhKL3YHX+6oKrT2Kv72PCEalnUY+usQDkB3NrLSrYOWtDg==",
+ "version": "57.0.9",
+ "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-57.0.9.tgz",
+ "integrity": "sha512-hHgfL1avkCdEvDSw7IwlKwRYYNgcxzbNNMIk6W6lTkJpY0MajinAfeJUS0J+wPCsjUfGbVqOJM+XhPaO5ulUxg==",
"license": "MIT",
"dependencies": {
"@expo/config-types": "^57.0.2",
"@expo/json-file": "~11.0.1",
"@expo/plist": "^0.8.1",
- "@expo/require-utils": "^57.0.4",
+ "@expo/require-utils": "^57.0.5",
"@expo/sdk-runtime-versions": "^1.0.0",
"chalk": "^4.1.2",
"debug": "^4.3.5",
@@ -1783,9 +1780,9 @@
"license": "MIT"
},
"node_modules/@expo/fingerprint": {
- "version": "0.20.9",
- "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.20.9.tgz",
- "integrity": "sha512-h+YvPyNmeAUCCqaXvftiXklA2zGyRcIPv1B8fFS0YSIgNhwcxFGh1EyLOMsvJ2aWsBViNtIJ1HIFLzNu43/r4w==",
+ "version": "0.20.10",
+ "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.20.10.tgz",
+ "integrity": "sha512-Ox/smKlpPpyTUwZAXS927xuJ8qICwNxHOtZElv0anp6xJ80FzwkfvCdC/NjCxlALS2Hp3IsmwyKd975gYgW12Q==",
"license": "MIT",
"dependencies": {
"@expo/env": "^2.4.2",
@@ -1805,12 +1802,12 @@
}
},
"node_modules/@expo/image-utils": {
- "version": "0.11.4",
- "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.11.4.tgz",
- "integrity": "sha512-pn/4770DIEOcYZr484uazuwg20FX/qaDkeMRF6J+oxejynDmEmO8wLsCudaNShFE0BhyKGQTYrs2rsRhqrqESw==",
+ "version": "0.11.5",
+ "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.11.5.tgz",
+ "integrity": "sha512-KPQBTpmpAfy/Vu9y4wPW808/qtZxjYmyJg8cm2QCPAupp+qEWA3b5zmk0ulOwQ9OgeHxuCPgUqWgkwHFo7UsrQ==",
"license": "MIT",
"dependencies": {
- "@expo/require-utils": "^57.0.4",
+ "@expo/require-utils": "^57.0.5",
"@expo/spawn-async": "^1.8.0",
"chalk": "^4.0.0",
"getenv": "^2.0.0",
@@ -1866,41 +1863,41 @@
}
},
"node_modules/@expo/metro": {
- "version": "56.0.0",
- "resolved": "https://registry.npmjs.org/@expo/metro/-/metro-56.0.0.tgz",
- "integrity": "sha512-5gIgQHtEpjjvsjKfVtIv23a98LLRV0/y07PDShEwYSytAMlE3FSF8RHXqtHc1sUJL6dn7hnuIBpIbrLXXuVi0A==",
- "license": "MIT",
- "dependencies": {
- "metro": "0.84.4",
- "metro-babel-transformer": "0.84.4",
- "metro-cache": "0.84.4",
- "metro-cache-key": "0.84.4",
- "metro-config": "0.84.4",
- "metro-core": "0.84.4",
- "metro-file-map": "0.84.4",
- "metro-minify-terser": "0.84.4",
- "metro-resolver": "0.84.4",
- "metro-runtime": "0.84.4",
- "metro-source-map": "0.84.4",
- "metro-symbolicate": "0.84.4",
- "metro-transform-plugins": "0.84.4",
- "metro-transform-worker": "0.84.4"
+ "version": "56.0.2",
+ "resolved": "https://registry.npmjs.org/@expo/metro/-/metro-56.0.2.tgz",
+ "integrity": "sha512-Ld5AeYMCCDa8bLeWhfuLbZFFjlV3f6ORqyPz2glGh6RltIngMuLf9BTC2yvHFjkKuGxL5SynijmA8xmNNWn5iA==",
+ "license": "MIT",
+ "dependencies": {
+ "metro": "0.84.5",
+ "metro-babel-transformer": "0.84.5",
+ "metro-cache": "0.84.5",
+ "metro-cache-key": "0.84.5",
+ "metro-config": "0.84.5",
+ "metro-core": "0.84.5",
+ "metro-file-map": "0.84.5",
+ "metro-minify-terser": "0.84.5",
+ "metro-resolver": "0.84.5",
+ "metro-runtime": "0.84.5",
+ "metro-source-map": "0.84.5",
+ "metro-symbolicate": "0.84.5",
+ "metro-transform-plugins": "0.84.5",
+ "metro-transform-worker": "0.84.5"
}
},
"node_modules/@expo/metro-config": {
- "version": "57.0.9",
- "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-57.0.9.tgz",
- "integrity": "sha512-+lalXZdoMaKTG1uKXsi2KWO0f7H3KiWrhZzAla4EmNSkcUMRx6K1rhZuBYoaJaKIp9BHZ333KbqRPron7slKQA==",
+ "version": "57.0.10",
+ "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-57.0.10.tgz",
+ "integrity": "sha512-YHcocILXfcBrwPlX5CtMKy0MitssjyHQg7ndFq+P26GzMnk8uXMpw9PFo/aNaAwh7d27/cOhbZON/TJdoHfVdQ==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.20.0",
"@babel/core": "^7.20.0",
"@babel/generator": "^7.20.5",
- "@expo/config": "~57.0.8",
+ "@expo/config": "~57.0.9",
"@expo/env": "~2.4.2",
"@expo/json-file": "~11.0.1",
- "@expo/metro": "~56.0.0",
- "@expo/require-utils": "^57.0.4",
+ "@expo/metro": "~56.0.2",
+ "@expo/require-utils": "^57.0.5",
"@expo/spawn-async": "^1.8.0",
"@jridgewell/gen-mapping": "^0.3.13",
"@jridgewell/remapping": "^2.3.5",
@@ -1927,9 +1924,9 @@
}
},
"node_modules/@expo/metro-file-map": {
- "version": "57.0.1",
- "resolved": "https://registry.npmjs.org/@expo/metro-file-map/-/metro-file-map-57.0.1.tgz",
- "integrity": "sha512-8JXfVstZN7QnP4NianZZnlTVboOWR0sG8trUDNajOjnbGlPln29vponXM84tY+3tAHapz5/TxE53L0ixUwqPtA==",
+ "version": "57.0.2",
+ "resolved": "https://registry.npmjs.org/@expo/metro-file-map/-/metro-file-map-57.0.2.tgz",
+ "integrity": "sha512-tb50nSIWwKpRufSkGuivOK0FbUxv1Uwqptb0SzFTk0bkmYmcy3gIwnCbOHTfmrQDVndRhPEiu2SYgh9Z0PCvGg==",
"license": "MIT",
"dependencies": {
"debug": "^4.3.4",
@@ -1941,9 +1938,9 @@
}
},
"node_modules/@expo/metro-runtime": {
- "version": "57.0.12",
- "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-57.0.12.tgz",
- "integrity": "sha512-WpgjfRFh88B5tKJrbsypvyxx0MKuXMo+ru9Gbjq4Qwj7OboJmrQ+g555YCgiQD3uk3J1ThzgEU06yZOV43suGA==",
+ "version": "57.0.13",
+ "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-57.0.13.tgz",
+ "integrity": "sha512-o5VKd/vGti4z8uEJRAvKgzUZf+b5UE6eXx9R9rjT5UFmcnjCGXjv5LNbzvcb8wiSSakDDXE/z4wpOhJEPqV5JA==",
"license": "MIT",
"dependencies": {
"@expo/log-box": "^57.0.3",
@@ -1965,6 +1962,300 @@
}
}
},
+ "node_modules/@expo/metro/node_modules/hermes-estree": {
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.35.0.tgz",
+ "integrity": "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==",
+ "license": "MIT"
+ },
+ "node_modules/@expo/metro/node_modules/hermes-parser": {
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.35.0.tgz",
+ "integrity": "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==",
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.35.0"
+ }
+ },
+ "node_modules/@expo/metro/node_modules/metro": {
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro/-/metro-0.84.5.tgz",
+ "integrity": "sha512-r1liLkyFZMVSEMNjU1CJU5pRzs3NdkxHqXS60O25c0rCIqAR+cGk7rPydw/g0WAIKVXojIBIF45yYBPagJGcgw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/core": "^7.25.2",
+ "@babel/generator": "^7.29.1",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "accepts": "^2.0.0",
+ "ci-info": "^2.0.0",
+ "connect": "^3.6.5",
+ "debug": "^4.4.0",
+ "error-stack-parser": "^2.0.6",
+ "flow-enums-runtime": "^0.0.6",
+ "graceful-fs": "^4.2.4",
+ "hermes-parser": "0.35.0",
+ "invariant": "^2.2.4",
+ "jest-worker": "^29.7.0",
+ "jsc-safe-url": "^0.2.2",
+ "lodash.throttle": "^4.1.1",
+ "metro-babel-transformer": "0.84.5",
+ "metro-cache": "0.84.5",
+ "metro-cache-key": "0.84.5",
+ "metro-config": "0.84.5",
+ "metro-core": "0.84.5",
+ "metro-file-map": "0.84.5",
+ "metro-resolver": "0.84.5",
+ "metro-runtime": "0.84.5",
+ "metro-source-map": "0.84.5",
+ "metro-symbolicate": "0.84.5",
+ "metro-transform-plugins": "0.84.5",
+ "metro-transform-worker": "0.84.5",
+ "mime-types": "^3.0.1",
+ "nullthrows": "^1.1.1",
+ "serialize-error": "^2.1.0",
+ "source-map": "^0.5.6",
+ "throat": "^5.0.0",
+ "ws": "^7.5.10",
+ "yargs": "^17.6.2"
+ },
+ "bin": {
+ "metro": "src/cli.js"
+ },
+ "engines": {
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ }
+ },
+ "node_modules/@expo/metro/node_modules/metro-babel-transformer": {
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.84.5.tgz",
+ "integrity": "sha512-2WbHILKMiJUzfdjmGOQOqU1bWi9//gqiclc/tkk/AIsrrVw3efhZ1uhkOwMTxUEPOzqoo091H0olLmVZH5FHGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.25.2",
+ "flow-enums-runtime": "^0.0.6",
+ "hermes-parser": "0.35.0",
+ "metro-cache-key": "0.84.5",
+ "nullthrows": "^1.1.1"
+ },
+ "engines": {
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ }
+ },
+ "node_modules/@expo/metro/node_modules/metro-cache": {
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.84.5.tgz",
+ "integrity": "sha512-WHS0n2OxQqtwEjSeQFPePNrMvEFhmQcUQM9cRJMHByWoi/GMWFBEWOf7hVkAM/0KRutAXNbDlSu/cZB6CyxgQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "exponential-backoff": "^3.1.1",
+ "flow-enums-runtime": "^0.0.6",
+ "https-proxy-agent": "^7.0.5",
+ "metro-core": "0.84.5"
+ },
+ "engines": {
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ }
+ },
+ "node_modules/@expo/metro/node_modules/metro-cache-key": {
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.84.5.tgz",
+ "integrity": "sha512-3dPB2TnvGjjf0/9O7AXVQURKXuQNauTZE7WpTGTlR017Gh/B5y0m/2wcqxfveUguHSpu89KhVxCAlr2k/H7uhQ==",
+ "license": "MIT",
+ "dependencies": {
+ "flow-enums-runtime": "^0.0.6"
+ },
+ "engines": {
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ }
+ },
+ "node_modules/@expo/metro/node_modules/metro-config": {
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.84.5.tgz",
+ "integrity": "sha512-zie+uN6oohscowi2S7ByU+wUw6CrT4ZxW9uAbONOObSxx86RGmnIAmjXHLkfmcdYoY7jzOPEbqcI6oeVmqyBQA==",
+ "license": "MIT",
+ "dependencies": {
+ "connect": "^3.6.5",
+ "flow-enums-runtime": "^0.0.6",
+ "jest-validate": "^29.7.0",
+ "metro": "0.84.5",
+ "metro-cache": "0.84.5",
+ "metro-core": "0.84.5",
+ "metro-runtime": "0.84.5",
+ "yaml": "^2.6.1"
+ },
+ "engines": {
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ }
+ },
+ "node_modules/@expo/metro/node_modules/metro-core": {
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.84.5.tgz",
+ "integrity": "sha512-xwm605hCi5Y6eJTTb8ZWo6pkUcoBEIyiQOfkZh5GwtDwUrP9SNhTQZhzJHrBCwwxlf3Ptl/pxWJgQ1rsNYMnrA==",
+ "license": "MIT",
+ "dependencies": {
+ "flow-enums-runtime": "^0.0.6",
+ "lodash.throttle": "^4.1.1",
+ "metro-resolver": "0.84.5"
+ },
+ "engines": {
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ }
+ },
+ "node_modules/@expo/metro/node_modules/metro-file-map": {
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.84.5.tgz",
+ "integrity": "sha512-mlm/JL8toSbSc2akpKIGmzvrVRSCgZ5vkbycI34oMLoOnLGuLyC8WTyVJ6P0hZG/usDaGwZSl/s9BCRriqjGJA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "fb-watchman": "^2.0.0",
+ "flow-enums-runtime": "^0.0.6",
+ "graceful-fs": "^4.2.4",
+ "invariant": "^2.2.4",
+ "jest-worker": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "nullthrows": "^1.1.1",
+ "walker": "^1.0.7"
+ },
+ "engines": {
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ }
+ },
+ "node_modules/@expo/metro/node_modules/metro-minify-terser": {
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.84.5.tgz",
+ "integrity": "sha512-BJoFwCEDsYnagPqarayInv2+diCDNDdLlaof/p6s9w4gh+gc9HXYM+pDvsKGKKUumpZswNF3Z/ftTMqKl/5IBg==",
+ "license": "MIT",
+ "dependencies": {
+ "flow-enums-runtime": "^0.0.6",
+ "terser": "^5.15.0"
+ },
+ "engines": {
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ }
+ },
+ "node_modules/@expo/metro/node_modules/metro-resolver": {
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.84.5.tgz",
+ "integrity": "sha512-VSSnepg1k6LyCwtb6eirWdAWlpKwBG8Rdtsr1mU38rMelFyWgh3/QuMSiZIZAIjwg/fsa8GhW5/FO54CAUPCEA==",
+ "license": "MIT",
+ "dependencies": {
+ "flow-enums-runtime": "^0.0.6"
+ },
+ "engines": {
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ }
+ },
+ "node_modules/@expo/metro/node_modules/metro-runtime": {
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.84.5.tgz",
+ "integrity": "sha512-U1m2+d1Pr+JO2/iVXBB2OfXXityz7tqwIorxfrT15IEgaHvpJBq/OHiqnOWPKJbUl3JcxjcdviZZOKk85oK4Qg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.25.0",
+ "flow-enums-runtime": "^0.0.6"
+ },
+ "engines": {
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ }
+ },
+ "node_modules/@expo/metro/node_modules/metro-source-map": {
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.84.5.tgz",
+ "integrity": "sha512-2BtV5L9uPc49F13Gn5wiP6bX/EncqzqTIk2VL/0F/96Vo0YEOjluT/qktQjFODfqGFsucwnh5mPEAl/2jVEfeg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "flow-enums-runtime": "^0.0.6",
+ "invariant": "^2.2.4",
+ "metro-symbolicate": "0.84.5",
+ "nullthrows": "^1.1.1",
+ "ob1": "0.84.5",
+ "source-map": "^0.5.6",
+ "vlq": "^1.0.0"
+ },
+ "engines": {
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ }
+ },
+ "node_modules/@expo/metro/node_modules/metro-symbolicate": {
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.84.5.tgz",
+ "integrity": "sha512-rQ40zYDAkaWBN9yvjUuAD0ZpzBMZSoKyGYXnb5JrfbKjun7fTvfoLHL3KXFYenBTYZkQtlp4cKSCv/1utxFyOw==",
+ "license": "MIT",
+ "dependencies": {
+ "flow-enums-runtime": "^0.0.6",
+ "invariant": "^2.2.4",
+ "metro-source-map": "0.84.5",
+ "nullthrows": "^1.1.1",
+ "source-map": "^0.5.6",
+ "vlq": "^1.0.0"
+ },
+ "bin": {
+ "metro-symbolicate": "src/index.js"
+ },
+ "engines": {
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ }
+ },
+ "node_modules/@expo/metro/node_modules/metro-transform-plugins": {
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.84.5.tgz",
+ "integrity": "sha512-+InaSVGaOyt0DyRo4Y/zIdPI6CZwnbNho5LAL23tgmuGwv7fyfkF7kKfPjZcfxXBcoYdTLLFnCfCH/dHSiCqNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.25.2",
+ "@babel/generator": "^7.29.1",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "flow-enums-runtime": "^0.0.6",
+ "nullthrows": "^1.1.1"
+ },
+ "engines": {
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ }
+ },
+ "node_modules/@expo/metro/node_modules/metro-transform-worker": {
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.84.5.tgz",
+ "integrity": "sha512-ui1Z8x4s5RL36gMmKLaMMO7O9NNDHNdthEZSCDQHAau3JcAsTaFOK6I+2q4I/kW5u8hSEjJk9L45TXSVJw6g1A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.25.2",
+ "@babel/generator": "^7.29.1",
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "flow-enums-runtime": "^0.0.6",
+ "metro": "0.84.5",
+ "metro-babel-transformer": "0.84.5",
+ "metro-cache": "0.84.5",
+ "metro-cache-key": "0.84.5",
+ "metro-minify-terser": "0.84.5",
+ "metro-source-map": "0.84.5",
+ "metro-transform-plugins": "0.84.5",
+ "nullthrows": "^1.1.1"
+ },
+ "engines": {
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ }
+ },
+ "node_modules/@expo/metro/node_modules/ob1": {
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.84.5.tgz",
+ "integrity": "sha512-aH9RkoZc7w/90HBamFxTw8ZLFr05wXS+iOnvmrgo53Ep8Pyrm5FieQSaPIVROkfFVQISeD/zo92fes26TOwe+A==",
+ "license": "MIT",
+ "dependencies": {
+ "flow-enums-runtime": "^0.0.6"
+ },
+ "engines": {
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ }
+ },
"node_modules/@expo/osascript": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.7.1.tgz",
@@ -2003,27 +2294,27 @@
}
},
"node_modules/@expo/prebuild-config": {
- "version": "57.0.13",
- "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-57.0.13.tgz",
- "integrity": "sha512-VhSySuXqOwK4fIc+9rgms7zN+c1Khj2xpuIgpVLPNyWRtpS8wB+eyV5CSohjSUBa3h+NsFdPk/VG6p0ZwlMDrg==",
+ "version": "57.0.14",
+ "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-57.0.14.tgz",
+ "integrity": "sha512-Lopsj0O/gqjVf94ZL8OH8Q7OV7YsREzxC+ScUQjgmYP5DTLlB193C6WO4hq/Mtia52NBLVAjRVMbW6TUJQtiWA==",
"license": "MIT",
"dependencies": {
- "@expo/config": "~57.0.8",
- "@expo/config-plugins": "~57.0.8",
+ "@expo/config": "~57.0.9",
+ "@expo/config-plugins": "~57.0.9",
"@expo/config-types": "^57.0.2",
- "@expo/image-utils": "^0.11.4",
+ "@expo/image-utils": "^0.11.5",
"@expo/json-file": "^11.0.1",
"@react-native/normalize-colors": "0.86.2",
"debug": "^4.3.1",
- "expo-modules-autolinking": "~57.0.10",
+ "expo-modules-autolinking": "~57.0.11",
"resolve-from": "^5.0.0",
"semver": "^7.6.0"
}
},
"node_modules/@expo/require-utils": {
- "version": "57.0.4",
- "resolved": "https://registry.npmjs.org/@expo/require-utils/-/require-utils-57.0.4.tgz",
- "integrity": "sha512-e7xbg/9BTQcsZE/oErafZXtI7kh5IgfasLJ97J5sFSzX2cA74pDvdlhW1KHVSaDkQyQv6h1LSLhsY7dEeOk7hw==",
+ "version": "57.0.5",
+ "resolved": "https://registry.npmjs.org/@expo/require-utils/-/require-utils-57.0.5.tgz",
+ "integrity": "sha512-kTAXj9lDFEIPMsbAOGCGbjBbMF0oi7CqkYM79KOX0DDD9wSwXmlKL1z2h8OwsrBf7mbOo2DjlRvZu4BEjrIxGw==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.20.0",
@@ -2070,9 +2361,9 @@
"license": "MIT"
},
"node_modules/@expo/ui": {
- "version": "57.0.12",
- "resolved": "https://registry.npmjs.org/@expo/ui/-/ui-57.0.12.tgz",
- "integrity": "sha512-ZkrECV6xZe0+Drq6C0hrBKvz1534/VyARNp9ziKihC6Db69W29+jBDZm8k9cyRyS1Hh23q6LP8J+2wDyUM8eAA==",
+ "version": "57.0.13",
+ "resolved": "https://registry.npmjs.org/@expo/ui/-/ui-57.0.13.tgz",
+ "integrity": "sha512-vGM2iVFKFg4P+XESrPPfCkHws9HuE5bzzw3BuxdJjIcHiVqq3lQlIyDTMTMg66sjYVRtqR6wAeadj1uDFYcRcg==",
"license": "MIT",
"dependencies": {
"sf-symbols-typescript": "^2.1.0",
@@ -3294,7 +3585,6 @@
"resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.86.2.tgz",
"integrity": "sha512-4XKEJ6jKW9lXMB1O5o47gBoGgolde1fbX13gLW/erlcn+1ky+MHHo5UjuM3RWGdPJHIvIzekDDumSUHhB9x5iQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/core": "^7.25.2",
"@babel/plugin-proposal-export-default-from": "^7.24.7",
@@ -3342,7 +3632,6 @@
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.36.0.tgz",
"integrity": "sha512-LhD0xdoedDw7ansQgXbB2DADLZIK/LRXuWNBPuVzMc5S2WK5GyT89tCM+cQzxFGO0mGyLK6D5TrVOJJzAoDy8Q==",
"license": "MIT",
- "peer": true,
"dependencies": {
"hermes-parser": "0.36.0"
}
@@ -3351,15 +3640,13 @@
"version": "0.36.0",
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.36.0.tgz",
"integrity": "sha512-A1+8zn5oss2CFP7pKsOaxorQG6FNIz1WU1VDqruLPPZl3LVgeE2C5xfFg8Ow6/Ow4mSslLLtYP1J3n38eKyW9w==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/@react-native/babel-preset/node_modules/hermes-parser": {
"version": "0.36.0",
"resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.36.0.tgz",
"integrity": "sha512-GdpwMmH5x6IpC1cijvcvYnlPB60Mh6kTSF/NFdYV/j56gYdi+0RIakYs+eqOV+bbO0SW7mgVVGSsTJxyPQfo3w==",
"license": "MIT",
- "peer": true,
"dependencies": {
"hermes-estree": "0.36.0"
}
@@ -3520,7 +3807,6 @@
"resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.86.2.tgz",
"integrity": "sha512-mX1wgLErdb2hDgXJr9zM9SWLe+ZteZTFTwRWGOQ53yEJwc9DVSnxdTlAtVdMeOj2ntycKh0R8jYdat2Am43cwQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/core": "^7.25.2",
"@react-native/babel-preset": "0.86.2",
@@ -3538,15 +3824,13 @@
"version": "0.36.0",
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.36.0.tgz",
"integrity": "sha512-A1+8zn5oss2CFP7pKsOaxorQG6FNIz1WU1VDqruLPPZl3LVgeE2C5xfFg8Ow6/Ow4mSslLLtYP1J3n38eKyW9w==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/@react-native/metro-babel-transformer/node_modules/hermes-parser": {
"version": "0.36.0",
"resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.36.0.tgz",
"integrity": "sha512-GdpwMmH5x6IpC1cijvcvYnlPB60Mh6kTSF/NFdYV/j56gYdi+0RIakYs+eqOV+bbO0SW7mgVVGSsTJxyPQfo3w==",
"license": "MIT",
- "peer": true,
"dependencies": {
"hermes-estree": "0.36.0"
}
@@ -3556,7 +3840,6 @@
"resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.86.2.tgz",
"integrity": "sha512-hJno256j+MS0b3JD1aD3ouTGZVacKNVBuXL2atMQQ8BZ060vl1ptnZ83y569aDW+/rgFSOcqn6ydKeSz4uUKQQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@react-native/js-polyfills": "0.86.2",
"@react-native/metro-babel-transformer": "0.86.2",
@@ -3924,6 +4207,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
"integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -3934,7 +4218,6 @@
"integrity": "sha512-HZOXsKT0tGI9LlUw2LuedXsVeB88wFa536vVL0M6vE8zN63nI+sSr1ByxmPToP5K5bukaVscyeCJcF9guVNJ1g==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"peerDependencies": {
"@types/react": "*"
}
@@ -4022,6 +4305,7 @@
"integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.67.0",
"@typescript-eslint/types": "8.67.0",
@@ -4607,6 +4891,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5157,9 +5442,9 @@
}
},
"node_modules/babel-preset-expo": {
- "version": "57.0.7",
- "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-57.0.7.tgz",
- "integrity": "sha512-/1RLnZTJVoTNo6nCdSv27BSA2LBzM/qEkNLznwWvztg64DhsAz7ByZIiAqdbau9FK3YqF+IV5a2ZsPXFclwq4A==",
+ "version": "57.0.8",
+ "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-57.0.8.tgz",
+ "integrity": "sha512-i4XOFUayGudQ845hbzKYYDAr2CL9jF1CQh8ehZTlNu3SKsWEIM+1KyY91P/3aqiEDsQOu5Pa/dVs937HvQTXig==",
"license": "MIT",
"dependencies": {
"@babel/generator": "^7.20.5",
@@ -5208,7 +5493,7 @@
"peerDependencies": {
"@babel/runtime": "^7.20.0",
"expo": "*",
- "expo-widgets": "^57.0.10",
+ "expo-widgets": "^57.0.12",
"react-refresh": ">=0.14.0 <1.0.0"
},
"peerDependenciesMeta": {
@@ -5354,6 +5639,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.11.12",
"caniuse-lite": "^1.0.30001809",
@@ -6571,6 +6857,7 @@
"integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -6778,6 +7065,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -7333,31 +7621,32 @@
}
},
"node_modules/expo": {
- "version": "57.0.15",
- "resolved": "https://registry.npmjs.org/expo/-/expo-57.0.15.tgz",
- "integrity": "sha512-9UooISw8S8Gxo9wpsPcGkyRlf2UzNr+F26LSU2+M8iaiZ1WAjPR0I75NKFX8ETbrSgthZ2lQHgy5IU2KE/mpBA==",
+ "version": "57.0.16",
+ "resolved": "https://registry.npmjs.org/expo/-/expo-57.0.16.tgz",
+ "integrity": "sha512-+RyhqrVXDJDJGlhV24DM8pLmofrR63J5q9xNcbWJlRefR7ggVgHmrl2DNs7qDYSlbFOAbIM0ijN7wanMi2PAMw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.20.0",
- "@expo/cli": "^57.0.17",
- "@expo/config": "~57.0.8",
- "@expo/config-plugins": "~57.0.8",
+ "@expo/cli": "^57.0.18",
+ "@expo/config": "~57.0.9",
+ "@expo/config-plugins": "~57.0.9",
"@expo/devtools": "~57.0.1",
"@expo/dom-webview": "~57.0.1",
- "@expo/fingerprint": "^0.20.9",
+ "@expo/fingerprint": "^0.20.10",
"@expo/local-build-cache-provider": "^57.0.7",
"@expo/log-box": "^57.0.3",
- "@expo/metro": "~56.0.0",
- "@expo/metro-config": "~57.0.9",
+ "@expo/metro": "~56.0.2",
+ "@expo/metro-config": "~57.0.10",
"@ungap/structured-clone": "^1.3.0",
- "babel-preset-expo": "~57.0.7",
- "expo-asset": "~57.0.13",
- "expo-constants": "~57.0.13",
+ "babel-preset-expo": "~57.0.8",
+ "expo-asset": "~57.0.14",
+ "expo-constants": "~57.0.14",
"expo-file-system": "~57.0.5",
"expo-font": "~57.0.1",
"expo-keep-awake": "~57.0.1",
- "expo-modules-autolinking": "~57.0.10",
- "expo-modules-core": "~57.0.12",
+ "expo-modules-autolinking": "~57.0.11",
+ "expo-modules-core": "~57.0.13",
"pretty-format": "^29.7.0",
"react-refresh": "^0.14.2",
"whatwg-url-minimum": "^0.1.2"
@@ -7395,13 +7684,13 @@
}
},
"node_modules/expo-asset": {
- "version": "57.0.13",
- "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-57.0.13.tgz",
- "integrity": "sha512-RPjMcmPXRMb6UbQdTIkmSDEnQL5LKGTu7ZXatq3U67V6ldlSdQeQ9pQV7zlmk0DrbnaMEh3HYiEh2YxWLNyCzA==",
+ "version": "57.0.14",
+ "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-57.0.14.tgz",
+ "integrity": "sha512-OR1wVSRcF6g8clB0tk3rY/uv3k8MUHf5PN6cHulnNpjkl5IYWpCvkcTZT3RFiJA9ThkumvWaWIJWtLAIeesTJw==",
"license": "MIT",
"dependencies": {
- "@expo/image-utils": "^0.11.4",
- "expo-constants": "~57.0.13"
+ "@expo/image-utils": "^0.11.5",
+ "expo-constants": "~57.0.14"
},
"peerDependencies": {
"expo": "*",
@@ -7409,10 +7698,21 @@
"react-native": "*"
}
},
+ "node_modules/expo-blur": {
+ "version": "57.0.2",
+ "resolved": "https://registry.npmjs.org/expo-blur/-/expo-blur-57.0.2.tgz",
+ "integrity": "sha512-Aoud8H8lmlNkbRufyvRLefmGFELdBf1n5Te/Xm+Zx8ORINH+aXL+gKb5mbftFSha860+I7pMArz77TBYz8HDVg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/expo-build-properties": {
- "version": "57.0.13",
- "resolved": "https://registry.npmjs.org/expo-build-properties/-/expo-build-properties-57.0.13.tgz",
- "integrity": "sha512-8lpBDTKJd7qGyvGdPS/CWW0ZR0GYMHBlLbSN9lL57JT6XLetDjnOZC0nBV4iOm7UKArNVYtvLIgzEeH1OfnEOQ==",
+ "version": "57.0.14",
+ "resolved": "https://registry.npmjs.org/expo-build-properties/-/expo-build-properties-57.0.14.tgz",
+ "integrity": "sha512-gIU7e37gHWDcozQPelPzGlIa+lg8s8/O/gSuU/M7isy46NMdrBZIu/1TCPuL6LwRUCErNvuzNs3t6ljtiGSOkw==",
"license": "MIT",
"dependencies": {
"@expo/schema-utils": "^57.0.2",
@@ -7435,10 +7735,11 @@
}
},
"node_modules/expo-constants": {
- "version": "57.0.13",
- "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.13.tgz",
- "integrity": "sha512-eB5AHp7kKxsVIBjTetUgj5WQSw8joI2ekzgTlcUv+Hc0x2t0jZU6IiL4DpyOT2yZQ71n0WJZCkmOKLgxlajIzg==",
+ "version": "57.0.14",
+ "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.14.tgz",
+ "integrity": "sha512-c9WlP18GqXRm/Kaa7rALRbpFvWiuijrXu3C/38iYdHt9lscA8deJhfK7TURRApWLacdzre5mnNmHdEthNeCEjg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@expo/env": "~2.4.2"
},
@@ -7448,22 +7749,22 @@
}
},
"node_modules/expo-crypto": {
- "version": "57.0.1",
- "resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-57.0.1.tgz",
- "integrity": "sha512-xwegXQw3ATgeL1ZuqbSNrGzOeG+zNeh6Z6DSJk825Qpa3TEQQ1kG3ioE1p3g/SNF373BAVz2iBKUTSytlIbBRA==",
+ "version": "57.0.2",
+ "resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-57.0.2.tgz",
+ "integrity": "sha512-OGLvHK7Hb7qsONMWGB9pJw3n+iY4OnZQmM/KwQEHRMrrOmNg0Lxj9+tp9bj46aAMMzXx0FTVA2jUKoplpXPnkA==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-dev-client": {
- "version": "57.0.14",
- "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-57.0.14.tgz",
- "integrity": "sha512-0WEZJZtUZC0PgkZXm3BHWVYiOBM8hbkMP50Nk24X+K6k5+EWBmwmsSO/K4sojKyO1Pxhb/GHtSYaV8t1YI1x/w==",
+ "version": "57.0.15",
+ "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-57.0.15.tgz",
+ "integrity": "sha512-3ryaF/bVBmq7v0jzg07zkBvPmfW6B34xZbEU0j18p6nedtJjAMfa2wNmyB7Eq1I/vhhxs5uI21nh08eRaZgF2g==",
"license": "MIT",
"dependencies": {
- "expo-dev-launcher": "~57.0.14",
- "expo-dev-menu": "~57.0.14",
+ "expo-dev-launcher": "~57.0.15",
+ "expo-dev-menu": "~57.0.15",
"expo-dev-menu-interface": "~57.0.0",
"expo-manifests": "~57.0.1",
"expo-updates-interface": "~57.0.1"
@@ -7473,13 +7774,13 @@
}
},
"node_modules/expo-dev-launcher": {
- "version": "57.0.14",
- "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-57.0.14.tgz",
- "integrity": "sha512-zb1Y6cBIhniBjTpihBHaWx0O81w3aWp5ddAw+ThpfCGrMVlYCdaBb2zlo1dZ1swJna7mn09eIID97U3pPAh83g==",
+ "version": "57.0.15",
+ "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-57.0.15.tgz",
+ "integrity": "sha512-lTRHfPcDKog/lK4RgQdN/252lplGm3MZ5DrVjVsUcmgjx6YfIohMRtkW3DTXXdRfTRcfpGUIFKg9JZvc1RO5kA==",
"license": "MIT",
"dependencies": {
"@expo/schema-utils": "^57.0.2",
- "expo-dev-menu": "~57.0.14",
+ "expo-dev-menu": "~57.0.15",
"expo-manifests": "~57.0.1"
},
"peerDependencies": {
@@ -7488,9 +7789,9 @@
}
},
"node_modules/expo-dev-menu": {
- "version": "57.0.14",
- "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-57.0.14.tgz",
- "integrity": "sha512-lTD5ErQrrH57v1HOM4RY2gA9hmvqXoZ/AArM7LQLCts3fKqaahOGg45ETRUgdvULv3K4wbKwhXwCRoANWMxGTw==",
+ "version": "57.0.15",
+ "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-57.0.15.tgz",
+ "integrity": "sha512-T6kJ/G9P+Ef5MS3TpH/JBtZIt+v4kQKUED1gzUV9aqKfCKhI6jC3rVV3vTGqBMOA2/RaP2VlkmTrg9/aSXtlRA==",
"license": "MIT",
"dependencies": {
"expo-dev-menu-interface": "~57.0.0"
@@ -7536,6 +7837,7 @@
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-57.0.1.tgz",
"integrity": "sha512-QyS9L1Kh9sKJg4gfU6rdbpxpmH+DyzBX8z6jVvXMUDoqLr1GqmkO/Wu379KCXjL///kWbhpNlbi7AgBuj4VdIQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"fontfaceobserver": "^2.1.0"
},
@@ -7597,6 +7899,7 @@
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-57.0.7.tgz",
"integrity": "sha512-Ujn4kY0bd1MoJFLYuvIUWamlZH61pkahrUpug3hBtOeS89LDbPyKNjjmWef6+CPTGQfed2PlfuMZuPO1BCrg5w==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"expo-constants": "~57.0.13",
"invariant": "^2.2.4"
@@ -7619,12 +7922,12 @@
}
},
"node_modules/expo-modules-autolinking": {
- "version": "57.0.10",
- "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-57.0.10.tgz",
- "integrity": "sha512-jNqLswMx8QHN8VXRgud+xT+9q+J9U63CEGlx+k4V/IE9Qyt9HrKbWp+pIDma0fOquta5HBfSfsShcI+NkEpTiA==",
+ "version": "57.0.11",
+ "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-57.0.11.tgz",
+ "integrity": "sha512-cvbEwYDKGhvMWoStZVceCdFRvrizm82uNMIDKIeo6NO4X/QC7PN4Eqm7Vn57DmgBdZNBJIk/647/NpNowPqAxQ==",
"license": "MIT",
"dependencies": {
- "@expo/require-utils": "^57.0.4",
+ "@expo/require-utils": "^57.0.5",
"@expo/spawn-async": "^1.8.0",
"chalk": "^4.1.0",
"commander": "^7.2.0"
@@ -7634,9 +7937,9 @@
}
},
"node_modules/expo-modules-core": {
- "version": "57.0.12",
- "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-57.0.12.tgz",
- "integrity": "sha512-hKqCdu8+78oNKWCgM4xSeblfmD/audgNVCn6uUCJuNBS/hc4DyC0Ia1X96SSZF9w9bGTLjNS8j2b7DshiC3WJA==",
+ "version": "57.0.13",
+ "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-57.0.13.tgz",
+ "integrity": "sha512-RQRlhb5ETd5zgS4RV02ktZRLGXFAe8EgK6gywQTq1lBZkthUrVsmg9j5xQ9nijQmTZqdsJtHNifaQdNoKf7ugw==",
"license": "MIT",
"dependencies": {
"@expo/expo-modules-macros-plugin": "0.6.1",
@@ -7664,15 +7967,16 @@
}
},
"node_modules/expo-router": {
- "version": "57.0.15",
- "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-57.0.15.tgz",
- "integrity": "sha512-vwr2HL2U7hqv3QTkuz0e8hQc5hkgpXztisy30VwhdXJVDPdP0/m8wSiYUm6GKyZAbcXfD4fn44ZYcJsSESY6cA==",
+ "version": "57.0.16",
+ "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-57.0.16.tgz",
+ "integrity": "sha512-k2ykpt91f2tVuaK+NTlyZPpy7ZL+VigGdgcrnTg+3KMDLBItXFAZezubeOe93CiirjJpXt5ktgQz8QMSse22HA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@expo/log-box": "^57.0.3",
- "@expo/metro-runtime": "^57.0.12",
+ "@expo/metro-runtime": "^57.0.13",
"@expo/schema-utils": "^57.0.2",
- "@expo/ui": "^57.0.12",
+ "@expo/ui": "^57.0.13",
"@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-tabs": "^1.1.12",
"@react-native-masked-view/masked-view": "^0.3.2",
@@ -7699,10 +8003,10 @@
},
"peerDependencies": {
"@expo/log-box": "^57.0.3",
- "@expo/metro-runtime": "^57.0.12",
+ "@expo/metro-runtime": "^57.0.13",
"@testing-library/react-native": ">= 13.2.0",
"expo": "*",
- "expo-constants": "^57.0.13",
+ "expo-constants": "^57.0.14",
"expo-linking": "^57.0.7",
"react": "*",
"react-dom": "*",
@@ -7749,17 +8053,18 @@
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-57.0.3.tgz",
"integrity": "sha512-aK+LdKzauHSGmsOStZtyxdzv0zWssCkxTw3m4QuOhfDSJsZaMRTd9O41d8ixU/QfELTbaJ0oRNcF7JFV/7O9YQ==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.16.0"
}
},
"node_modules/expo-sharing": {
- "version": "57.0.14",
- "resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-57.0.14.tgz",
- "integrity": "sha512-X/GG2Z/aGHzC90gnX4rX88BJjoWEt5Piqgeybb0taOvSCu8N/sHROvbIrYp07NSLagOqGTYAgfDmmyQXtrchJQ==",
+ "version": "57.0.15",
+ "resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-57.0.15.tgz",
+ "integrity": "sha512-2OT7aCk0Qh/nW3A5AuNeAHNd1P+RWA8f5k4Vc5LdaaYFX/8xdNPIzCOC07s7qaoIE0iuaf8dXQCAxCGXJWZrBw==",
"license": "MIT",
"dependencies": {
- "@expo/config-plugins": "^57.0.8",
+ "@expo/config-plugins": "^57.0.9",
"@expo/config-types": "^57.0.2",
"@expo/plist": "^0.8.1"
},
@@ -7770,13 +8075,13 @@
}
},
"node_modules/expo-splash-screen": {
- "version": "57.0.7",
- "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-57.0.7.tgz",
- "integrity": "sha512-QxqfjOCTnTGnqK2BxfEtxUJOcnsJbUXO873Q342r3VkgHAG877eozsNzgYWQXcCFj/asMwx2uy9OXxBShxc5rQ==",
+ "version": "57.0.8",
+ "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-57.0.8.tgz",
+ "integrity": "sha512-BEsrKg4niYBZa5AFzWRyGqxA4ZemtBOPwbCy+C336iGYYPuZYLEgWJGmm2xD1gRfCdrM/TP+l2ONd86Zvk3KyA==",
"license": "MIT",
"dependencies": {
- "@expo/config-plugins": "~57.0.8",
- "@expo/image-utils": "^0.11.4",
+ "@expo/config-plugins": "~57.0.9",
+ "@expo/image-utils": "^0.11.5",
"xml2js": "0.6.0"
},
"peerDependencies": {
@@ -7850,28 +8155,28 @@
}
},
"node_modules/expo/node_modules/@expo/cli": {
- "version": "57.0.17",
- "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-57.0.17.tgz",
- "integrity": "sha512-PQc7if117dNh2qe+CHdlmSWpgwhFiP9CJ7XbwNF0w4KSKxFpJ2qUjDPoAcCMW1VZg1x30mEJ907bT8G1iDIZBQ==",
+ "version": "57.0.18",
+ "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-57.0.18.tgz",
+ "integrity": "sha512-ZLHe7/a76nno3ZvC8czmUhbv/tmdzKa27fqW2J/Ybhx78u71C7qpeRk8UgWGYWeIhonT8XILVMxECjlYsj6hfQ==",
"license": "MIT",
"dependencies": {
"@expo/code-signing-certificates": "^0.0.6",
- "@expo/config": "~57.0.8",
- "@expo/config-plugins": "~57.0.8",
+ "@expo/config": "~57.0.9",
+ "@expo/config-plugins": "~57.0.9",
"@expo/devcert": "^1.2.1",
"@expo/env": "~2.4.2",
- "@expo/image-utils": "^0.11.4",
+ "@expo/image-utils": "^0.11.5",
"@expo/inline-modules": "^0.1.6",
"@expo/json-file": "^11.0.1",
"@expo/log-box": "^57.0.3",
- "@expo/metro": "~56.0.0",
- "@expo/metro-config": "~57.0.9",
- "@expo/metro-file-map": "^57.0.1",
+ "@expo/metro": "~56.0.2",
+ "@expo/metro-config": "~57.0.10",
+ "@expo/metro-file-map": "^57.0.2",
"@expo/osascript": "^2.7.1",
"@expo/package-manager": "^1.13.1",
"@expo/plist": "^0.8.1",
- "@expo/prebuild-config": "^57.0.13",
- "@expo/require-utils": "^57.0.4",
+ "@expo/prebuild-config": "^57.0.14",
+ "@expo/require-utils": "^57.0.5",
"@expo/router-server": "^57.0.7",
"@expo/schema-utils": "^57.0.2",
"@expo/spawn-async": "^1.8.0",
@@ -8038,6 +8343,7 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=10.0.0"
},
@@ -8370,6 +8676,7 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -9673,6 +9980,7 @@
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@jest/core": "^29.7.0",
"@jest/types": "^29.6.3",
@@ -13023,6 +13331,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -13042,6 +13351,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -13094,6 +13404,7 @@
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.86.2.tgz",
"integrity": "sha512-zbJXGZpwfZGA79Z9ob6Atvfx4nAQL8yJBa35s58E4Oo+khPykfQP2sTeumkKbjwajFYfVayg8pj7Il9nIfTk7A==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@react-native/assets-registry": "0.86.2",
"@react-native/codegen": "0.86.2",
@@ -13182,6 +13493,7 @@
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.32.0.tgz",
"integrity": "sha512-uYIMOKlKENORq2SABE+jIjbPU+h5I/sQKcq2v16zRq848nwEp1fWRVwML4QWqijc8UcXJC25o54S8GQd4Mf2OA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@egjs/hammerjs": "^2.0.17",
"@types/react-test-renderer": "^19.1.0",
@@ -13208,6 +13520,7 @@
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.5.1.tgz",
"integrity": "sha512-RnMvtDuR+68ig864gAvZCOdZehqhC5rFmMo0kn+ARfgVSTvFeF6IFLBVgMPUu0KwihaapEyW24WRi6nEyy1kSA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"react-native-is-edge-to-edge": "^1.3.1",
"semver": "^7.7.3"
@@ -13223,6 +13536,7 @@
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.7.0.tgz",
"integrity": "sha512-/9/MtQz8ODphjsLdZ+GZAIcC/RtoqW9EeShf7Uvnfgm/pzYrJ75y3PV/J1wuAV1T5Dye5ygq4EAW20RoBq0ABQ==",
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"react": "*",
"react-native": "*"
@@ -13247,6 +13561,7 @@
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
"integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.18.6",
"@react-native/normalize-colors": "^0.74.1",
@@ -13279,6 +13594,7 @@
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.10.1.tgz",
"integrity": "sha512-62mRM19bDpfpdI8HLkEErcdOsrAPDtE9lA/sw+5lLRpzBHNhxaoj9QyY2KjXqUmirelxkX4zuPGTC3VdA0feJA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/plugin-transform-arrow-functions": "^7.27.1",
"@babel/plugin-transform-class-properties": "^7.28.6",
@@ -13339,7 +13655,6 @@
"integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -13355,6 +13670,7 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -14938,6 +15254,7 @@
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"devOptional": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -15675,6 +15992,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
+ "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/package.json b/package.json
index d6e7b1d..1a6547f 100644
--- a/package.json
+++ b/package.json
@@ -3,28 +3,29 @@
"main": "expo-router/entry",
"version": "1.0.0-beta.13",
"dependencies": {
- "@expo/ui": "~57.0.12",
+ "@expo/ui": "~57.0.13",
"@expo/vector-icons": "^15.0.2",
"@noble/hashes": "^2.3.0",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-navigation/native": "^7.3.16",
"base64-js": "^1.5.1",
- "expo": "~57.0.15",
- "expo-build-properties": "~57.0.13",
+ "expo": "~57.0.16",
+ "expo-blur": "~57.0.2",
+ "expo-build-properties": "~57.0.14",
"expo-clipboard": "~57.0.1",
"expo-constants": "~57.0.12",
- "expo-crypto": "~57.0.1",
- "expo-dev-client": "~57.0.14",
+ "expo-crypto": "~57.0.2",
+ "expo-dev-client": "~57.0.15",
"expo-device": "~57.0.1",
"expo-file-system": "~57.0.4",
"expo-font": "~57.0.1",
"expo-glass-effect": "~57.0.1",
"expo-image": "~57.0.3",
"expo-linking": "~57.0.7",
- "expo-router": "~57.0.15",
+ "expo-router": "~57.0.16",
"expo-secure-store": "~57.0.1",
- "expo-sharing": "~57.0.14",
- "expo-splash-screen": "~57.0.7",
+ "expo-sharing": "~57.0.15",
+ "expo-splash-screen": "~57.0.8",
"expo-status-bar": "~57.0.1",
"expo-symbols": "~57.0.2",
"expo-system-ui": "~57.0.2",
@@ -41,6 +42,8 @@
"react-native-worklets": "0.10.1"
},
"devDependencies": {
+ "@emnapi/core": "^1.11.3",
+ "@emnapi/runtime": "^1.11.3",
"@testing-library/react-native": "^14.0.1",
"@types/jest": "^29.5.14",
"@types/react": "~19.2.2",
diff --git a/src/app/(tabs)/remote.tsx b/src/app/(tabs)/remote.tsx
index a406c08..3f51937 100644
--- a/src/app/(tabs)/remote.tsx
+++ b/src/app/(tabs)/remote.tsx
@@ -48,8 +48,7 @@ export default function RemoteScreen() {
return ;
}
return (
- } bottomAccessory={deviceSwitcher}>
-
+ } bottomAccessory={deviceSwitcher} stickyAccessory={}>
{preferences.surface === 'mouse' ? 0} /> : null}
{preferences.surface === 'typing' ? : null}
{preferences.surface === 'window' ? : null}
diff --git a/src/components/Screen.tsx b/src/components/Screen.tsx
index 1a89202..0dbcc9a 100644
--- a/src/components/Screen.tsx
+++ b/src/components/Screen.tsx
@@ -1,31 +1,118 @@
-import type { PropsWithChildren, ReactNode } from 'react';
-import { ScrollView, View } from 'react-native';
+import { BlurTargetView } from 'expo-blur';
+import { type PropsWithChildren, type ReactNode, useCallback, useRef, useState } from 'react';
+import { ScrollView, View, type NativeScrollEvent, type NativeSyntheticEvent } from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { AppText } from './AppText';
+import { StickyBackdrop } from './StickyBackdrop';
import { useLayout, useTheme } from '@/theme/ThemeContext';
-type ScreenProps = PropsWithChildren<{ title: string; description?: string; headerAccessory?: ReactNode; bottomAccessory?: ReactNode; nativeHeader?: boolean }>;
+type ScreenProps = PropsWithChildren<{
+ title: string;
+ description?: string;
+ headerAccessory?: ReactNode;
+ bottomAccessory?: ReactNode;
+ nativeHeader?: boolean;
+ stickyAccessory?: ReactNode;
+}>;
-export function Screen({ title, description, headerAccessory, bottomAccessory, nativeHeader = false, children }: ScreenProps) {
+function ScreenHeader({ description, headerAccessory, stackHeader, title }: { description: string | undefined; headerAccessory: ReactNode; stackHeader: boolean; title: string }) {
+ const { spacing } = useTheme();
+ return
+
+ {title}
+ {description ? {description} : null}
+
+ {headerAccessory ? {headerAccessory} : null}
+ ;
+}
+
+type StickyScreenContentProps = PropsWithChildren<{
+ description: string | undefined;
+ headerAccessory: ReactNode;
+ isExpanded: boolean;
+ nativeHeader: boolean;
+ paddingBottom: number;
+ paddingHorizontal: number;
+ stackHeader: boolean;
+ stickyAccessory: ReactNode;
+ title: string;
+}>;
+
+function StickyScreenContent({ children, description, headerAccessory, isExpanded, nativeHeader, paddingBottom, paddingHorizontal, stackHeader, stickyAccessory, title }: StickyScreenContentProps) {
+ const { spacing } = useTheme();
+ const [pinned, setPinned] = useState(false);
+ const blurTarget = useRef(null);
+ const pinnedRef = useRef(false);
+ const scrollOffsetRef = useRef(0);
+ const stickyTopRef = useRef(null);
+ const maxWidth = isExpanded ? 960 : 640;
+
+ const updatePinned = useCallback(() => {
+ if (stickyTopRef.current === null) return;
+ const nextPinned = scrollOffsetRef.current >= stickyTopRef.current - 1;
+ if (nextPinned === pinnedRef.current) return;
+ pinnedRef.current = nextPinned;
+ setPinned(nextPinned);
+ }, []);
+
+ const handleScroll = useCallback((event: NativeSyntheticEvent) => {
+ scrollOffsetRef.current = event.nativeEvent.contentOffset.y;
+ updatePinned();
+ }, [updatePinned]);
+
+ return
+
+ {!nativeHeader ? : null}
+
+ {
+ stickyTopRef.current = event.nativeEvent.layout.y;
+ updatePinned();
+ }}
+ style={{ maxWidth, paddingVertical: spacing.sm, width: '100%' }}
+ >
+
+ {stickyAccessory}
+
+
+ {children}
+
+ ;
+}
+
+export function Screen({ title, description, headerAccessory, bottomAccessory, nativeHeader = false, stickyAccessory, children }: ScreenProps) {
const { colors, spacing } = useTheme();
const { isCompact, isExpanded, isLargeText } = useLayout();
const insets = useSafeAreaInsets();
const stackHeader = isCompact || isLargeText;
+ const paddingBottom = bottomAccessory ? spacing.xl : spacing.xxxl + insets.bottom;
+ const paddingHorizontal = isExpanded ? spacing.xxl : spacing.xl;
return (
-
+ {stickyAccessory === undefined ?
- {!nativeHeader ?
-
- {title}
- {description ? {description} : null}
-
- {headerAccessory ? {headerAccessory} : null}
- : null}
+ {!nativeHeader ? : null}
{children}
-
+ : {children}}
{bottomAccessory ?
{bottomAccessory}
: null}
diff --git a/src/components/StickyBackdrop.test.tsx b/src/components/StickyBackdrop.test.tsx
new file mode 100644
index 0000000..38f3977
--- /dev/null
+++ b/src/components/StickyBackdrop.test.tsx
@@ -0,0 +1,60 @@
+import { render } from '@testing-library/react-native';
+import { StyleSheet, type View } from 'react-native';
+import type { RefObject } from 'react';
+
+import { palettes, radii, spacing, typography } from '@/theme/tokens';
+import { StickyBackdrop } from './StickyBackdrop';
+
+const mockUseTheme = jest.fn();
+
+jest.mock('@/theme/ThemeContext', () => ({
+ useTheme: () => mockUseTheme(),
+}));
+
+const blurTarget = { current: null } as RefObject;
+
+function theme(scheme: 'dark' | 'light', reducedTransparency = false) {
+ return { colors: palettes[scheme], radii, reducedMotion: false, reducedTransparency, scheme, spacing, typography };
+}
+
+describe('StickyBackdrop', () => {
+ beforeEach(() => mockUseTheme.mockReturnValue(theme('dark')));
+
+ it('renders nothing before content reaches the sticky selector', async () => {
+ const view = await render();
+ expect(view.toJSON()).toBeNull();
+ });
+
+ it('uses the Android 12+ blur method, target, and dark wash', async () => {
+ const view = await render();
+ const blur = view.getByTestId('screen-sticky-blur-view', { includeHiddenElements: true });
+ expect(blur.props.blurMethod).toBe('dimezisBlurViewSdk31Plus');
+ expect(blur.props.blurTarget).toBe(blurTarget);
+ expect(blur.props.intensity).toBe(70);
+ expect(blur.props.tint).toBe('dark');
+ expect(blur.props.pointerEvents).toBe('none');
+ expect(StyleSheet.flatten(view.getByTestId('screen-sticky-backdrop-wash', { includeHiddenElements: true }).props.style).backgroundColor).toBe('rgba(11, 11, 13, 0.72)');
+ });
+
+ it('uses the light tint and wash with the light theme', async () => {
+ mockUseTheme.mockReturnValue(theme('light'));
+ const view = await render();
+ expect(view.getByTestId('screen-sticky-blur-view', { includeHiddenElements: true }).props.tint).toBe('light');
+ expect(StyleSheet.flatten(view.getByTestId('screen-sticky-backdrop-wash', { includeHiddenElements: true }).props.style).backgroundColor).toBe('rgba(245, 245, 247, 0.72)');
+ });
+
+ it('uses an opaque, non-interactive fallback when transparency is reduced', async () => {
+ mockUseTheme.mockReturnValue(theme('dark', true));
+ const view = await render();
+ const fallback = view.getByTestId('screen-sticky-backdrop-solid', { includeHiddenElements: true });
+ expect(view.queryByTestId('screen-sticky-blur-view', { includeHiddenElements: true })).toBeNull();
+ expect(fallback.props.accessible).toBe(false);
+ expect(fallback.props.importantForAccessibility).toBe('no-hide-descendants');
+ expect(fallback.props.pointerEvents).toBe('none');
+ expect(StyleSheet.flatten(fallback.props.style)).toMatchObject({
+ backgroundColor: palettes.dark.background,
+ borderBottomColor: palettes.dark.border,
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ });
+ });
+});
diff --git a/src/components/StickyBackdrop.tsx b/src/components/StickyBackdrop.tsx
new file mode 100644
index 0000000..4180905
--- /dev/null
+++ b/src/components/StickyBackdrop.tsx
@@ -0,0 +1,41 @@
+import { BlurView } from 'expo-blur';
+import type { RefObject } from 'react';
+import { StyleSheet, View, type View as NativeView } from 'react-native';
+
+import { useTheme } from '@/theme/ThemeContext';
+
+export function StickyBackdrop({ blurTarget, visible }: { blurTarget: RefObject; visible: boolean }) {
+ const { colors, reducedTransparency, scheme } = useTheme();
+
+ if (!visible) return null;
+
+ const frame = [
+ StyleSheet.absoluteFill,
+ { borderBottomColor: colors.border, borderBottomWidth: StyleSheet.hairlineWidth },
+ ];
+
+ if (reducedTransparency) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/components/accessibility.test.tsx b/src/components/accessibility.test.tsx
index 3f2bbc2..eb7a6ec 100644
--- a/src/components/accessibility.test.tsx
+++ b/src/components/accessibility.test.tsx
@@ -19,6 +19,40 @@ describe('accessibility primitives', () => {
const view = await render( undefined} />);
expect(view.getByRole('header')).toBeTruthy();
expect(view.getByRole('button', { name: 'Click' })).toBeTruthy();
+ expect(view.getByTestId('screen-scroll').props.stickyHeaderIndices).toBeUndefined();
+ expect(view.getByTestId('screen-scroll').props.onScroll).toBeUndefined();
+ expect(view.queryByTestId('screen-sticky-accessory')).toBeNull();
+ });
+
+ it('pins a supplied accessory without adding scanning stops', async () => {
+ const view = await render( undefined} />} bottomAccessory={ undefined} />}> undefined} />);
+ const scroll = view.getByTestId('screen-scroll');
+ const header = view.getByTestId('screen-sticky-header-content');
+ const sticky = view.getByTestId('screen-sticky-accessory');
+ const content = view.getByTestId('screen-content');
+
+ expect(scroll.props.stickyHeaderIndices).toEqual([1]);
+ expect(scroll.props.scrollEventThrottle).toBe(16);
+ expect(header.parent).toBe(sticky.parent);
+ expect(sticky.parent).toBe(content.parent);
+ expect(header.parent?.children).toEqual([header, sticky, content]);
+ expect(view.queryByTestId('screen-sticky-backdrop-blur')).toBeNull();
+ expect(view.getAllByRole('button')).toHaveLength(3);
+ expect(view.getByTestId('screen-scroll').parent?.props.children[1].props.testID).toBe('screen-bottom-accessory');
+
+ await fireEvent(view.getByTestId('screen-sticky-accessory'), 'layout', { nativeEvent: { layout: { height: 64, width: 350, x: 0, y: 100 } } });
+ await fireEvent.scroll(scroll, { nativeEvent: { contentOffset: { x: 0, y: 99 } } });
+ expect(view.queryByTestId('screen-sticky-backdrop-blur')).toBeNull();
+ await fireEvent.scroll(scroll, { nativeEvent: { contentOffset: { x: 0, y: 100 } } });
+ expect(view.getByTestId('screen-sticky-backdrop-blur', { includeHiddenElements: true })).toBeTruthy();
+ expect(view.getAllByRole('button')).toHaveLength(3);
+
+ await fireEvent(view.getByTestId('screen-sticky-accessory'), 'layout', { nativeEvent: { layout: { height: 96, width: 350, x: 0, y: 140 } } });
+ expect(view.queryByTestId('screen-sticky-backdrop-blur')).toBeNull();
+ await fireEvent.scroll(scroll, { nativeEvent: { contentOffset: { x: 0, y: 150 } } });
+ expect(view.getByTestId('screen-sticky-backdrop-blur', { includeHiddenElements: true })).toBeTruthy();
+ await fireEvent.scroll(scroll, { nativeEvent: { contentOffset: { x: 0, y: 0 } } });
+ expect(view.queryByTestId('screen-sticky-backdrop-blur')).toBeNull();
});
it('keeps read-only status text out of switch scanning', async () => {
diff --git a/src/remote/DisconnectedRemote.tsx b/src/remote/DisconnectedRemote.tsx
index 501e987..dffd5af 100644
--- a/src/remote/DisconnectedRemote.tsx
+++ b/src/remote/DisconnectedRemote.tsx
@@ -41,8 +41,7 @@ export function DisconnectedRemote({ connection, selectedSurface, retry, choose,
const presentation = disconnectedRemotePresentation(connection);
useAccessibilityAnnouncement(presentation.message);
return (
-
-
+ }>
{presentation.busy ? undefined} /> : null}{presentation.primaryAction ? : null}{presentation.chooseAction ? : null}>} />
);
diff --git a/src/remote/RemoteScreen.test.tsx b/src/remote/RemoteScreen.test.tsx
new file mode 100644
index 0000000..e95a91c
--- /dev/null
+++ b/src/remote/RemoteScreen.test.tsx
@@ -0,0 +1,105 @@
+import { render } from '@testing-library/react-native';
+import { createElement } from 'react';
+import { Text } from 'react-native';
+
+import type { ConnectionState } from '@/connection/ConnectionManager';
+import type { PointerProfile } from '@/domain/protocol/types';
+import RemoteScreen from '@/app/(tabs)/remote';
+
+const mockSurface = (label: string) => createElement(Text, null, label);
+
+const mockManager = {
+ connectPreferred: jest.fn(),
+ registerCleanup: jest.fn(() => () => undefined),
+};
+const mockBridge = {
+ connect: jest.fn(async () => true),
+ disconnect: jest.fn(async () => undefined),
+ nextGeneration: jest.fn(() => 1),
+ setForwardingActive: jest.fn(async () => true),
+ setRepeatActive: jest.fn(async () => true),
+ snapshot: jest.fn(() => ({ version: 1, captureAvailable: false, externalSwitches: [] })),
+ subscribe: jest.fn(() => () => undefined),
+};
+const mockBridgeSnapshot = { version: 1, captureAvailable: false, externalSwitches: [] };
+const mockPreferences = { surface: 'mouse' as const, typingMode: 'live' as const, draft: '', forwardingHoldToStopMs: 5_000, forwardingProfiles: {}, remoteName: null };
+const mockSessionState = { repeat: null, dragging: false, modifiers: [], streamOpen: false };
+let mockConnection: ConnectionState;
+
+jest.mock('expo-router', () => ({
+ useLocalSearchParams: () => ({}),
+ useRouter: () => ({ navigate: jest.fn() }),
+}));
+jest.mock('@/bridge/BridgeContext', () => ({
+ useBridgeSnapshot: () => mockBridgeSnapshot,
+ useSwitchifyBridge: () => mockBridge,
+}));
+jest.mock('@/connection/ConnectionContext', () => ({
+ useConnectionManager: () => mockManager,
+ useConnectionState: () => mockConnection,
+}));
+jest.mock('@/connection/usePreferredPcConnection', () => ({ usePreferredPcConnection: () => undefined }));
+jest.mock('@/storage/usePreferences', () => ({ usePreferences: () => mockPreferences }));
+jest.mock('@/remote/useProfileStatusAnnouncement', () => ({ useProfileStatusAnnouncement: () => undefined }));
+jest.mock('@/remote/RemoteDeviceSwitcher', () => ({ RemoteDeviceSwitcher: () => null }));
+jest.mock('@/remote/RemoteSession', () => ({
+ RemoteSession: class {
+ cleanup = jest.fn(async () => undefined);
+ dispose = jest.fn();
+ snapshot = () => mockSessionState;
+ subscribe = () => () => undefined;
+ },
+}));
+jest.mock('@/remote/MouseSurface', () => ({ MouseSurface: () => mockSurface('Mouse controls') }));
+jest.mock('@/remote/TypingSurface', () => ({ TypingSurface: () => mockSurface('Typing controls') }));
+jest.mock('@/remote/WindowSurface', () => ({ WindowSurface: () => mockSurface('Window controls') }));
+jest.mock('@/forwarding/ForwardingSurface', () => ({
+ ForwardingRestoreState: class { clear = jest.fn(); },
+ ForwardingSurface: () => mockSurface('Forwarding controls'),
+ shouldClearForwardingRestore: () => false,
+}));
+
+const desktop = { desktopId: 'desktop-1', displayName: 'Office PC', platform: 'windows' as const, peripheralId: 'peripheral-1', rssi: -50 };
+const profile: PointerProfile = {
+ displayId: 'display-1', scaleFactor: 1, bounds: { x: 0, y: 0, width: 1920, height: 1080 }, maxDelta: 128,
+ recommendedDeltas: { small: 16, medium: 32, large: 64 },
+ capabilities: {
+ noAckMouseMove: false, noAckCommands: [], supportedCommands: [],
+ mouseRepeat: { supported: false, enabled: false, intervalMs: 250, minIntervalMs: 100, maxIntervalMs: 2_000 },
+ pointerSpeed: { supported: false, setSupported: false, scalePercent: 100, minScalePercent: 5, maxScalePercent: 225, stepPercent: 5, baseMoveDelta: 32, effectiveMoveDelta: 32 },
+ displayNavigation: { supported: false, displayCount: 1 },
+ },
+};
+
+describe('RemoteScreen sticky surface selector', () => {
+ beforeEach(() => {
+ mockConnection = { kind: 'connected', desktop, profile, profileStatus: 'ready' };
+ jest.clearAllMocks();
+ });
+
+ it('pins the selector when connected controls are available', async () => {
+ const view = await render();
+ expect(view.getByTestId('screen-sticky-accessory')).toBeTruthy();
+ expect(view.getByRole('button', { name: 'Surface' })).toBeTruthy();
+ expect(view.getByText('Mouse controls')).toBeTruthy();
+ });
+
+ it('does not expose the selector while a profile is recovering or unavailable', async () => {
+ mockConnection = { kind: 'connected', desktop, profile: null, profileStatus: 'recovering' };
+ const view = await render();
+ expect(view.queryByTestId('screen-sticky-accessory')).toBeNull();
+ expect(view.queryByRole('button', { name: 'Surface' })).toBeNull();
+
+ mockConnection = { kind: 'connected', desktop, profile: null, profileStatus: 'unavailable' };
+ await view.rerender();
+ expect(view.queryByTestId('screen-sticky-accessory')).toBeNull();
+ expect(view.queryByRole('button', { name: 'Surface' })).toBeNull();
+ });
+
+ it('pins the selector in disconnected states', async () => {
+ mockConnection = { kind: 'idle', saved: [] };
+ const view = await render();
+ expect(view.getByTestId('screen-sticky-accessory')).toBeTruthy();
+ expect(view.getByRole('button', { name: 'Surface' })).toBeTruthy();
+ });
+});
diff --git a/src/theme/ThemeContext.test.tsx b/src/theme/ThemeContext.test.tsx
index bd25599..f165bc3 100644
--- a/src/theme/ThemeContext.test.tsx
+++ b/src/theme/ThemeContext.test.tsx
@@ -1,5 +1,5 @@
-import { render } from '@testing-library/react-native';
-import { StyleSheet, Text, useColorScheme, useWindowDimensions, View } from 'react-native';
+import { act, render } from '@testing-library/react-native';
+import { AccessibilityInfo, StyleSheet, Text, useColorScheme, useWindowDimensions, View } from 'react-native';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Screen } from '@/components/Screen';
import { ThemeProvider, useTheme } from './ThemeContext';
@@ -14,11 +14,17 @@ function SchemeProbe() {
return {useTheme().scheme};
}
+function TransparencyProbe() {
+ return {useTheme().reducedTransparency ? 'opaque' : 'blurred'};
+}
+
describe('ThemeProvider', () => {
beforeEach(() => {
mockWindowDimensions.mockReturnValue({ width: 390, height: 844, scale: 3, fontScale: 1 });
});
+ afterEach(() => jest.restoreAllMocks());
+
it('updates consumers when the device color scheme changes', async () => {
mockColorScheme.mockReturnValue('light');
const view = await render();
@@ -28,6 +34,27 @@ describe('ThemeProvider', () => {
expect(view.getByText('dark')).toBeTruthy();
});
+ it('tracks Reduce Transparency and removes its listener on unmount', async () => {
+ let transparencyListener: ((enabled: boolean) => void) | undefined;
+ const removeTransparencyListener = jest.fn();
+ jest.spyOn(AccessibilityInfo, 'isReduceTransparencyEnabled').mockResolvedValue(false);
+ const addEventListener = jest.spyOn(AccessibilityInfo, 'addEventListener') as unknown as jest.Mock;
+ addEventListener.mockImplementation((event: string, listener: (enabled: boolean) => void) => {
+ if (event === 'reduceTransparencyChanged') {
+ transparencyListener = listener;
+ return { remove: removeTransparencyListener };
+ }
+ return { remove: jest.fn() };
+ });
+
+ const view = await render();
+ expect(view.getByText('blurred')).toBeTruthy();
+ await act(async () => transparencyListener?.(true));
+ await view.findByText('opaque');
+ await view.unmount();
+ expect(removeTransparencyListener).toHaveBeenCalledTimes(1);
+ });
+
it('changes screen width for tablet layouts and omits a duplicate native-route heading', async () => {
mockColorScheme.mockReturnValue('dark');
const view = await render(Body);
diff --git a/src/theme/ThemeContext.tsx b/src/theme/ThemeContext.tsx
index 7117963..e77b09f 100644
--- a/src/theme/ThemeContext.tsx
+++ b/src/theme/ThemeContext.tsx
@@ -7,23 +7,30 @@ type ThemeValue = {
colors: typeof palettes.dark | typeof palettes.light;
radii: typeof radii;
reducedMotion: boolean;
+ reducedTransparency: boolean;
scheme: 'dark' | 'light';
spacing: typeof spacing;
typography: typeof typography;
};
-const defaultTheme: ThemeValue = { colors: palettes.dark, radii, reducedMotion: false, scheme: 'dark', spacing, typography };
+const defaultTheme: ThemeValue = { colors: palettes.dark, radii, reducedMotion: false, reducedTransparency: false, scheme: 'dark', spacing, typography };
const ThemeContext = createContext(defaultTheme);
export function ThemeProvider({ children }: PropsWithChildren) {
const scheme: 'light' | 'dark' = useColorScheme() === 'light' ? 'light' : 'dark';
const [reducedMotion, setReducedMotion] = useState(false);
+ const [reducedTransparency, setReducedTransparency] = useState(false);
useEffect(() => {
void AccessibilityInfo.isReduceMotionEnabled().then(setReducedMotion);
- const subscription = AccessibilityInfo.addEventListener('reduceMotionChanged', setReducedMotion);
- return () => subscription.remove();
+ void AccessibilityInfo.isReduceTransparencyEnabled().then(setReducedTransparency);
+ const motionSubscription = AccessibilityInfo.addEventListener('reduceMotionChanged', setReducedMotion);
+ const transparencySubscription = AccessibilityInfo.addEventListener('reduceTransparencyChanged', setReducedTransparency);
+ return () => {
+ motionSubscription.remove();
+ transparencySubscription.remove();
+ };
}, []);
- const value = useMemo(() => ({ colors: palettes[scheme], radii, reducedMotion, scheme, spacing, typography }), [reducedMotion, scheme]);
+ const value = useMemo(() => ({ colors: palettes[scheme], radii, reducedMotion, reducedTransparency, scheme, spacing, typography }), [reducedMotion, reducedTransparency, scheme]);
return {children};
}