diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index cf1cb43f4..f01d29934 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -72,7 +72,7 @@ The current distribution mechanism is local tarballs. `scripts/pack.ts` writes c `~/.spark` is the default global registry; `SPARK_HOME` overrides it. SDK records are versioned. Runtime registration records a path rather than copying an application. The managed prebuilt build project is created under the spark home unless a project is supplied explicitly. App-local `.spark` data and the global spark home are different scopes. -The template manifests and `scripts/prepare-runtimes.ts` are the authoritative pins. We remain on Expo Desktop `1.0.0` and native template `54.81.1`, with Expo 54.0.37, React Native 0.81.6, React Native macOS 0.81.7, and React Native Windows 0.81.35. Creation uses a subprocess-scoped npm 11 executable because Expo Desktop misreads npm 12's local-tarball metadata. The selected package manager installs dependencies. This compatibility adapter does not patch upstream or change the host npm installation. +The template manifests and `scripts/prepare-runtimes.ts` are the authoritative pins. We use Expo Desktop `1.0.6` and native template `55.83.10`, with Expo 55.0.32, React Native 0.83.10, React Native macOS 0.83.0, and React Native Windows 0.83.2. Creation uses a subprocess-scoped npm 11 executable because Expo Desktop misreads npm 12's local-tarball metadata. The selected package manager installs dependencies. This compatibility adapter does not patch upstream or change the host npm installation. Windows uses its own complete application template, with no runtime removal/replacement of macOS source files. SDK packing prepares the pinned Runtimes archive and Windows Nitro, OP-SQLite, and WebView adapters on either host OS. Patch application uses JavaScript; consumers receive ordinary dependency archives. @@ -233,7 +233,7 @@ For a missing runtime, inspect SDK registration and binary metadata. For an inco Windows support is part of the existing framework. `spark create --platform windows`, `spark sdk build-runner --platform windows`, `spark dev`, and `spark build --dev` share project discovery, runtime metadata, registry, compatibility policy, Metro gating, terminal actions, and build records with macOS. There is no second session implementation or external source kit. See the [Windows guide](docs/windows-slice.md) for commands and prerequisites. -The development targets are Windows 11 x64 and ARM64, RNW 0.81.35, New Architecture/Hermes, and the pinned Expo Desktop template. That RNW template uses MSVC v145 / Visual Studio 2026. The minimal starter provides the native host; the SDK prebuilt profile adds the desktop SDK and external libraries. Universal projects preserve configuration and generated projects across platform switches. Windows architecture defaults to the OS CPU (including ARM64 when the CLI is emulated) and can be overridden with `SPARK_WINDOWS_ARCH=x64|arm64`. It participates in the native fingerprint and product path. Build records retain the latest build per mode; registered binaries for the other architecture remain intact. Expo Desktop beta owns generation and RNW receives the corresponding `--arch x64` or `--arch ARM64`. +The development targets are Windows 11 x64 and ARM64, RNW 0.83.2, New Architecture/Hermes, and the pinned Expo Desktop template. That RNW template uses MSVC v145 / Visual Studio 2026. The minimal starter provides the native host; the SDK prebuilt profile adds the desktop SDK and external libraries. Universal projects preserve configuration and generated projects across platform switches. Windows architecture defaults to the OS CPU (including ARM64 when the CLI is emulated) and can be overridden with `SPARK_WINDOWS_ARCH=x64|arm64`. It participates in the native fingerprint and product path. Build records retain the latest build per mode; registered binaries for the other architecture remain intact. Expo Desktop beta owns generation and RNW receives the corresponding `--arch x64` or `--arch ARM64`. Windows signatures include Windows native sources/project files and host/config integration; generated build outputs and NuGet lockfiles do not invalidate the source signature. Framework/runtime version changes participate in the mandatory host signature. Directly installed native packages without a Windows implementation fail clearly. This remains a constrained development graph, not acceptance of every third-party dependency arrangement or custom native project modification. diff --git a/README.md b/README.md index 220b7f570..065432687 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ Packaging requires signing credentials and a notarization profile. It signs a st Windows uses the same CLI, starter, prebuilt registry, native compatibility checks, and managed Metro session. The Windows starter includes the native host; the SDK prebuilt profile includes the desktop modules and external integrations. Their Windows source implementations are ready for native acceptance. There is no separate source kit to install. -On Windows 11 x64 or ARM64 (including Parallels), install the prerequisites in the [Windows guide](docs/windows-slice.md), including Visual Studio 2026 / MSVC v145 for the pinned RNW 0.81.35 template. Then run from the framework checkout: +On Windows 11 x64 or ARM64 (including Parallels), install the prerequisites in the [Windows guide](docs/windows-slice.md), including Visual Studio 2026 / MSVC v145 for the pinned RNW 0.83.2 template. Then run from the framework checkout: ```powershell bun install @@ -316,7 +316,7 @@ macOS 14+ (Apple Silicon) supported; Windows preview, not yet verified This is a prerelease; APIs and tooling may change. Intel macOS has architecture selection tooling but no verified native build/runtime support; see [Intel macOS](docs/macos-intel.md). Windows currently covers development builds, with details in the [Windows guide](docs/windows-slice.md). Linux and Mac App Store distribution are unsupported. Mobile/web development delegates to Expo. -The checkout uses Expo SDK 54, React Native 0.81, and Expo Desktop 1.0.0. See the [integration boundary](docs/expo-desktop-integration.md) for upstream requirements. +The checkout uses Expo SDK 55, React Native 0.83, and Expo Desktop 1.0.6. See the [integration boundary](docs/expo-desktop-integration.md) for upstream requirements. Use the [Mac and Windows manual acceptance checklist](docs/desktop-manual-acceptance.md) to validate a checkout on real machines, including runtime rebuilds and OS interactions. diff --git a/bun.lock b/bun.lock index 8664d3a35..6a38852eb 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "spark-workspace", "devDependencies": { - "@expo/ui": "0.2.0-beta.9", + "@expo/ui": "55.0.17", "@legendapp/list": "3.4.0", "@legendapp/spark": "0.0.1", "@legendapp/spark-audio": "0.0.1", @@ -39,32 +39,32 @@ "@react-native-async-storage/async-storage": "2.2.0", "@react-native-runtimes/core": "0.1.0-alpha.2", "@types/node": "24.13.6", - "@types/react": "19.1.10", + "@types/react": "19.2.18", "@types/ws": "8.18.1", "ajv": "8.20.0", "base64-js": "1.5.1", "diff": "8.0.4", - "expo": "54.0.37", - "expo-audio": "1.1.1", - "expo-clipboard": "8.0.8", - "expo-crypto": "15.0.8", - "expo-document-picker": "14.0.8", - "expo-file-system": "19.0.24", - "expo-linking": "8.0.12", - "expo-secure-store": "15.0.8", - "expo-sharing": "14.0.8", - "expo-web-browser": "15.0.10", + "expo": "55.0.32", + "expo-audio": "55.0.18", + "expo-clipboard": "55.0.17", + "expo-crypto": "55.0.19", + "expo-document-picker": "55.0.17", + "expo-file-system": "55.0.26", + "expo-linking": "55.0.17", + "expo-secure-store": "55.0.18", + "expo-sharing": "55.0.25", + "expo-web-browser": "55.0.20", "pngjs": "3.4.0", - "react": "19.1.4", - "react-dom": "19.1.4", - "react-native": "0.81.6", + "react": "19.2.0", + "react-dom": "19.2.0", + "react-native": "0.83.10", "react-native-enriched-html": "1.1.1", "react-native-enriched-markdown": "1.1.1", "react-native-nitro-image": "0.15.2", "react-native-nitro-modules": "0.37.0", "react-native-vision-camera": "5.2.3", "react-native-webview": "16.0.0", - "react-test-renderer": "19.1.4", + "react-test-renderer": "19.2.0", "tailwindcss": "4.2.4", "typescript": "5.9.3", "uniwind": "1.6.3", @@ -78,36 +78,97 @@ "version": "0.1.0", "dependencies": { "@babel/parser": "7.28.5", + "@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6", + "@babel/plugin-proposal-optional-chaining": "7.21.0", + "@babel/plugin-transform-arrow-functions": "7.27.1", + "@babel/plugin-transform-shorthand-properties": "7.27.1", + "@babel/plugin-transform-template-literals": "7.29.7", "@babel/traverse": "7.28.5", - "@expo/ui": "0.2.0-beta.9", + "@expo/ui": "55.0.17", + "@expo/vector-icons": "15.1.1", + "@gorhom/bottom-sheet": "5.2.14", "@legendapp/spark": "0.0.1", "@op-engineering/op-sqlite": "18.2.1", "@react-native-async-storage/async-storage": "2.2.0", + "@react-native-community/blur": "4.4.1", "@react-native-community/cli": "20.1.3", "@react-native-community/cli-platform-apple": "20.1.3", + "@react-native-community/datetimepicker": "9.2.2", + "@react-native-community/netinfo": "11.5.2", + "@react-native-masked-view/masked-view": "0.3.2", "@react-native-runtimes/core": "0.1.0-alpha.2", - "@react-native/metro-config": "0.81.6", - "@types/react": "19.1.10", + "@react-native/metro-config": "0.83.10", + "@react-navigation/bottom-tabs": "7.21.0", + "@react-navigation/native": "7.5.1", + "@react-navigation/native-stack": "7.21.0", + "@sentry/react-native": "8.32.0", + "@shopify/react-native-skia": "2.14.0", + "@types/react": "19.2.18", "base64-js": "1.5.1", - "expo": "54.0.37", - "expo-clipboard": "8.0.8", + "expo": "55.0.32", + "expo-application": "55.0.19", + "expo-blur": "55.0.18", + "expo-clipboard": "55.0.17", + "expo-crypto": "55.0.19", "expo-desktop": "1.0.6", "expo-desktop-config-plugins": "1.3.2", - "expo-desktop-metro-config": "54.81.3", - "expo-desktop-modules-core": "54.0.14", + "expo-desktop-metro-config": "55.83.3", + "expo-desktop-modules-core": "55.0.2", "expo-desktop-prebuild-config": "1.1.3", - "expo-desktop-stubs": "54.0.15", - "expo-desktop-template-bare-minimum": "54.81.6", - "expo-linking": "8.0.12", - "expo-secure-store": "15.0.8", - "react": "19.1.4", - "react-native": "0.81.6", + "expo-desktop-stubs": "55.0.2", + "expo-desktop-template-bare-minimum": "55.83.10", + "expo-device": "55.0.21", + "expo-font": "55.0.8", + "expo-glass-effect": "55.0.11", + "expo-haptics": "55.0.18", + "expo-image": "55.0.11", + "expo-image-manipulator": "55.0.21", + "expo-image-picker": "55.0.24", + "expo-keep-awake": "55.0.8", + "expo-linear-gradient": "55.0.18", + "expo-linking": "55.0.17", + "expo-localization": "55.0.19", + "expo-location": "55.1.15", + "expo-router": "55.0.18", + "expo-secure-store": "55.0.18", + "expo-splash-screen": "55.0.25", + "expo-status-bar": "55.0.6", + "expo-symbols": "55.0.9", + "expo-system-ui": "55.0.22", + "expo-web-browser": "55.0.20", + "react": "19.2.0", + "react-native": "0.83.10", + "react-native-app-logs": "0.3.1", + "react-native-blurhash": "2.1.3", + "react-native-drawer-layout": "4.2.12", "react-native-enriched-html": "1.1.1", "react-native-enriched-markdown": "1.1.1", - "react-native-macos": "0.81.7", + "react-native-fast-image": "10.9.9", + "react-native-gesture-handler": "2.30.0", + "react-native-graph": "1.4.0", + "react-native-keyboard-controller": "1.22.9", + "react-native-macos": "0.83.0", + "react-native-mmkv": "4.3.2", + "react-native-nitro-fetch": "1.9.0", + "react-native-nitro-image": "0.15.2", "react-native-nitro-modules": "0.37.0", + "react-native-nitro-sqlite": "10.2.0", + "react-native-nitro-zxing": "0.3.0", + "react-native-pressable-opacity": "1.0.10", + "react-native-quick-base64": "3.0.1", + "react-native-quick-crypto": "1.1.9", + "react-native-reanimated": "4.2.1", + "react-native-release-profiler": "0.4.5", + "react-native-safe-area-context": "5.6.2", + "react-native-screen-corner-radius": "0.2.3", + "react-native-screens": "4.23.0", + "react-native-svg": "15.15.3", + "react-native-tracking-transparency": "0.1.2", + "react-native-unistyles": "3.6.0", "react-native-webview": "16.0.0", - "react-native-windows": "0.81.35", + "react-native-windows": "0.83.2", + "react-native-worklets": "0.7.4", + "react-native-worklets-core": "1.6.3", "tailwindcss": "4.2.4", "typescript": "5.9.3", "uniwind": "1.6.3", @@ -117,9 +178,9 @@ "name": "@legendapp/spark-native-greeting", "version": "0.0.1", "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", - "react-native-windows": "0.81.35", + "react": "^19.2.0", + "react-native": "0.83.10", + "react-native-windows": "0.83.2", }, "optionalPeers": [ "react-native-windows", @@ -132,8 +193,8 @@ "@legendapp/spark": "0.0.1", }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", }, }, "packages/ai": { @@ -144,8 +205,8 @@ "@legendapp/spark-processes": "0.0.1", }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", }, }, "packages/audio": { @@ -155,9 +216,9 @@ "@legendapp/spark-desktop-app": "0.0.1", }, "peerDependencies": { - "expo-audio": "1.1.1", - "react": "^19.1.0", - "react-native": "0.81.6", + "expo-audio": "55.0.18", + "react": "^19.2.0", + "react-native": "0.83.10", }, "optionalPeers": [ "expo-audio", @@ -172,9 +233,9 @@ "base64-js": "1.5.1", }, "peerDependencies": { - "expo-crypto": "~15.0.8", - "expo-web-browser": "~15.0.10", - "react-native": "0.81.6", + "expo-crypto": "~55.0.19", + "expo-web-browser": "~55.0.20", + "react-native": "0.83.10", }, "optionalPeers": [ "expo-crypto", @@ -203,9 +264,9 @@ "@legendapp/spark-desktop-app": "0.0.1", }, "peerDependencies": { - "expo-clipboard": "~8.0.8", - "react": "^19.1.0", - "react-native": "0.81.6", + "expo-clipboard": "~55.0.17", + "react": "^19.2.0", + "react-native": "0.83.10", }, "optionalPeers": [ "expo-clipboard", @@ -219,8 +280,8 @@ "react-native-nitro-modules": "0.37.0", }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", }, }, "packages/commands": { @@ -234,22 +295,22 @@ "@legendapp/state": "^3.0.0-beta.42", }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", }, }, "packages/config-plugin": { "name": "@legendapp/spark-desktop-config", "version": "0.0.1", "dependencies": { - "@expo/config-plugins": "54.0.5", + "@expo/config-plugins": "55.0.12", "@legendapp/spark-desktop-host": "0.0.1", "@legendapp/spark-window-options": "0.0.1", "diff": "8.0.4", "expo-desktop-config-plugins": "1.3.2", }, "devDependencies": { - "@expo/config-types": "54.0.10", + "@expo/config-types": "55.0.6", }, }, "packages/context-menu": { @@ -259,8 +320,8 @@ "@legendapp/spark-desktop-app": "0.0.1", }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", }, }, "packages/create-spark": { @@ -319,13 +380,14 @@ "@legendapp/spark-webview": "0.0.1", "@legendapp/spark-window-options": "0.0.1", "@react-native-runtimes/core": "0.1.0-alpha.2", + "@react-native-windows/find-dotnet-tools": "0.0.0-canary.3", "react-native-nitro-modules": "0.37.0", }, "peerDependencies": { - "expo": "54.0.37", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "react-native": "0.81.6", + "expo": "55.0.32", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-native": "0.83.10", "react-native-enriched-html": "1.1.1", "react-native-enriched-markdown": "1.1.1", }, @@ -339,8 +401,8 @@ "name": "@legendapp/spark-desktop-app", "version": "0.0.1", "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", }, }, "packages/desktop-host": { @@ -357,9 +419,9 @@ "@legendapp/spark-desktop-app": "0.0.1", }, "peerDependencies": { - "expo-linking": "~8.0.12", - "react": "^19.1.0", - "react-native": "0.81.6", + "expo-linking": "~55.0.17", + "react": "^19.2.0", + "react-native": "0.83.10", }, "optionalPeers": [ "expo-linking", @@ -372,8 +434,8 @@ "@legendapp/spark-desktop-app": "0.0.1", }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", }, }, "packages/desktop-windows": { @@ -385,8 +447,8 @@ "@legendapp/state": "^3.0.0-beta.42", }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", }, }, "packages/documents": { @@ -401,8 +463,8 @@ "@legendapp/spark-native-menu": "0.0.1", }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", }, }, "packages/drag-drop": { @@ -412,8 +474,8 @@ "@legendapp/spark-desktop-app": "0.0.1", }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", }, }, "packages/file-dialog": { @@ -423,8 +485,8 @@ "@legendapp/spark-desktop-app": "0.0.1", }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", }, }, "packages/file-system": { @@ -434,8 +496,8 @@ "@legendapp/spark-desktop-app": "0.0.1", }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", }, }, "packages/global-shortcuts": { @@ -445,8 +507,8 @@ "@legendapp/spark-desktop-app": "0.0.1", }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", }, }, "packages/html": { @@ -456,8 +518,8 @@ "@legendapp/spark-rich-text": "0.0.1", }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", "react-native-enriched-html": "1.1.1", }, "optionalPeers": [ @@ -471,8 +533,8 @@ "@legendapp/spark-rich-text": "0.0.1", }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", "react-native-enriched-markdown": "1.1.1", }, "optionalPeers": [ @@ -486,8 +548,8 @@ "@legendapp/spark-desktop-app": "0.0.1", }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", }, }, "packages/native-menu": { @@ -497,8 +559,8 @@ "@legendapp/spark-desktop-app": "0.0.1", }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", }, }, "packages/notifications": { @@ -508,8 +570,8 @@ "@legendapp/spark-desktop-app": "0.0.1", }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", }, }, "packages/processes": { @@ -519,8 +581,8 @@ "@legendapp/spark-desktop-app": "0.0.1", }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", }, }, "packages/rich-text": { @@ -530,8 +592,8 @@ "@legendapp/spark-desktop-shortcuts": "0.0.1", }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", }, }, "packages/secure-storage": { @@ -541,9 +603,9 @@ "@legendapp/spark-desktop-app": "0.0.1", }, "peerDependencies": { - "expo-secure-store": "~15.0.8", - "react": "^19.1.0", - "react-native": "0.81.6", + "expo-secure-store": "~55.0.18", + "react": "^19.2.0", + "react-native": "0.83.10", }, "optionalPeers": [ "expo-secure-store", @@ -568,8 +630,8 @@ }, "peerDependencies": { "@legendapp/list": ">=3.3.0", - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", }, }, "packages/sqlite": { @@ -593,8 +655,8 @@ "@legendapp/spark-desktop-app": "0.0.1", }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", }, }, "packages/tray": { @@ -604,8 +666,8 @@ "@legendapp/spark-desktop-app": "0.0.1", }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", }, }, "packages/ui": { @@ -617,10 +679,10 @@ "tailwind-merge": "^3.4.0", }, "peerDependencies": { - "@expo/ui": "0.2.0-beta.9", - "react": "^19.1.0", - "react-native": "0.81.6", - "react-native-windows": "0.81.35", + "@expo/ui": "55.0.17", + "react": "^19.2.0", + "react-native": "0.83.10", + "react-native-windows": "0.83.2", "uniwind": "^1.6.3", }, "optionalPeers": [ @@ -636,8 +698,8 @@ "@legendapp/spark-desktop-app": "0.0.1", }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", }, }, "packages/webview": { @@ -659,7 +721,7 @@ }, }, "overrides": { - "@expo/cli": "54.0.27", + "@expo/cli": "55.0.37", "@op-engineering/op-sqlite": "18.2.1", "@tiptap/core": "3.32.0", "@tiptap/extension-blockquote": "3.32.0", @@ -683,28 +745,26 @@ "@tiptap/pm": "3.32.0", "@tiptap/react": "3.32.0", "dompurify": "3.4.17", - "expo": "54.0.37", - "expo-asset": "12.0.13", - "expo-constants": "18.0.14", - "expo-desktop-modules-core": "54.0.14", - "expo-desktop-stubs": "54.0.15", - "expo-file-system": "19.0.24", - "expo-font": "14.0.12", - "expo-keep-awake": "15.0.8", - "expo-modules-core": "3.0.30", + "expo": "55.0.32", + "expo-asset": "55.0.20", + "expo-constants": "55.0.17", + "expo-desktop-modules-core": "55.0.2", + "expo-desktop-stubs": "55.0.2", + "expo-file-system": "55.0.26", + "expo-font": "55.0.8", + "expo-keep-awake": "55.0.8", + "expo-modules-core": "55.0.27", "fast-xml-parser": "5.11.2", "postcss": "8.5.31", - "react": "19.1.4", - "react-dom": "19.1.4", - "react-native": "0.81.6", - "react-native-macos": "0.81.7", + "react": "19.2.0", + "react-dom": "19.2.0", + "react-native": "0.83.10", + "react-native-macos": "0.83.0", "react-native-nitro-modules": "0.37.0", "react-native-webview": "16.0.0", - "react-native-windows": "0.81.35", + "react-native-windows": "0.83.2", }, "packages": { - "@0no-co/graphql.web": ["@0no-co/graphql.web@1.3.4", "", { "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" }, "optionalPeers": ["graphql"] }, "sha512-imSwulOeDQodRy/olQmVEo2PiY6ntjkZ9eiGdw6lMYylh/tay9b7MusyJBmEnkL8GiKRKr6ltr+D42mY5bd8Bg=="], - "@ark/schema": ["@ark/schema@0.56.2", "", { "dependencies": { "@ark/util": "0.56.2" } }, "sha512-Qx4D2JFbBWpntiHZaTv7bGG4H/M2rigiknezKg/WVyDSaLdE4YCcWAOoFB7pjjDqHbbV2OqRfntm1nnXvwMexg=="], "@ark/util": ["@ark/util@0.56.2", "", {}, "sha512-9kU2sUE38FZEGG7l3hamYMBieLYEJh2L1mrYD2eXpT+78EnQSV1bhjxJhnxGBMSTbtwpBSDNSK+K60WvaI/DTQ=="], @@ -763,6 +823,10 @@ "@babel/plugin-proposal-export-default-from": ["@babel/plugin-proposal-export-default-from@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-p+G5BNXDcy3bOXplhY4HybQ1GxH3i2Tppmdm/3epyRu2VgJJZuUlZ61MqRTg582Q7ZLBdP7fePYvsumSEkMxcQ=="], + "@babel/plugin-proposal-nullish-coalescing-operator": ["@babel/plugin-proposal-nullish-coalescing-operator@7.18.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA=="], + + "@babel/plugin-proposal-optional-chaining": ["@babel/plugin-proposal-optional-chaining@7.21.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.20.2", "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA=="], + "@babel/plugin-syntax-async-generators": ["@babel/plugin-syntax-async-generators@7.8.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw=="], "@babel/plugin-syntax-bigint": ["@babel/plugin-syntax-bigint@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg=="], @@ -805,7 +869,7 @@ "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA=="], - "@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ=="], + "@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA=="], "@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-remap-async-to-generator": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA=="], @@ -813,11 +877,11 @@ "@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ=="], - "@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA=="], + "@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA=="], "@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A=="], - "@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g=="], + "@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.28.4", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA=="], "@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/template": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA=="], @@ -839,7 +903,7 @@ "@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ=="], - "@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg=="], + "@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA=="], "@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw=="], @@ -847,7 +911,7 @@ "@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng=="], - "@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ=="], + "@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg=="], "@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g=="], @@ -871,19 +935,21 @@ "@babel/plugin-transform-runtime": ["@babel/plugin-transform-runtime@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xmAscdE/AsqRW7vutbPNoUmu/nF5SrLKPs7aoJgEjo35lLKA/Bc0i2rMv/hr1+Y0o1bQCiVtith3u2vdgRL39Q=="], - "@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg=="], + "@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ=="], "@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.29.8", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-4S9ksMGVWUshvgK0mKfvZky7leuG5/uoFVwMpAomJ8bMoDJiNHRVmc1EglwW/CmGVSqqWpEbXm9FmbRit22qoA=="], "@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA=="], + "@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA=="], + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.29.9", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-FFwIwzU+7SCOuxxV4YtJql6T9981ZVTm+FHO5GhVsRqCTdy0WwrEZh3l42ARpXruJUDGOptdduHW6Zr7pNPLNg=="], - "@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA=="], + "@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw=="], "@babel/preset-react": ["@babel/preset-react@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "@babel/plugin-transform-react-display-name": "^7.29.7", "@babel/plugin-transform-react-jsx": "^7.29.7", "@babel/plugin-transform-react-jsx-development": "^7.29.7", "@babel/plugin-transform-react-pure-annotations": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-C+PV1TFUPTmBQGoPBL8j2QmLpZ117YTCwxIZeJOM96GbYMFSc7/pOXU5lVykwnZxyTqQxRsvoRk6f2FktZgGHA=="], - "@babel/preset-typescript": ["@babel/preset-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "@babel/plugin-syntax-jsx": "^7.29.7", "@babel/plugin-transform-modules-commonjs": "^7.29.7", "@babel/plugin-transform-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ=="], + "@babel/preset-typescript": ["@babel/preset-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="], "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], @@ -891,51 +957,65 @@ "@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], - "@babel/traverse--for-generate-function-map": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="], - "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], "@clack/core": ["@clack/core@1.5.1", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ=="], "@clack/prompts": ["@clack/prompts@1.8.1", "", { "dependencies": { "@clack/core": "1.5.1", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ=="], - "@expo/cli": ["@expo/cli@54.0.27", "", { "dependencies": { "@0no-co/graphql.web": "^1.0.8", "@expo/code-signing-certificates": "^0.0.6", "@expo/config": "~12.0.14", "@expo/config-plugins": "~54.0.5", "@expo/devcert": "^1.2.1", "@expo/env": "~2.0.12", "@expo/image-utils": "^0.8.8", "@expo/json-file": "^10.0.16", "@expo/metro": "~54.2.0", "@expo/metro-config": "~54.0.17", "@expo/osascript": "^2.3.8", "@expo/package-manager": "^1.9.11", "@expo/plist": "^0.4.9", "@expo/prebuild-config": "^54.0.9", "@expo/schema-utils": "^0.1.9", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.3.0", "@react-native/dev-middleware": "0.81.5", "@urql/core": "^5.0.6", "@urql/exchange-retry": "^1.3.0", "accepts": "^1.3.8", "agent-cli-detector": "^0.1.2", "arg": "^5.0.2", "better-opn": "~3.0.2", "bplist-creator": "0.1.0", "bplist-parser": "^0.3.1", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "env-editor": "^0.4.1", "expo-server": "^1.0.7", "freeport-async": "^2.0.0", "getenv": "^2.0.0", "glob": "^13.0.0", "lan-network": "^0.2.1", "minimatch": "^9.0.0", "node-forge": "^1.3.3", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^4.0.3", "pretty-bytes": "^5.6.0", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "qrcode-terminal": "0.11.0", "require-from-string": "^2.0.2", "requireg": "^0.2.2", "resolve": "^1.22.2", "resolve-from": "^5.0.0", "resolve.exports": "^2.0.3", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "tar": "^7.5.2", "terminal-link": "^2.1.1", "undici": "^6.18.2", "wrap-ansi": "^7.0.0", "ws": "^8.12.1" }, "peerDependencies": { "expo": "*", "expo-router": "*", "react-native": "*" }, "optionalPeers": ["expo-router"], "bin": { "expo-internal": "build/bin/cli" } }, "sha512-BxKOr6e8wmUwhGSyEVKY+CHWgS3Cre9ibgx1QWcdCenKTVKAz62qGmYL51q0lCcKg6vQY+4Usw9RK5Il4ZFaIA=="], + "@craftzdog/react-native-buffer": ["@craftzdog/react-native-buffer@6.1.2", "", { "dependencies": { "ieee754": "^1.2.1", "react-native-quick-base64": "^3.0.0" } }, "sha512-KV1HitN05FHLLDG7Zb/yftDsa+mKBYBzFMQ0PMldvUicq6vWOtAvz9mDavt7Fzozh+WNqORE+yFDkkdWysZ/SA=="], + + "@egjs/hammerjs": ["@egjs/hammerjs@2.0.17", "", { "dependencies": { "@types/hammerjs": "^2.0.36" } }, "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A=="], + + "@expo-google-fonts/material-symbols": ["@expo-google-fonts/material-symbols@0.4.52", "", {}, "sha512-iC9GRpj7WV0GENbHTYPtJqGjyy/7yI3soG9SrKTY1ta19cbMZm2HA0nUdIK8d6OD9uv1BZHAh3Zn/UWV57IeWg=="], + + "@expo/cli": ["@expo/cli@55.0.37", "", { "dependencies": { "@expo/code-signing-certificates": "^0.0.6", "@expo/config": "~55.0.21", "@expo/config-plugins": "~55.0.12", "@expo/devcert": "^1.2.1", "@expo/env": "~2.1.3", "@expo/image-utils": "^0.8.17", "@expo/json-file": "^10.0.15", "@expo/log-box": "55.0.13", "@expo/metro": "~55.1.2", "@expo/metro-config": "~55.0.27", "@expo/osascript": "^2.4.4", "@expo/package-manager": "^1.10.6", "@expo/plist": "^0.5.4", "@expo/prebuild-config": "^55.0.22", "@expo/require-utils": "^55.0.8", "@expo/router-server": "^55.0.19", "@expo/schema-utils": "^55.0.5", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.4.0", "@react-native/dev-middleware": "0.83.10", "accepts": "^1.3.8", "agent-cli-detector": "^0.1.2", "arg": "^5.0.2", "better-opn": "~3.0.2", "bplist-creator": "0.1.0", "bplist-parser": "^0.3.1", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "dnssd-advertise": "^1.1.6", "expo-server": "^55.0.12", "fetch-nodeshim": "^0.4.10", "getenv": "^2.0.0", "glob": "^13.0.0", "lan-network": "^0.2.1", "multitars": "^1.0.2", "node-forge": "^1.3.3", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^4.0.3", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "resolve-from": "^5.0.0", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "terminal-link": "^2.1.1", "toqr": "^0.1.1", "wrap-ansi": "^7.0.0", "ws": "^8.12.1", "zod": "^3.25.76" }, "peerDependencies": { "expo": "*", "expo-router": "*", "react-native": "*" }, "optionalPeers": ["expo-router", "react-native"], "bin": { "expo-internal": "build/bin/cli" } }, "sha512-ennGX6iY1Ba/88cKWomD7xCIPEY0FDU14zAHyFpQTQV+9vg32i45sCRzOWUqkDXuZq4QzzznNqq87eFnlKfh/A=="], "@expo/code-signing-certificates": ["@expo/code-signing-certificates@0.0.6", "", { "dependencies": { "node-forge": "^1.3.3" } }, "sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w=="], - "@expo/config": ["@expo/config@12.0.14", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "@expo/config-plugins": "~54.0.5", "@expo/config-types": "^54.0.10", "@expo/json-file": "^10.0.16", "deepmerge": "^4.3.1", "getenv": "^2.0.0", "glob": "^13.0.0", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0", "resolve-workspace-root": "^2.0.0", "semver": "^7.6.0", "slugify": "^1.3.4", "sucrase": "~3.35.1" } }, "sha512-3dfbBd9LnPDgyylhCgkOsaG8Adg52uOVOTQYH5lf23a/t8M5eQpXKCvzUarrf62B78057n2NnkiofK9TfPgvzw=="], + "@expo/config": ["@expo/config@55.0.21", "", { "dependencies": { "@expo/config-plugins": "~55.0.11", "@expo/config-types": "^55.0.6", "@expo/json-file": "^10.0.15", "@expo/require-utils": "^55.0.8", "deepmerge": "^4.3.1", "getenv": "^2.0.0", "glob": "^13.0.0", "resolve-workspace-root": "^2.0.0", "semver": "^7.6.0", "slugify": "^1.3.4" } }, "sha512-yHmSSnBrm2ecB1DNsCrZZrScVnvXrtg3+PAMwI3ocxEtD9YK8yX5xV2xi2pmor9TYitAO+udFWxo+XDEGibznw=="], - "@expo/config-plugins": ["@expo/config-plugins@54.0.5", "", { "dependencies": { "@expo/config-types": "^54.0.10", "@expo/json-file": "~10.0.16", "@expo/plist": "^0.4.9", "@expo/sdk-runtime-versions": "^1.0.0", "chalk": "^4.1.2", "debug": "^4.3.5", "getenv": "^2.0.0", "glob": "^13.0.0", "resolve-from": "^5.0.0", "semver": "^7.5.4", "slash": "^3.0.0", "slugify": "^1.6.6", "xcode": "^3.0.1", "xml2js": "0.6.0" } }, "sha512-aWQ3sViNRoQWw6So4A2qhWCt24CuGBK6MrRHI1AG+V6/NQAjIZCHaSvcXK2gXKpmisRhTSUWaKPIgLJQFB+AeQ=="], + "@expo/config-plugins": ["@expo/config-plugins@55.0.12", "", { "dependencies": { "@expo/config-types": "^55.0.6", "@expo/json-file": "~10.0.15", "@expo/plist": "^0.5.4", "@expo/sdk-runtime-versions": "^1.0.0", "chalk": "^4.1.2", "debug": "^4.3.5", "getenv": "^2.0.0", "glob": "^13.0.0", "resolve-from": "^5.0.0", "semver": "^7.5.4", "slugify": "^1.6.6", "xcode": "^4.0.0", "xml2js": "0.6.0" } }, "sha512-Z6KlUKqQDZaGU7jag1AaX2wCyFLvGwH/FTEjjIvLV0wTbrj1I91LhlVKYsI6BhUuzan0DOxvYBs9p88jGY5Lrw=="], - "@expo/config-types": ["@expo/config-types@54.0.10", "", {}, "sha512-/J16SC2an1LdtCZ67xhSkGXpALYUVUNyZws7v+PVsFZxClYehDSoKLqyRaGkpHlYrCc08bS0RF5E0JV6g50psA=="], + "@expo/config-types": ["@expo/config-types@55.0.6", "", {}, "sha512-S+GJKYoIjnWlert/9vXuTohaTsMbyOLSVxdIgPgoq3P4N1p4CWrfyZLnz6qRug8wYSO5fcYcS9mFleyEP8wRLg=="], "@expo/devcert": ["@expo/devcert@1.2.1", "", { "dependencies": { "@expo/sudo-prompt": "^9.3.1", "debug": "^3.1.0" } }, "sha512-qC4eaxmKMTmJC2ahwyui6ud8f3W60Ss7pMkpBq40Hu3zyiAaugPXnZ24145U7K36qO9UHdZUVxsCvIpz2RYYCA=="], - "@expo/devtools": ["@expo/devtools@0.1.8", "", { "dependencies": { "chalk": "^4.1.2" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-SVLxbuanDjJPgc0sy3EfXUMLb/tXzp6XIHkhtPVmTWJAp+FOr6+5SeiCfJrCzZFet0Ifyke2vX3sFcKwEvCXwQ=="], + "@expo/devtools": ["@expo/devtools@55.0.3", "", { "dependencies": { "chalk": "^4.1.2" }, "peerDependencies": { "react": "*", "react-native": "*" }, "optionalPeers": ["react", "react-native"] }, "sha512-KoIDgo0NoXeWLsIcOdZqtAG/1LlsM+JL0DA3bo0vCYaOYTBLXi/ZvRBqa20Ub8D2vKLNa+FgRQW0gRg04Ps1Pg=="], + + "@expo/dom-webview": ["@expo/dom-webview@55.0.6", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-ZNm8tiNEZysxrr36J0x4mOCGyJDcaIvL/3tMxBz0VJIJDcV19xjuJAhJQxHovu+jKx6s9tRyEAINa1mdrzV39g=="], "@expo/env": ["@expo/env@2.4.3", "", { "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", "getenv": "^2.0.0" } }, "sha512-M1NXeZCA1mkMkYOyIe7PlyRX0/jqFtMoJgyblnlq/vpCRfmueFT7RnGSQG8uEFDF5WHOFGijAQ3fogPh3/n5Ng=="], - "@expo/fingerprint": ["@expo/fingerprint@0.15.5", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "arg": "^5.0.2", "chalk": "^4.1.2", "debug": "^4.3.4", "getenv": "^2.0.0", "glob": "^13.0.0", "ignore": "^5.3.1", "minimatch": "^10.2.2", "p-limit": "^3.1.0", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, "bin": { "fingerprint": "bin/cli.js" } }, "sha512-mdVoAMcux1WlM6kd1RoWiHRNqKqS+J6mKmWQ/BKgeh937S/fcW58EE68O6nc4KDXtWi3PBeNHskOFcgyIuD4hw=="], + "@expo/fingerprint": ["@expo/fingerprint@0.16.8", "", { "dependencies": { "@expo/env": "^2.1.3", "@expo/spawn-async": "^1.7.2", "arg": "^5.0.2", "chalk": "^4.1.2", "debug": "^4.3.4", "getenv": "^2.0.0", "glob": "^13.0.0", "ignore": "^5.3.1", "minimatch": "^10.2.2", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, "bin": { "fingerprint": "bin/cli.js" } }, "sha512-RaLOikl+alkvG9ZrBQFGi3kVXJdG5GQXY1H3V1ZFCwFyhFikwuBozVrl4KL7U+N0Tm8Bf1qDmpTEIx8JOyrQog=="], "@expo/image-utils": ["@expo/image-utils@0.8.17", "", { "dependencies": { "@expo/require-utils": "^55.0.8", "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "getenv": "^2.0.0", "jimp-compact": "0.16.1", "parse-png": "^2.1.0", "semver": "^7.6.0" } }, "sha512-1eVrX6FFkPNl9DJNiw3Gjsf6PtG7HttbapEVvcROVrA9PCMikujOJWWHDYKGptmKoVIIVajFZE8UHGHUb3mPug=="], "@expo/json-file": ["@expo/json-file@10.2.0", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "json5": "^2.2.3" } }, "sha512-S6XzKe3R9GQeHiUPXc3xJjOv2VJhOEwFYf7xdC2z2cUqt3kZJ9mSO877sNQloVdnW/SUCtPY3bexlM7nwq+CAQ=="], - "@expo/metro": ["@expo/metro@54.2.0", "", { "dependencies": { "metro": "0.83.3", "metro-babel-transformer": "0.83.3", "metro-cache": "0.83.3", "metro-cache-key": "0.83.3", "metro-config": "0.83.3", "metro-core": "0.83.3", "metro-file-map": "0.83.3", "metro-minify-terser": "0.83.3", "metro-resolver": "0.83.3", "metro-runtime": "0.83.3", "metro-source-map": "0.83.3", "metro-symbolicate": "0.83.3", "metro-transform-plugins": "0.83.3", "metro-transform-worker": "0.83.3" } }, "sha512-h68TNZPGsk6swMmLm9nRSnE2UXm48rWwgcbtAHVMikXvbxdS41NDHHeqg1rcQ9AbznDRp6SQVC2MVpDnsRKU1w=="], + "@expo/local-build-cache-provider": ["@expo/local-build-cache-provider@55.0.16", "", { "dependencies": { "@expo/config": "~55.0.21", "chalk": "^4.1.2" } }, "sha512-/m2kb/+G2ryZ60FPZcuiLXzXp55p/X8QPXuU5CV/il0sSJcY0sQFICfM9ApokzHtzNQ0nDQOBUoQY3weilgP2g=="], + + "@expo/log-box": ["@expo/log-box@55.0.13", "", { "dependencies": { "@expo/dom-webview": "^55.0.6", "anser": "^1.4.9", "stacktrace-parser": "^0.1.10" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-pV623uwyKjw/L1HVWOpwWOu/ISLH1+c+ESVv30alQMbEaE3cLcwcQ+UnHiAGayMBNMQwK57eckOgH40RBXHfCA=="], - "@expo/metro-config": ["@expo/metro-config@54.0.17", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.20.0", "@babel/generator": "^7.20.5", "@expo/config": "~12.0.14", "@expo/env": "~2.0.12", "@expo/json-file": "~10.0.16", "@expo/metro": "~54.2.0", "@expo/spawn-async": "^1.7.2", "browserslist": "^4.25.0", "chalk": "^4.1.0", "debug": "^4.3.2", "dotenv": "~16.4.5", "dotenv-expand": "~11.0.6", "getenv": "^2.0.0", "glob": "^13.0.0", "hermes-parser": "^0.29.1", "jsc-safe-url": "^0.2.4", "lightningcss": "^1.30.1", "picomatch": "^4.0.3", "postcss": "~8.4.32", "resolve-from": "^5.0.0" }, "peerDependencies": { "expo": "*" } }, "sha512-PQFgQCZGY0DffZUvBzJttDPreZfHrQakaBlKjnvOUMNXbDna+TYmg1IFZuIDUYJezLcdp+TvVFTLjNi1+mqaVw=="], + "@expo/metro": ["@expo/metro@55.1.2", "", { "dependencies": { "metro": "0.83.8", "metro-babel-transformer": "0.83.8", "metro-cache": "0.83.8", "metro-cache-key": "0.83.8", "metro-config": "0.83.8", "metro-core": "0.83.8", "metro-file-map": "0.83.8", "metro-minify-terser": "0.83.8", "metro-resolver": "0.83.8", "metro-runtime": "0.83.8", "metro-source-map": "0.83.8", "metro-symbolicate": "0.83.8", "metro-transform-plugins": "0.83.8", "metro-transform-worker": "0.83.8" } }, "sha512-EOBoqqX4jD7JvHBHKDoek8SmWyAFPuJePyDSqIAhIjOCPAM67fVxd4+pCXJ6SDK1P9so55v2iTFBwCoeCmEj4Q=="], + + "@expo/metro-config": ["@expo/metro-config@55.0.27", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.20.0", "@babel/generator": "^7.20.5", "@expo/config": "~55.0.21", "@expo/env": "~2.1.3", "@expo/json-file": "~10.0.15", "@expo/metro": "~55.1.2", "@expo/spawn-async": "^1.7.2", "browserslist": "^4.25.0", "chalk": "^4.1.0", "debug": "^4.3.2", "getenv": "^2.0.0", "glob": "^13.0.0", "hermes-parser": "^0.32.0", "jsc-safe-url": "^0.2.4", "lightningcss": "^1.30.1", "picomatch": "^4.0.3", "postcss": "^8.5.14", "resolve-from": "^5.0.0" }, "peerDependencies": { "expo": "*" }, "optionalPeers": ["expo"] }, "sha512-oikn1NPFpQgNqL/HTqA6oAG2Af/BwldIezlecTidc8D+fkFgLFKkZHUq1Hh1g/vuCPGTQNWDlHla13dAgYwRvg=="], + + "@expo/metro-runtime": ["@expo/metro-runtime@55.0.12", "", { "dependencies": { "@expo/log-box": "55.0.13", "anser": "^1.4.9", "pretty-format": "^29.7.0", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-dom": "*", "react-native": "*" }, "optionalPeers": ["react-dom"] }, "sha512-EeqXrRBvChdt6+brlUkZM5749QoS7OlN7Zsn/AT8hhGV+xNKglirVRkcKQFmKqPgjgmNxfwgLJ6ddanwZ9dapg=="], "@expo/osascript": ["@expo/osascript@2.7.1", "", { "dependencies": { "@expo/spawn-async": "^1.8.0" } }, "sha512-Zn03EX6In7ts2lPUW2ESUSkEhEWQN1qqsiXjadtZMJOuZRkMiAg1ZQHuvz9DjByDWNJ2pBwAGyrts9lj9k389g=="], "@expo/package-manager": ["@expo/package-manager@1.13.1", "", { "dependencies": { "@expo/json-file": "^11.0.1", "@expo/spawn-async": "^1.8.0", "chalk": "^4.0.0", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "resolve-workspace-root": "^2.0.0" } }, "sha512-y/K+CaYYpZpNGZhSX4HyLT/vyIunFjNfyoxNysPBCefeLKI/VCx6f9LNPzrxayr3rCYO5bl9O8H+HRQK265Nkg=="], - "@expo/plist": ["@expo/plist@0.7.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-vrpryU1GoqSIRNqRB2D3IjXDmzNYfiQpEF6AH/xknlD7eiYmEDt3mb26V7cLcedcPG8PY/1xWHdBXVQJfEAh6Q=="], + "@expo/plist": ["@expo/plist@0.5.4", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-Jqppj0FULNq6Zp5JtQrFICl8TtpMjwwUbxEcEC2T3z7m+TOrTQEHZXz3D3Ay7vhbmvD+VMgfWJ4ARclJXeN8Eg=="], "@expo/prebuild-config": ["@expo/prebuild-config@56.0.23", "", { "dependencies": { "@expo/config": "~56.0.14", "@expo/config-plugins": "~56.0.16", "@expo/config-types": "^56.0.7", "@expo/image-utils": "^0.10.5", "@expo/json-file": "^10.2.0", "@react-native/normalize-colors": "0.85.3", "debug": "^4.3.1", "expo-modules-autolinking": "~56.0.23", "resolve-from": "^5.0.0", "semver": "^7.6.0" } }, "sha512-cXHsaLMMN1NJUV84TAXbajdllzQVFwtZ4dW+iyWEjc1nFGiMih66Oj7EfvhAM2y6IvsDEEu02XoCnl4zlAlRHA=="], - "@expo/require-utils": ["@expo/require-utils@56.1.7", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8" }, "peerDependencies": { "typescript": "^5.0.0 || ^5.0.0-0 || ^6.0.0 || ^7.0.0" } }, "sha512-7Huw3V4VVThWZglueHpHX4gxdYvjhKOO5DzC/1NBuaDY4IPf/iPhqE+diFA6J/ZF0nC+tcC4YmUidXhnr7cmzQ=="], + "@expo/require-utils": ["@expo/require-utils@55.0.8", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8" }, "peerDependencies": { "typescript": "^5.0.0 || ^5.0.0-0" } }, "sha512-DDS5R67iz2SJwncbpcUMHtnsq31dSfllhaEncKxGlK0/ewgc2CaXUCkDA2AumfT7ubnSXiPkU9Gn8v2xCrvj+A=="], + + "@expo/router-server": ["@expo/router-server@55.0.19", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "@expo/metro-runtime": "^55.0.12", "expo": "*", "expo-constants": "^55.0.17", "expo-font": "^55.0.8", "expo-router": "*", "expo-server": "^55.0.12", "react": "*", "react-dom": "*", "react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1" }, "optionalPeers": ["@expo/metro-runtime", "expo-router", "react-dom", "react-server-dom-webpack"] }, "sha512-bm2CFp9eI9biqZXz7Vkske0LXHZaLyonWV6sTEWUaR8zJ0NdVnUxb1Vd0iCdhW8GtweOUfMK4z7gojd0kydxjA=="], - "@expo/schema-utils": ["@expo/schema-utils@0.1.9", "", {}, "sha512-t9bYwG4Z0yCVzHYJoDMci1OFq2FkBkhStlfUGSkspKYTwB/84+x6sY+CXCgdhkQNQtvWaugW5KUs9YZfAXq9Sg=="], + "@expo/schema-utils": ["@expo/schema-utils@55.0.5", "", {}, "sha512-wdV4SzWJ/l+6y/r9vDaqPqXGyfxUD1igbX8rRbRBfkVXenAUY2qenq6HF4S0iJ0pjpVDNTANn5aQY7VV55hhsA=="], "@expo/sdk-runtime-versions": ["@expo/sdk-runtime-versions@1.0.0", "", {}, "sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ=="], @@ -943,7 +1023,7 @@ "@expo/sudo-prompt": ["@expo/sudo-prompt@9.3.2", "", {}, "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw=="], - "@expo/ui": ["@expo/ui@0.2.0-beta.9", "", { "dependencies": { "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-RaBcp0cMe5GykQogJwRZGy4o4JHDLtrr+HaurDPhwPKqVATsV0rR11ysmFe4QX8XWLP/L3od7NOkXUi5ailvaw=="], + "@expo/ui": ["@expo/ui@55.0.17", "", { "dependencies": { "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-NW/wFs+mhQeA/5W0yUNn6qKRNJBGiuF7NjCLZDFvg5Tyd+hDUEDEhBsLvI9MJ8B2aYuGcbuH9XZBIPF22LMrfQ=="], "@expo/vector-icons": ["@expo/vector-icons@15.1.1", "", { "peerDependencies": { "expo-font": ">=14.0.4", "react": "*", "react-native": "*" } }, "sha512-Iu2VkcoI5vygbtYngm7jb4ifxElNVXQYdDrYkT7UCEIiKLeWnQY0wf2ZhHZ+Wro6Sc5TaumpKUOqDRpLi5rkvw=="], @@ -957,14 +1037,16 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="], + "@gorhom/bottom-sheet": ["@gorhom/bottom-sheet@5.2.14", "", { "dependencies": { "@gorhom/portal": "1.0.14", "invariant": "^2.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-native": "*", "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.16.1", "react-native-reanimated": ">=3.16.0 || >=4.0.0-" }, "optionalPeers": ["@types/react", "@types/react-native"] }, "sha512-uLQFlDjp9z+jrOFcMSEldPqL5JdaXL3vXOh+juhwoNvXgTsEorJLjHTugXu+YccAG/0KJnShzKCrb71MHBsvJg=="], + + "@gorhom/portal": ["@gorhom/portal@1.0.14", "", { "dependencies": { "nanoid": "^3.3.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A=="], + "@hapi/hoek": ["@hapi/hoek@9.3.0", "", {}, "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="], "@hapi/topo": ["@hapi/topo@5.1.0", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg=="], "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], - "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], - "@isaacs/ttlcache": ["@isaacs/ttlcache@1.4.1", "", {}, "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA=="], "@istanbuljs/load-nyc-config": ["@istanbuljs/load-nyc-config@1.1.0", "", { "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", "get-package-type": "^0.1.0", "js-yaml": "^3.13.1", "resolve-from": "^5.0.0" } }, "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ=="], @@ -1079,6 +1161,8 @@ "@legendapp/state": ["@legendapp/state@3.0.0-beta.48", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "expo-sqlite": "^15.0.0" }, "optionalPeers": ["expo-sqlite"] }, "sha512-aa7vpUSoBHnXQhYf2x/10C0w1EiesegRhR7LuxnIcEPyZzZyuSF2mevf1jphlh0zzqwIJm9QxnsayDNn8/kwcA=="], + "@margelo/hermes-profile-transformer": ["@margelo/hermes-profile-transformer@0.0.10", "", { "dependencies": { "source-map": "^0.7.3" } }, "sha512-mQp4pMbdBRUPNdgXEt0H+VlQWdEOpvaImP5HBgwY0dsFLxN/qnCGZoERe5kyN5q5x5GT5bj9kmZPt5V7EMhfRA=="], + "@microsoft/1ds-core-js": ["@microsoft/1ds-core-js@4.4.4", "", { "dependencies": { "@microsoft/applicationinsights-core-js": "3.4.4", "@microsoft/applicationinsights-shims": "3.0.1", "@microsoft/dynamicproto-js": "^2.0.3", "@nevware21/ts-async": ">= 0.5.5 < 0.6.0", "@nevware21/ts-utils": ">= 0.14.0 < 2.x" } }, "sha512-rXWefMBlDfkdYsIfrifw30Yq2s5PD4iU1VbifL8AWPB94S/CKKaUg+Zy4rZPLmCTocONalrs8GrcpaPxN/KXrg=="], "@microsoft/1ds-post-js": ["@microsoft/1ds-post-js@4.4.4", "", { "dependencies": { "@microsoft/applicationinsights-core-js": "3.4.4", "@microsoft/applicationinsights-shims": "3.0.1", "@microsoft/dynamicproto-js": "^2.0.3", "@nevware21/ts-async": ">= 0.5.5 < 0.6.0", "@nevware21/ts-utils": ">= 0.14.0 < 2.x" } }, "sha512-ah1NOJx3sBZ+2uLQHFUrT5G8xbQ4vllfxNwDHPR+dh6cuE98ynlsdlgMtArkCqdRMILcDMTu8ZD4v9NASafAmw=="], @@ -1107,25 +1191,69 @@ "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + "@radix-ui/primitive": ["@radix-ui/primitive@1.2.0", "", {}, "sha512-KABY+VMIRPJL+Vyzv/335FEhXXD/xDv8BMb1kl48/aXJZMH0LtHCTx5ga5RnxaEP3nZHtmTLf1P9vanmFX46bw=="], + + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-slot": "1.5.0", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+yiQzqcc7Sy7/5s0jrMCjpPRFB3538VNrXPWdTLZhE/SCfgEws4aI6lHWN/vvcnyhHndROWBzqmqB4QBrITZ9Q=="], + + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.5", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA=="], + + "@radix-ui/react-context": ["@radix-ui/react-context@1.3.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-v5AYEocMISkbJ+1fTXzZpiFv7NRr3D2WiYbLIKf+FkmJ8VkP6d503eV2n34MFussHXY1pcoq3yfJiQH77dc4Mg=="], + + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.3.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-dismissable-layer": "1.2.1", "@radix-ui/react-focus-guards": "1.2.0", "@radix-ui/react-focus-scope": "1.3.1", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-portal": "1.2.0", "@radix-ui/react-presence": "1.1.11", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-slot": "1.5.0", "@radix-ui/react-use-controllable-state": "1.3.0", "@radix-ui/react-use-layout-effect": "1.1.4", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+0E4qvVTwjAWra9sEhJ+9BTsvHE00zkUuNw1VGh7S+Gc13dfYk+S1PVQdchWDj+XYHhAai7Z7IpzXVDCGsDplg=="], + + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FtRA52a/r+LKStFT+neLUm+8vSe7wbzkbQGGeU8ixFiwC8NU14Evoq9ATECqL3eGbcUzJR4fyfA4KkK/q6dfqg=="], + + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.2.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-use-callback-ref": "1.1.5", "@radix-ui/react-use-effect-event": "0.0.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-KIfDcAsx+oVylFZ4xryVKusJUrLLhTQmfnJW4kwk4Km5myvhHCrj04hnWWUC166+xD4DHdH8tQGylwYX1h6+Pg=="], + + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MPm5R0iRGUDrDnPNxmLBkv+X7pN4XKzt+MbWJEuJQsH3NFH4omrbpXpucyYCCKTSt+na/ZxGhEWQrWTZ0285cA=="], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.3.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-use-callback-ref": "1.1.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ugeMhbXt4y5S2T1GnIqbWDKEN962PDwnX1fN0eKOOgf5f63Dh2mn8F1FUySlgW0iF6GiVsRF32DzuDIWTUyQJg=="], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.4", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA=="], + + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.2.0", "", { "dependencies": { "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-StX7xXnaRU41VvFtMm9zlhVGz8XF6GmWWTXftkQaY+tqVkbs/R9UmAp5mLzHHapk5hM9RMZQvO/3usyIP/g42g=="], + + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.11", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Z7/atlkab36Z8hm+V8zYc79r0W+tiOB1o0tun1By94JyadkekLQmtEAnBSP9XKXBNQsStoStS4+gX10JpS5akA=="], + + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.2.0", "", { "dependencies": { "@radix-ui/react-slot": "1.5.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-HCTSPMNqv55Qk0IX4geJbpyXazmqulcimwuyjxTkKi29v+QZbG2Zho9BnbBq4YS2PF+KlNb2CXoLz66uUmblkg=="], + + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.2.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-collection": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-direction": "1.2.0", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-use-callback-ref": "1.1.5", "@radix-ui/react-use-controllable-state": "1.3.0", "@radix-ui/react-use-is-hydrated": "0.1.3", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UwraORzVUeEPMclhO1p0vz0qI+TbdDLO9i+FiOqhJyHKwdpCRCck9yVEscUnAMIzCiuNoQa8D4A9bIpOPXGt6Q=="], + + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.5.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-tyYd9ddPXAysAbdbD9eYxgwpCSv6f3PncqVSNKQFSR5lcJVLz3cb/pANDoYrLiHBpXv7skortyUgGUWsYuPPFQ=="], + + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.2.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-direction": "1.2.0", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-presence": "1.1.11", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-roving-focus": "1.2.1", "@radix-ui/react-use-controllable-state": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-hRoJlode+hg5ZNlR+3tkfkUliIybdTpD5lkAvHQ9yaXseGf7XVO9Ib2GPEIOuaJJbo7YWYbNpDDRfo8Dhf6/xA=="], + + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.5", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y5GBf4NKeADoXI1geiMlMuMoURT3k9MizzzKhGb/m55H94aGigZ01PIpkvsO6lML13BA6XYAXSJ6gza7vTn5nw=="], + + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.3.0", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-use-effect-event": "0.0.5", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-TAcBI7bw2ljYCbv4+zuTSd6zICu8pRt6SDN2vJ/Z06K2MXh6I9WYFe/A9ovlFDtR5HVHGm5iDxDoABm/++dbKg=="], + + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.5", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg=="], + + "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw=="], + + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw=="], + "@react-native-async-storage/async-storage": ["@react-native-async-storage/async-storage@2.2.0", "", { "dependencies": { "merge-options": "^3.0.4" }, "peerDependencies": { "react-native": "^0.0.0-0 || >=0.65 <1.0" } }, "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw=="], + "@react-native-community/blur": ["@react-native-community/blur@4.4.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-XBSsRiYxE/MOEln2ayunShfJtWztHwUxLFcSL20o+HNNRnuUDv+GXkF6FmM2zE8ZUfrnhQ/zeTqvnuDPGw6O8A=="], + "@react-native-community/cli": ["@react-native-community/cli@20.1.3", "", { "dependencies": { "@react-native-community/cli-clean": "20.1.3", "@react-native-community/cli-config": "20.1.3", "@react-native-community/cli-doctor": "20.1.3", "@react-native-community/cli-server-api": "20.1.3", "@react-native-community/cli-tools": "20.1.3", "@react-native-community/cli-types": "20.1.3", "commander": "^9.4.1", "deepmerge": "^4.3.0", "execa": "^5.0.0", "find-up": "^5.0.0", "fs-extra": "^8.1.0", "graceful-fs": "^4.1.3", "picocolors": "^1.1.1", "prompts": "^2.4.2", "semver": "^7.5.2" }, "bin": { "rnc-cli": "build/bin.js" } }, "sha512-sLo8cu9JyFNfuuF1C+8NJ4DHE/PEFaXGd4enkcxi/OJjGG8+sOQrdjNQ4i+cVh/2c+ah1mEMwsYjc3z0+/MqSg=="], "@react-native-community/cli-clean": ["@react-native-community/cli-clean@20.1.3", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.3", "execa": "^5.0.0", "fast-glob": "^3.3.2", "picocolors": "^1.1.1" } }, "sha512-sFLdLzapfC0scjgzBJJWYDY2RhHPjuuPkA5r6q0gc/UQH/izXpMpLrhh1DW84cMDraNACK0U62tU7ebNaQ1LMQ=="], "@react-native-community/cli-config": ["@react-native-community/cli-config@20.1.3", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.3", "cosmiconfig": "^9.0.0", "deepmerge": "^4.3.0", "fast-glob": "^3.3.2", "joi": "^17.2.1", "picocolors": "^1.1.1" } }, "sha512-n73nW0cG92oNF0r994pPqm0DjAShOm3F8LSffDYhJqNAno+h/csmv/37iL4NtSpmKIO8xqsG3uVTXz9X/hzNaQ=="], - "@react-native-community/cli-config-android": ["@react-native-community/cli-config-android@17.0.0", "", { "dependencies": { "@react-native-community/cli-tools": "17.0.0", "chalk": "^4.1.2", "fast-glob": "^3.3.2", "fast-xml-parser": "^4.4.1" } }, "sha512-XwWgcjvpjhqeUQrs04UCmmVLLMHBpsXE+CfjgGK/BPtMsaD76N5OvD94WlbQs0okVYmGngOudoE9XFb5wcnHrA=="], + "@react-native-community/cli-config-android": ["@react-native-community/cli-config-android@20.0.0", "", { "dependencies": { "@react-native-community/cli-tools": "20.0.0", "chalk": "^4.1.2", "fast-glob": "^3.3.2", "fast-xml-parser": "^4.4.1" } }, "sha512-asv60qYCnL1v0QFWcG9r1zckeFlKG+14GGNyPXY72Eea7RX5Cxdx8Pb6fIPKroWH1HEWjYH9KKHksMSnf9FMKw=="], "@react-native-community/cli-config-apple": ["@react-native-community/cli-config-apple@20.1.3", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.3", "execa": "^5.0.0", "fast-glob": "^3.3.2", "picocolors": "^1.1.1" } }, "sha512-QX9B83nAfCPs0KiaYz61kAEHWr9sttooxzRzNdQwvZTwnsIpvWOT9GvMMj/19OeXiQzMJBzZX0Pgt6+spiUsDQ=="], "@react-native-community/cli-doctor": ["@react-native-community/cli-doctor@20.1.3", "", { "dependencies": { "@react-native-community/cli-config": "20.1.3", "@react-native-community/cli-platform-android": "20.1.3", "@react-native-community/cli-platform-apple": "20.1.3", "@react-native-community/cli-platform-ios": "20.1.3", "@react-native-community/cli-tools": "20.1.3", "command-exists": "^1.2.8", "deepmerge": "^4.3.0", "envinfo": "^7.13.0", "execa": "^5.0.0", "node-stream-zip": "^1.9.1", "ora": "^5.4.1", "picocolors": "^1.1.1", "semver": "^7.5.2", "wcwidth": "^1.0.1", "yaml": "^2.2.1" } }, "sha512-EI+mAPWn255/WZ4CQohy1I049yiaxVr41C3BeQ2BCyhxODIDR8XRsLzYb1t9MfqK/C3ZncUN2mPSRXFeKPPI1w=="], - "@react-native-community/cli-platform-android": ["@react-native-community/cli-platform-android@17.0.0", "", { "dependencies": { "@react-native-community/cli-config-android": "17.0.0", "@react-native-community/cli-tools": "17.0.0", "chalk": "^4.1.2", "execa": "^5.0.0", "logkitty": "^0.7.1" } }, "sha512-bgn7FHf45DCtm7U8ZCXt6pTZ+f+Uzg8WtYgGoXpzOtFSeXgqmeuhHRyFP2ZSeUDDSVuHsJTgxQC13A2+mUnkvQ=="], + "@react-native-community/cli-platform-android": ["@react-native-community/cli-platform-android@20.0.0", "", { "dependencies": { "@react-native-community/cli-config-android": "20.0.0", "@react-native-community/cli-tools": "20.0.0", "chalk": "^4.1.2", "execa": "^5.0.0", "logkitty": "^0.7.1" } }, "sha512-th3ji1GRcV6ACelgC0wJtt9daDZ+63/52KTwL39xXGoqczFjml4qERK90/ppcXU0Ilgq55ANF8Pr+UotQ2AB/A=="], "@react-native-community/cli-platform-apple": ["@react-native-community/cli-platform-apple@20.1.3", "", { "dependencies": { "@react-native-community/cli-config-apple": "20.1.3", "@react-native-community/cli-tools": "20.1.3", "execa": "^5.0.0", "fast-xml-parser": "^5.3.6", "picocolors": "^1.1.1" } }, "sha512-XJ+DqAD4hkplWVXK5AMgN7pP9+4yRSe5KfZ/b42+ofkDBI55ALlUmX+9HWE3fMuRjcotTCoNZqX2ov97cFDXpQ=="], - "@react-native-community/cli-platform-ios": ["@react-native-community/cli-platform-ios@17.0.0", "", { "dependencies": { "@react-native-community/cli-platform-apple": "17.0.0" } }, "sha512-WV/YvMUcy/CUi4W/C8WO9wnWWOH+kIsR5Y/bFtRdsjIP0FIWncWAUi8cSyCTWW9G0rOA1Cy5afHpdE8A9G/B/w=="], + "@react-native-community/cli-platform-ios": ["@react-native-community/cli-platform-ios@20.0.0", "", { "dependencies": { "@react-native-community/cli-platform-apple": "20.0.0" } }, "sha512-Z35M+4gUJgtS4WqgpKU9/XYur70nmj3Q65c9USyTq6v/7YJ4VmBkmhC9BticPs6wuQ9Jcv0NyVCY0Wmh6kMMYw=="], "@react-native-community/cli-server-api": ["@react-native-community/cli-server-api@20.1.3", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.3", "body-parser": "^2.2.2", "compression": "^1.7.1", "connect": "^3.6.5", "errorhandler": "^1.5.1", "nocache": "^3.0.1", "open": "^6.2.0", "pretty-format": "^29.7.0", "serve-static": "^1.13.1", "strict-url-sanitise": "0.0.1", "ws": "^6.2.3" } }, "sha512-hsNsdUKZDd2T99OuNuiXz4VuvLa1UN0zcxefmPjXQgI0byrBLzzDr+o7p03sKuODSzKi2h+BMnUxiS07HACQLA=="], @@ -1133,53 +1261,73 @@ "@react-native-community/cli-types": ["@react-native-community/cli-types@20.1.3", "", { "dependencies": { "joi": "^17.2.1" } }, "sha512-IdAcegf0pH1hVraxWTG1ACLkYC0LDQfqtaEf42ESyLIF3Xap70JzL/9tAlxw7lSCPZPFWhrcgU0TBc4SkC/ecw=="], - "@react-native-macos/virtualized-lists": ["@react-native-macos/virtualized-lists@0.81.7", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.1.4", "react": "*", "react-native": "*" } }, "sha512-kD+MdpS+pLG2c4ghX5lD5a9GR0N/fKx77d2+fimTJMoJXp0qXwp1xJriZj8ZkL/Xqb3HgpxVcp0e5esgG3nBwg=="], + "@react-native-community/datetimepicker": ["@react-native-community/datetimepicker@9.2.2", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "expo": ">=52.0.0", "react": "*", "react-native": "*", "react-native-windows": "*" }, "optionalPeers": ["expo", "react-native-windows"] }, "sha512-0cZGrK61UZ8VxpKKV/lun/L/b5wKus55IhvqqdewUayHwIc73lrLI0l5CIlHw8tA0/yQEzHEMEdtHQglAD+hKA=="], + + "@react-native-community/netinfo": ["@react-native-community/netinfo@11.5.2", "", { "peerDependencies": { "react": "*", "react-native": ">=0.59" } }, "sha512-/g0m65BtX9HU+bPiCH2517bOHpEIUsGrWFXDzi1a5nNKn5KujQgm04WhL7/OSXWKHyrT8VVtUoJA0XKRxueBpQ=="], + + "@react-native-macos/virtualized-lists": ["@react-native-macos/virtualized-lists@0.83.0", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.2.0", "react": "*", "react-native": "*" }, "optionalPeers": ["@types/react"] }, "sha512-BCqsS5WmJ1rZ62gR+kixlxpx2V62q/sFc/sqPkVwblqN/ianB3YdJ7noG+mGyxEscddUp7qu22+pfWJEEOKXZw=="], + + "@react-native-masked-view/masked-view": ["@react-native-masked-view/masked-view@0.3.2", "", { "peerDependencies": { "react": ">=16", "react-native": ">=0.57" } }, "sha512-XwuQoW7/GEgWRMovOQtX3A4PrXhyaZm0lVUiY8qJDvdngjLms9Cpdck6SmGAUNqQwcj2EadHC1HwL0bEyoa/SQ=="], "@react-native-runtimes/core": ["@react-native-runtimes/core@0.1.0-alpha.2", "", { "peerDependencies": { "@expo/config-plugins": ">=9.0.0", "react": "*", "react-native": "*", "react-native-nitro-modules": "*" } }, "sha512-of6A8BXTnIziF/+CaDQmIY0saC+5TpegJhs4CCkRXIhspWluSt3AcI/hYBwgHimJxH8BIsXpQ6JJT0EFZfq25g=="], - "@react-native-windows/cli": ["@react-native-windows/cli@0.81.9", "", { "dependencies": { "@react-native-windows/codegen": "0.81.7", "@react-native-windows/find-dotnet-tools": "0.0.0-canary.3", "@react-native-windows/fs": "0.81.2", "@react-native-windows/package-utils": "0.81.2", "@react-native-windows/telemetry": "0.81.4", "@xmldom/xmldom": "^0.8.13", "chalk": "^4.1.0", "cli-spinners": "^2.2.0", "envinfo": "^7.5.0", "execa": "^5.0.0", "find-up": "^4.1.0", "glob": "^7.1.1", "lodash": "^4.17.15", "mustache": "^4.0.1", "ora": "^3.4.0", "prompts": "^2.4.1", "semver": "^7.3.2", "shelljs": "^0.8.4", "username": "^5.1.0", "xml-formatter": "^2.4.0", "xml-parser": "^1.2.1", "xpath": "^0.0.27" }, "peerDependencies": { "react-native": "^0.81.0" } }, "sha512-dyCrzPwTpEkfhv3sMmpY6VU1ZLDpOGVXETIjPfLYKnlGI/EM9kpBZcBpr7iyJV9ofZ2/3Ox+AFd4mXDQJGo/MA=="], + "@react-native-windows/cli": ["@react-native-windows/cli@0.83.2", "", { "dependencies": { "@react-native-windows/codegen": "0.83.1", "@react-native-windows/fs": "0.83.1", "@react-native-windows/package-utils": "0.83.1", "@react-native-windows/telemetry": "0.83.1", "@xmldom/xmldom": "^0.7.7", "chalk": "^4.1.0", "cli-spinners": "^2.2.0", "envinfo": "^7.5.0", "execa": "^5.0.0", "find-up": "^4.1.0", "glob": "^7.1.1", "lodash": "^4.17.15", "mustache": "^4.0.1", "ora": "^3.4.0", "prompts": "^2.4.1", "semver": "^7.3.2", "shelljs": "^0.8.4", "username": "^5.1.0", "xml-formatter": "^2.4.0", "xml-parser": "^1.2.1", "xpath": "^0.0.27" }, "peerDependencies": { "react-native": "*" } }, "sha512-uM3MCmBYU3Cnf+h+4Mc062TLAFhRn4s82EgnIghaPv6dCRstKNA/f9pqo9357p33Ay+uJOlr5VWr1ndGWW9x9w=="], - "@react-native-windows/codegen": ["@react-native-windows/codegen@0.81.7", "", { "dependencies": { "@react-native-windows/fs": "0.81.2", "chalk": "^4.1.0", "globby": "^11.1.0", "mustache": "^4.0.1", "source-map-support": "^0.5.19", "yargs": "^16.2.0" }, "peerDependencies": { "react-native": "^0.81.0" }, "bin": { "react-native-windows-codegen": "bin.js" } }, "sha512-vbwf8/ouLD4zrdiQnmyW3BEU9QutOjEvrQF6IxrZGQQ73IG7eWABEI9nxxJD91gpSryPp7ZfNPvXXlFCXXrHbg=="], + "@react-native-windows/codegen": ["@react-native-windows/codegen@0.83.1", "", { "dependencies": { "@react-native-windows/fs": "0.83.1", "chalk": "^4.1.0", "globby": "^11.1.0", "minimatch": "^10.0.3", "mustache": "^4.0.1", "source-map-support": "^0.5.19", "yargs": "^16.2.0" }, "peerDependencies": { "react-native": "*" }, "bin": { "react-native-windows-codegen": "bin.js" } }, "sha512-ZLMpt1pCS5FdPZQJerhQ9BMugNkhoO5LSPiShDqWeLAq4eSLTN/ulRZ2h9UAyd7DLi32hpolmeFVrhK8vMFNzg=="], "@react-native-windows/find-dotnet-tools": ["@react-native-windows/find-dotnet-tools@0.0.0-canary.3", "", { "dependencies": { "@react-native-windows/fs": "^0.81.2" } }, "sha512-4PtQ7pwfLhJBxCgYR7Y54/ZZWerz9AiJQu7/NycJAKkW5Odg8FrUawbmOccZzYavQA6kUqMrbaEF//6jzUPzxg=="], - "@react-native-windows/find-repo-root": ["@react-native-windows/find-repo-root@0.81.2", "", { "dependencies": { "@react-native-windows/fs": "0.81.2", "find-up": "^4.1.0" } }, "sha512-YFl5Op0FqShNoTej6QU9r60lT6PU8aZ7gVXYgv27zE9vRlucTxZWdeOwaEBy+twdP7qtWKTLPE6s/3ygmUMPWg=="], + "@react-native-windows/find-repo-root": ["@react-native-windows/find-repo-root@0.83.1", "", { "dependencies": { "@react-native-windows/fs": "0.83.1", "find-up": "^4.1.0", "minimatch": "^10.0.3" } }, "sha512-aQti0W11IZj6g+djQSfwwsnxkVMoGWFDHdv+QUs412+Ks4dtACnb4aHoq7rM+IiyDTjuGwF/6yAKF5hZ+dX86A=="], "@react-native-windows/fs": ["@react-native-windows/fs@0.81.2", "", { "dependencies": { "graceful-fs": "^4.2.8" } }, "sha512-SFzWRHNxr37bMb3Zpt32RoYOX2cW52C4wrjWcTLDT+jhVVwPPX5aAnfVjd/rPzUMX5etR1pl3EK48l6hYScfDQ=="], - "@react-native-windows/package-utils": ["@react-native-windows/package-utils@0.81.2", "", { "dependencies": { "@react-native-windows/find-repo-root": "0.81.2", "@react-native-windows/fs": "0.81.2", "get-monorepo-packages": "^1.2.0", "lodash": "^4.17.15" } }, "sha512-MMnH7yOHLWQdIvH7z4k6++pkmarRlmbtzHAOP06vSjXWZOeNFIQ8pyLbCqXDXZ37wf8CNMPRdW1D7FWZBH871w=="], + "@react-native-windows/package-utils": ["@react-native-windows/package-utils@0.83.1", "", { "dependencies": { "@react-native-windows/find-repo-root": "0.83.1", "@react-native-windows/fs": "0.83.1", "get-monorepo-packages": "^1.2.0", "lodash": "^4.17.15", "minimatch": "^10.0.3" } }, "sha512-LZv92FAoncPMK5n3d5lczzwxARi3p2jJkCXYW4tmAch3dLWk2A+jgJXEtgBP0Cb1yrGqEkLQQY88uP8wXMX0EQ=="], - "@react-native-windows/telemetry": ["@react-native-windows/telemetry@0.81.4", "", { "dependencies": { "@microsoft/1ds-core-js": "^4.3.0", "@microsoft/1ds-post-js": "^4.3.0", "@react-native-windows/fs": "0.81.2", "@xmldom/xmldom": "^0.8.13", "ci-info": "^3.2.0", "envinfo": "^7.8.1", "lodash": "^4.17.21", "os-locale": "^5.0.0", "xpath": "^0.0.27" } }, "sha512-OlShsJufENRC5yYgbcKomU0d+avaNzD/qg9Vo5dOKgCqPExwWukjGlg50W1HdnAnjUTnOy74V6nqn+wpe1SYzQ=="], + "@react-native-windows/telemetry": ["@react-native-windows/telemetry@0.83.1", "", { "dependencies": { "@microsoft/1ds-core-js": "^4.3.0", "@microsoft/1ds-post-js": "^4.3.0", "@react-native-windows/fs": "0.83.1", "@xmldom/xmldom": "^0.7.7", "ci-info": "^3.2.0", "envinfo": "^7.8.1", "lodash": "^4.17.21", "minimatch": "^10.0.3", "os-locale": "^5.0.0", "xpath": "^0.0.27" } }, "sha512-4dghrp4/GBV8A/vjjWAMcs8S70dmGsnwilCoYFhGtvmUL4U2HfxHrwFYu3LrxFQwX2+HDq8rE+tlnKbe+8wuUg=="], "@react-native/assets": ["@react-native/assets@1.0.0", "", {}, "sha512-KrwSpS1tKI70wuKl68DwJZYEvXktDHdZMG0k2AXD/rJVSlB23/X2CB2cutVR0HwNMJIal9HOUOBB2rVfa6UGtQ=="], - "@react-native/assets-registry": ["@react-native/assets-registry@0.81.6", "", {}, "sha512-nNlJ7mdXFoq/7LMG3eJIncqjgXkpDJak3xO8Lb4yQmFI3XVI1nupPRjlYRY0ham1gLE0F/AWvKFChsKUfF5lOQ=="], + "@react-native/assets-registry": ["@react-native/assets-registry@0.83.10", "", {}, "sha512-AcVfyQ+8HsWCecXEnSaRffP71KqaWrhNB8bLulYCpKO7i5h/lmwgF191uafU/WiS0LbEMTGlXwWBshPto1phyA=="], + + "@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.83.10", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@react-native/codegen": "0.83.10" } }, "sha512-iRfhxsOePloy12TUzpL9dCwnxLi3eurg8+JurSozAmWh4HtmKu0IJHNjbEKk9htI5n4RlFom8LQ9vVcvgWOTbw=="], - "@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.81.5", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@react-native/codegen": "0.81.5" } }, "sha512-oF71cIH6je3fSLi6VPjjC3Sgyyn57JLHXs+mHWc9MoCiJJcM4nqsS5J38zv1XQ8d3zOW2JtHro+LF0tagj2bfQ=="], + "@react-native/babel-preset": ["@react-native/babel-preset@0.83.10", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-arrow-functions": "^7.24.7", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-function-name": "^7.25.1", "@babel/plugin-transform-literals": "^7.25.2", "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-numeric-separator": "^7.24.7", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-shorthand-properties": "^7.24.7", "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", "@react-native/babel-plugin-codegen": "0.83.10", "babel-plugin-syntax-hermes-parser": "0.32.0", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-wTZWvs5cUQqr2qBAoJNKUy6IBH7wqPAJdxTC834S/7vBqtXypYHxDAFzYXkIh8pRTC86IBYxyc2IrBYHnB8O2g=="], - "@react-native/babel-preset": ["@react-native/babel-preset@0.81.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-arrow-functions": "^7.24.7", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-function-name": "^7.25.1", "@babel/plugin-transform-literals": "^7.25.2", "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-numeric-separator": "^7.24.7", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-shorthand-properties": "^7.24.7", "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", "@react-native/babel-plugin-codegen": "0.81.5", "babel-plugin-syntax-hermes-parser": "0.29.1", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" }, "peerDependencies": { "@babel/core": "*" } }, "sha512-UoI/x/5tCmi+pZ3c1+Ypr1DaRMDLI3y+Q70pVLLVgrnC3DHsHRIbHcCHIeG/IJvoeFqFM2sTdhSOLJrf8lOPrA=="], + "@react-native/codegen": ["@react-native/codegen@0.83.10", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.32.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "yargs": "^17.6.2" } }, "sha512-rTcuuwRDPLrBmhXc9vAr2gispQFCEqd2WVPh2+N5eV80p8tf9mHy7CoFnmPy8A8csK6HGlANaD9SMPvccQjh4g=="], - "@react-native/codegen": ["@react-native/codegen@0.81.6", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.29.1", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "yargs": "^17.6.2" }, "peerDependencies": { "@babel/core": "*" } }, "sha512-9KoYRep/KDnELLLmIYTtIIEOClVUJ88pxWObb/0sjkacA7uL4SgfbAg7rWLURAQJWI85L1YS67IhdEqNNk1I7w=="], + "@react-native/community-cli-plugin": ["@react-native/community-cli-plugin@0.83.10", "", { "dependencies": { "@react-native/dev-middleware": "0.83.10", "debug": "^4.4.0", "invariant": "^2.2.4", "metro": "^0.83.6", "metro-config": "^0.83.6", "metro-core": "^0.83.6", "semver": "^7.1.3" }, "peerDependencies": { "@react-native-community/cli": "*", "@react-native/metro-config": "*" }, "optionalPeers": ["@react-native-community/cli", "@react-native/metro-config"] }, "sha512-7QmZ7FLAIJbYjgxILBUE1k71lQB6atXZSLRczKE6Iti+BO3XHNiDSCZxuxUkYUJI0V/0kCbO/M8/a5zqHNu+Rw=="], - "@react-native/community-cli-plugin": ["@react-native/community-cli-plugin@0.81.6", "", { "dependencies": { "@react-native/dev-middleware": "0.81.6", "debug": "^4.4.0", "invariant": "^2.2.4", "metro": "^0.83.1", "metro-config": "^0.83.1", "metro-core": "^0.83.1", "semver": "^7.1.3" }, "peerDependencies": { "@react-native-community/cli": "*", "@react-native/metro-config": "*" } }, "sha512-oTwIheF4TU7NkfoHxwSQAKtIDx4SQEs2xufgM3gguY7WkpnhGa/BYA/A+hdHXfqEKJFKlHcXQu4BrV/7Sv1fhw=="], + "@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.83.10", "", {}, "sha512-AlSOMdXoaSXi4O82f3APvw14e+EfrEvwPerfH2AI3JUrD/yQjFtbIxeyRPd89/MlKIbZU4cwyVFqj96bj39J5g=="], - "@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.81.5", "", {}, "sha512-bnd9FSdWKx2ncklOetCgrlwqSGhMHP2zOxObJbOWXoj7GHEmih4MKarBo5/a8gX8EfA1EwRATdfNBQ81DY+h+w=="], + "@react-native/debugger-shell": ["@react-native/debugger-shell@0.83.10", "", { "dependencies": { "cross-spawn": "^7.0.6", "fb-dotslash": "0.5.8" } }, "sha512-hk9xFI9H412XSX1lpVuKrnxUQe3zIPKxFceYPUwx2L5aZQQ/yjypWkLs+LLldV6eh93B2sBnZbns1oFSE3LQNw=="], - "@react-native/dev-middleware": ["@react-native/dev-middleware@0.81.5", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.81.5", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^4.4.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "serve-static": "^1.16.2", "ws": "^6.2.3" } }, "sha512-WfPfZzboYgo/TUtysuD5xyANzzfka8Ebni6RIb2wDxhb56ERi7qDrE4xGhtPsjCL4pQBXSVxyIlCy0d8I6EgGA=="], + "@react-native/dev-middleware": ["@react-native/dev-middleware@0.83.10", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.83.10", "@react-native/debugger-shell": "0.83.10", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^4.4.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "serve-static": "^1.16.2", "ws": "^7.5.10" } }, "sha512-V39TcESd9LGzDBs7PYVsx5oE1oGv1dLC0GIyJyEyehZjmOYk5sJ4C0m1ATKPeDE7JhiY8s/p6pNOBNp+NF4FGQ=="], - "@react-native/gradle-plugin": ["@react-native/gradle-plugin@0.81.6", "", {}, "sha512-atUItC5MZ6yaNaI0sbsoDwUdF+KMNZcMKBIrNhXlUyIj3x1AQ6Cf8CHHv6Qokn8ZFw+uU6GWmQSiOWYUbmi8Ag=="], + "@react-native/gradle-plugin": ["@react-native/gradle-plugin@0.83.10", "", {}, "sha512-DpWzrcmxAEgD/tvy4r4WH10PYJPdfDCvtctVZ2Ez0sHlNYgWI6STfqxeAcZhU84hMhNsTec3IHDxNJ2cG8xPjQ=="], - "@react-native/js-polyfills": ["@react-native/js-polyfills@0.81.6", "", {}, "sha512-P5MWH/9vM24XkJ1TasCq42DMLoCUjZVSppTn6VWv/cI65NDjuYEy7bUSaXbYxGTnqiKyPG5Y+ADymqlIkdSAcw=="], + "@react-native/js-polyfills": ["@react-native/js-polyfills@0.83.10", "", {}, "sha512-q28LKHga5lf2ZX4u1T8Bj5RXqyzOBRWSXVjCCdczj/oacAVZPUrandGC1E9fR35fujyb3ZCqZbIQCvs8MhsTNA=="], - "@react-native/metro-babel-transformer": ["@react-native/metro-babel-transformer@0.81.6", "", { "dependencies": { "@babel/core": "^7.25.2", "@react-native/babel-preset": "0.81.6", "hermes-parser": "0.29.1", "nullthrows": "^1.1.1" }, "peerDependencies": { "@babel/core": "*" } }, "sha512-Z5up8Z1I6PAgY6Z78yXmRJHps5d8FqQIYbAALmSyg5tmtrJyd04JgrwEWhwH1HQsxB5beWwQRntA5WUfWNlQPA=="], + "@react-native/metro-babel-transformer": ["@react-native/metro-babel-transformer@0.83.10", "", { "dependencies": { "@babel/core": "^7.25.2", "@react-native/babel-preset": "0.83.10", "hermes-parser": "0.32.0", "nullthrows": "^1.1.1" } }, "sha512-Sl+NC7j9ek1WOTmqDU0iPLdxwyT83oxQWKud7OqiyycCZ11QrtwM6bzxRV7Ag4fJJoLY31h0fxwm6m7sTzAONQ=="], - "@react-native/metro-config": ["@react-native/metro-config@0.81.6", "", { "dependencies": { "@react-native/js-polyfills": "0.81.6", "@react-native/metro-babel-transformer": "0.81.6", "metro-config": "^0.83.1", "metro-runtime": "^0.83.1" } }, "sha512-tdLKkC1Dv0EW6dayL0pAItbdWhrVZDCiTcyb43cLtkeaQYpJgXeRX+1PsmkFywZDn/ojxiGxe/HLg5yZH0gbdA=="], + "@react-native/metro-config": ["@react-native/metro-config@0.83.10", "", { "dependencies": { "@react-native/js-polyfills": "0.83.10", "@react-native/metro-babel-transformer": "0.83.10", "metro-config": "^0.83.6", "metro-runtime": "^0.83.6" } }, "sha512-ay7e/GJjjylpeMSEe7NdgM0+C0W2s9QCdNStCQkqKzW2HngZOuNiQyCjULq0tOXQNf/00dfrki7xDlqCeUqMgQ=="], - "@react-native/new-app-screen": ["@react-native/new-app-screen@0.81.5", "", { "peerDependencies": { "@types/react": "^19.1.0", "react": "*", "react-native": "*" } }, "sha512-9Vi4TtS9vJho+/Kpw5sEHS80PLqiNqSJ0dJ51Rz9Bfbc8d/B474SlHtwefH0UqeWGJ+t4+MFlWKVvvoIh/EL/A=="], + "@react-native/new-app-screen": ["@react-native/new-app-screen@0.83.4", "", { "peerDependencies": { "@types/react": "^19.1.0", "react": "*", "react-native": "*" }, "optionalPeers": ["@types/react"] }, "sha512-MMurPtpVmWv3gtsdx3/C9wTqGhwyks0dDvxYOjFpK71241cBNQVVVLZCwYyea4hW6ytFMLYtnbP9+U+yVXvCSQ=="], - "@react-native/normalize-colors": ["@react-native/normalize-colors@0.81.6", "", {}, "sha512-/OCgUysHIFhfmZxbJAydVc58l2SGIZbWpbQXBrYEYch8YElBbDFQ8IUtyogB7YJJQ8ewHZFj93rQGaECgkvvcw=="], + "@react-native/normalize-colors": ["@react-native/normalize-colors@0.83.10", "", {}, "sha512-dWgqcxaBy27oLx9tndcTF0917vbLOOstyNjYD58J6Z7YxkEZSaKG6+A+3I1KV6O1Xg2D69QThp4t6ezoC3NiGA=="], - "@react-native/virtualized-lists": ["@react-native/virtualized-lists@0.81.6", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.1.4", "react": "*", "react-native": "*" } }, "sha512-1RrZl3a7iCoAS2SGaRLjJPIn8bg/GLNXzqkIB2lufXcJsftu1umNLRIi17ZoDRejAWSd2pUfUtQBASo4R2mw4Q=="], + "@react-native/virtualized-lists": ["@react-native/virtualized-lists@0.83.10", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.2.0", "react": "*", "react-native": "*" }, "optionalPeers": ["@types/react"] }, "sha512-KNX6jCYcCnOKfoJyGMcgDrAVaQXOuNymBi2UC9sf2msEmgkrGgoWa+eKiATqaCgN44UFKlJIuNj4wUPTCWzZ0Q=="], + + "@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.21.0", "", { "dependencies": { "@react-navigation/elements": "^2.9.45", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { "@react-navigation/native": "^7.5.1", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-G3P88TPiHw9rxGhstwdeVgaBj3YQl8emiF8zLgyfXOF2AIk31myIrtDQeu2Mg5Ad7YWMs4W8zkXuD78xChlTHQ=="], + + "@react-navigation/core": ["@react-navigation/core@7.24.0", "", { "dependencies": { "@react-navigation/routers": "^7.6.5", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "react-is": "^19.1.0", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": ">= 18.2.0" } }, "sha512-PPMHGSgLB65eqWOBHeD/nGyzGJV3rcZ5sZbsCXlaC37HnLmwTEjB4CaOQn8CBVVv/Sc0Kl/W5dpPLls9Stk2WA=="], + + "@react-navigation/elements": ["@react-navigation/elements@2.9.45", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.5.1", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-r64qCgjA6asQo4Zlv2lAl68ARkfPbV+BNRm3n9JlMoFr4Ma2A+92Z+sWT1coJRoSXkAwVNb70+LjpM6fo6/D4A=="], + + "@react-navigation/native": ["@react-navigation/native@7.5.1", "", { "dependencies": { "@react-navigation/core": "^7.24.0", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "standard-navigation": "^0.0.8", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*" } }, "sha512-p9xOvhG8q8rq0FPAoh0JC22ALbyRZt7U2uH8XrHRDjvqOEFrumME7szM6LaWW2WctolAVeZAfOTV+pUKULI/kQ=="], + + "@react-navigation/native-stack": ["@react-navigation/native-stack@7.21.0", "", { "dependencies": { "@react-navigation/elements": "^2.9.45", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0", "warn-once": "^0.1.1" }, "peerDependencies": { "@react-navigation/native": "^7.5.1", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-bYrNW87nDtbMXAJsosYwu59xwTqeuaurfCDA+62MQ6hY4+2Iv71+Sik3ulW92fOqg8s2SZinOJosHEWoi9Ayqw=="], + + "@react-navigation/routers": ["@react-navigation/routers@7.6.5", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-hpvpVFt6wu3U014F/0Qy45VZCErlUvIV8kN1L6HovN5P9OpbXAzFTaRQmG7VW6czg2tSkUL5S3dZA0HIu5QirQ=="], "@rnx-kit/metro-config": ["@rnx-kit/metro-config@2.2.6", "", { "dependencies": { "@rnx-kit/tools-node": "^3.0.0", "@rnx-kit/tools-react-native": "^2.3.1", "@rnx-kit/tools-workspaces": "^0.2.6" }, "peerDependencies": { "@react-native/metro-config": "*", "react": "*", "react-native": "*" }, "optionalPeers": ["@react-native/metro-config"] }, "sha512-iptQ5DOxDVy8N43e87LBDrk3wV5FOSr4TJngoVUV4pFgoNtVdwsh8aBeOxZDUsnGYEZPOY1m0b+09E0L5H7e3A=="], @@ -1237,6 +1385,48 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + "@sentry/browser": ["@sentry/browser@10.76.2", "", { "dependencies": { "@sentry/browser-utils": "10.76.2", "@sentry/conventions": "^0.16.0", "@sentry/core": "10.76.2", "@sentry/feedback": "10.76.2", "@sentry/replay": "10.76.2", "@sentry/replay-canvas": "10.76.2" } }, "sha512-dFyuWYjDs0/LetQZqK5IHj0/wIC8sBYADyMjU5Fo0nt2vzT11CPPG2AKeITIQcoRRs9IZ8ahtDMqWMvBPfDs1w=="], + + "@sentry/browser-utils": ["@sentry/browser-utils@10.76.2", "", { "dependencies": { "@sentry/conventions": "^0.16.0", "@sentry/core": "10.76.2" } }, "sha512-onALxAdvLWyK+0erbrv7LVzh1zX9sErOM21eCz1dpNUIQadrR9tmd5J1kL9a2Wagq1RyIWRmy/YtpwOK4myZaQ=="], + + "@sentry/bundler-plugins": ["@sentry/bundler-plugins@10.76.2", "", { "dependencies": { "@babel/core": "^7.18.5", "@sentry/cli": "^2.58.6", "@sentry/core": "10.76.2", "dotenv": "^17.4.2", "find-up": "^5.0.0", "glob": "^13.0.6", "magic-string": "~0.30.8" }, "peerDependencies": { "rollup": ">=3.2.0", "webpack": ">=5.0.0" }, "optionalPeers": ["rollup", "webpack"] }, "sha512-5ovSYkHaTTDwe6p9vq0AzE+W9likkINBpzCv7VychGqYUfBGNzr+0YVkAt9//saJI9BKs+lWud5qX5iASf6wkw=="], + + "@sentry/cli": ["@sentry/cli@3.8.1", "", { "dependencies": { "progress": "^2.0.3", "proxy-from-env": "^1.1.0", "undici": "^6.22.0", "which": "^2.0.2" }, "optionalDependencies": { "@sentry/cli-darwin": "3.8.1", "@sentry/cli-linux-arm": "3.8.1", "@sentry/cli-linux-arm64": "3.8.1", "@sentry/cli-linux-i686": "3.8.1", "@sentry/cli-linux-x64": "3.8.1", "@sentry/cli-win32-arm64": "3.8.1", "@sentry/cli-win32-i686": "3.8.1", "@sentry/cli-win32-x64": "3.8.1" }, "bin": { "sentry-cli": "bin/sentry-cli" } }, "sha512-9bcaIaUzT/GLIKRyO1VUiX+DKaLza9/QBjdFdSuzOoP721Oy/HNGjn67ieA6yIpEyw7uEcUGuRqy8aUozKGaEg=="], + + "@sentry/cli-darwin": ["@sentry/cli-darwin@3.8.1", "", { "os": "darwin" }, "sha512-fARVvybd2wg9ajwCYETI8gvBhkD1xN39z15jgC8HvfssVEUfC2yKfl9W4/NZ3YbXIlVT4aNhbrmfHd36IRhffg=="], + + "@sentry/cli-linux-arm": ["@sentry/cli-linux-arm@3.8.1", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "arm" }, "sha512-mRyEuD7LDPdwMw8jsTuW0egBkSk2kddCZS0TON7+mRhWEgW21acrTJTvkHxcc2bL8wRIAqyG8z5dnedt3r8ang=="], + + "@sentry/cli-linux-arm64": ["@sentry/cli-linux-arm64@3.8.1", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "arm64" }, "sha512-0juQjMZ0Oc2o1XEYKw0jVZpGLq3beA1kJ9mK+NUqh8ulbyRzfv7ALK6i2VKDRmiepN5TRLB11nvwD50DySGirQ=="], + + "@sentry/cli-linux-i686": ["@sentry/cli-linux-i686@3.8.1", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "ia32" }, "sha512-RNP8WJTYcZ546+otTpAB9P99s1zD3sv1iimDXjufwTsCWyqdo0D1mpN2F30u6+0ur6l91+H88m5pfep3gc+7BA=="], + + "@sentry/cli-linux-x64": ["@sentry/cli-linux-x64@3.8.1", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "x64" }, "sha512-rmie06o7WOmbdqIqGCHrqiBj7WDn4DOnZIi+uGKwPepDuhdupMz728resdJN23dPYTWojKlTwXBU6ahzmV5kLA=="], + + "@sentry/cli-win32-arm64": ["@sentry/cli-win32-arm64@3.8.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-qizCjnnJs19kUnTvbW2IQoLdqWOXY6nZlCZuNPnl3y9VGzvk5hSFVtKsAj83U2+xQgkBm+/bboNsJ6Dvv1cGfA=="], + + "@sentry/cli-win32-i686": ["@sentry/cli-win32-i686@3.8.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-PuJKeSPum2nKwRwKF1UCA79J3NQ5I6Ds+nwN1GA2Bth5I3F/SJ421TEXxs05Q5ghr+mRHWXCz2e4jecikVVgPA=="], + + "@sentry/cli-win32-x64": ["@sentry/cli-win32-x64@3.8.1", "", { "os": "win32", "cpu": "x64" }, "sha512-F/62XBSZmjRzh/PylnMM4uBjW15I97ZSK+Bp8NfAS+bxqeCpWc7sh9NOqnaNqYoVAMfEkecu6SzKXr0H58Zc7A=="], + + "@sentry/conventions": ["@sentry/conventions@0.16.0", "", {}, "sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ=="], + + "@sentry/core": ["@sentry/core@10.76.2", "", { "dependencies": { "@sentry/conventions": "^0.16.0" } }, "sha512-vndrQalRiwYatTkjlLWvyf69KocXIb9TKJ4A9oiyWguL8PJYtqIRTlRe4OpkpjLrxS18fvRdV0jEm29c/9p3hQ=="], + + "@sentry/expo-upload-sourcemaps": ["@sentry/expo-upload-sourcemaps@8.32.0", "", { "dependencies": { "@sentry/cli": "3.8.1" }, "peerDependencies": { "@expo/env": "*", "dotenv": "*" }, "optionalPeers": ["@expo/env", "dotenv"], "bin": { "expo-upload-sourcemaps": "cli.js" } }, "sha512-Pl6j2ZalUZ/CWdwuTeY4b6LqiInrXUBOKGgkE7gwkIZPLkp9/5H8XN9vNJWItPMxsdF72UcJEA1whYEatTCmGg=="], + + "@sentry/feedback": ["@sentry/feedback@10.76.2", "", { "dependencies": { "@sentry/core": "10.76.2" } }, "sha512-w8T7+LfAXjPPg/CpIcXHzPfeMHXR+61WhfYCwKNFK4mUQknYK4m0Kbt28dte9fSsijo4hKvHZ+tJc591SaD4ig=="], + + "@sentry/react": ["@sentry/react@10.76.2", "", { "dependencies": { "@sentry/browser": "10.76.2", "@sentry/conventions": "^0.16.0", "@sentry/core": "10.76.2" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "sha512-ylW0pGg6LA8iPEPjoLXi/6IxzPSTo92pqwVMPAGRhS04he904jsIV4oMt97irDfyGEUUdZFdm/tpWO40Xuu4RQ=="], + + "@sentry/react-native": ["@sentry/react-native@8.32.0", "", { "dependencies": { "@sentry/browser": "10.76.2", "@sentry/bundler-plugins": "10.76.2", "@sentry/cli": "3.8.1", "@sentry/core": "10.76.2", "@sentry/expo-upload-sourcemaps": "8.32.0", "@sentry/react": "10.76.2" }, "peerDependencies": { "@babel/core": ">=7.0.0", "@expo/config": "*", "@expo/config-types": "*", "@expo/env": "*", "@react-navigation/native": "*", "dotenv": "*", "expo": ">=49.0.0", "expo-router": "*", "expo-updates": "*", "metro": "*", "metro-config": "*", "metro-resolver": "*", "open": "*", "promise": "*", "react": ">=17.0.0", "react-native": ">=0.65.0" }, "optionalPeers": ["@babel/core", "@expo/config", "@expo/config-types", "@expo/env", "@react-navigation/native", "dotenv", "expo", "expo-router", "expo-updates", "metro", "metro-config", "metro-resolver", "open", "promise"], "bin": { "sentry-eas-build-on-error": "scripts/eas-build-hook.js", "sentry-eas-build-on-success": "scripts/eas-build-hook.js", "sentry-eas-build-on-complete": "scripts/eas-build-hook.js", "sentry-expo-upload-sourcemaps": "scripts/expo-upload-sourcemaps.js" } }, "sha512-Z7W2t0oDrm46JU+FgdB0AL+Ru5urBVmFhYl/RjuPi2AnAjddmbNyDZGzpOLSYtl6oWqqz2u4zGuj3ZFjrCMJOg=="], + + "@sentry/replay": ["@sentry/replay@10.76.2", "", { "dependencies": { "@sentry/browser-utils": "10.76.2", "@sentry/core": "10.76.2" } }, "sha512-xMNFEZ7KuEvfj9j1cKmcH+p6u5t3NZs1vcrj+sBW5+S9C327ASOAvcR23ZoM0UhTXr9j7j+Dozs/NYHW2EYjpw=="], + + "@sentry/replay-canvas": ["@sentry/replay-canvas@10.76.2", "", { "dependencies": { "@sentry/core": "10.76.2", "@sentry/replay": "10.76.2" } }, "sha512-mx7/pXIS7CpuduN5TFI36pBoe62CeTQ4IMQJ+MEJXAhxMmYFZQunc24dNltcs6hjzrhtZqGBYSAV1J+iCKFwdg=="], + + "@shopify/react-native-skia": ["@shopify/react-native-skia@2.14.0", "", { "dependencies": { "canvaskit-wasm": "0.41.0", "react-native-skia-android": "154.0.0", "react-native-skia-apple-ios": "154.0.0", "react-native-skia-apple-macos": "154.0.0", "react-native-skia-apple-tvos": "154.0.0", "react-reconciler": "0.31.0" }, "peerDependencies": { "react": ">=19.0", "react-native": ">=0.78", "react-native-reanimated": ">=4.0.0", "react-native-worklets": ">=0.7.0" }, "optionalPeers": ["react-native", "react-native-reanimated", "react-native-worklets"], "bin": { "setup-skia-web": "scripts/setup-canvaskit.js" } }, "sha512-mzizcTeUAgvam+m1BsxTs+K8ujUXkzoCmwumUhV5NWcoImmKJiWUMfQYgw2OOt1p6qpr7+G5pPOELAmNzZRvlw=="], + "@sideway/address": ["@sideway/address@4.1.5", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q=="], "@sideway/formula": ["@sideway/formula@3.0.1", "", {}, "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="], @@ -1251,6 +1441,8 @@ "@spark-examples/kitchen-sink": ["@spark-examples/kitchen-sink@workspace:examples/kitchen-sink"], + "@sqltools/formatter": ["@sqltools/formatter@1.2.5", "", {}, "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw=="], + "@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="], @@ -1337,6 +1529,8 @@ "@types/graceful-fs": ["@types/graceful-fs@4.1.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ=="], + "@types/hammerjs": ["@types/hammerjs@2.0.46", "", {}, "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw=="], + "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="], "@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="], @@ -1345,7 +1539,7 @@ "@types/node": ["@types/node@24.13.6", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-SGrw/h3KPFshy3OE6ZL53LMBG5vGQQ8/gIpiqz/kRZhPJ7HgwCEs8LBuNtWLa8dvGZVpSF7+Bf+c11HUrCb/yg=="], - "@types/react": ["@types/react@19.1.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg=="], + "@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="], "@types/react-dom": ["@types/react-dom@19.3.0", "", { "peerDependencies": { "@types/react": "^19.3.0" } }, "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q=="], @@ -1363,16 +1557,14 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.4.0", "", {}, "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ=="], - "@urql/core": ["@urql/core@5.2.0", "", { "dependencies": { "@0no-co/graphql.web": "^1.0.13", "wonka": "^6.3.2" } }, "sha512-/n0ieD0mvvDnVAXEQgX/7qJiVcvYvNkOHeBvkwtylfjydar123caCXcl58PXFY11oU1oquJocVXHxLAbtv4x1A=="], - - "@urql/exchange-retry": ["@urql/exchange-retry@1.3.2", "", { "dependencies": { "@urql/core": "^5.1.2", "wonka": "^6.3.2" }, "peerDependencies": { "@urql/core": "^5.0.0" } }, "sha512-TQMCz2pFJMfpNxmSfX1VSfTjwUIFx/mL+p1bnfM1xjjdla7Z+KnGMW/EhFbpckp3LyWAH4PgOsMwOMnIN+MBFg=="], - "@vitest/mocker": ["@vitest/mocker@5.0.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.31", "@vitest/spy": "5.0.1", "estree-walker": "^3.0.3", "magic-string": "^1.2.3" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw"] }, "sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q=="], "@vitest/spy": ["@vitest/spy@5.0.1", "", {}, "sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q=="], "@vscode/sudo-prompt": ["@vscode/sudo-prompt@9.3.2", "", {}, "sha512-gcXoCN00METUNFeQOFJ+C9xUI0DKB+0EGMVg7wbVYRHBw2Eq3fKisDZOkRdOz3kqXRKOENMfShPOmypw1/8nOw=="], + "@webgpu/types": ["@webgpu/types@0.1.21", "", {}, "sha512-pUrWq3V5PiSGFLeLxoGqReTZmiiXwY3jRkIG5sLLKjyqNxrwm/04b4nw7LSmGWJcKk59XOM/YRTUwOzo4MMlow=="], + "@xmldom/xmldom": ["@xmldom/xmldom@0.8.15", "", {}, "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA=="], "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], @@ -1397,7 +1589,7 @@ "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + "ansis": ["ansis@4.4.0", "", {}, "sha512-9k3v7xcHwgdO/DruxGIg4HtjvlAZlcnsX/mzqUb1t3NkYnl9kK2UJ+Gq0io+vQf7iT//BD/HB/NBkUR1LWxoeA=="], "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], @@ -1409,6 +1601,8 @@ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + "arkregex": ["arkregex@0.0.8", "", { "dependencies": { "@ark/util": "0.56.2" } }, "sha512-PJcx6G1kQTgLKPUbeYlYecDRaKq15AMSGVajlKFYWlPeJRQL+j3dKE6tyMs40HZ99djS1l9Vhl3ezAHy9JBIqQ=="], "arktype": ["arktype@2.2.3", "", { "dependencies": { "@ark/schema": "0.56.2", "@ark/util": "0.56.2", "arkregex": "0.0.8" } }, "sha512-7W+0RLTUNJiBFIIZXwOQxSR8Z273IAd6IvqBeG9+gHnQKFsIx2C0iOtGTmMrPnlX4qLXyc5+ll7A0BIj9WrbTg=="], @@ -1425,6 +1619,8 @@ "async-limiter": ["async-limiter@1.0.1", "", {}, "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="], + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + "babel-jest": ["babel-jest@29.7.0", "", { "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" } }, "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg=="], "babel-plugin-istanbul": ["babel-plugin-istanbul@6.1.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-instrument": "^5.0.4", "test-exclude": "^6.0.0" } }, "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA=="], @@ -1441,17 +1637,17 @@ "babel-plugin-react-native-web": ["babel-plugin-react-native-web@0.21.2", "", {}, "sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA=="], - "babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.29.1", "", { "dependencies": { "hermes-parser": "0.29.1" } }, "sha512-2WFYnoWGdmih1I1J5eIqxATOeycOqRwYxAQBu3cUu/rhwInwHUg7k60AFNbuGjSDL8tje5GDrAnxzRLcu2pYcA=="], + "babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.32.0", "", { "dependencies": { "hermes-parser": "0.32.0" } }, "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg=="], "babel-plugin-transform-flow-enums": ["babel-plugin-transform-flow-enums@0.0.2", "", { "dependencies": { "@babel/plugin-syntax-flow": "^7.12.1" } }, "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ=="], "babel-preset-current-node-syntax": ["babel-preset-current-node-syntax@1.2.0", "", { "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg=="], - "babel-preset-expo": ["babel-preset-expo@54.0.12", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-transform-class-static-block": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.25.9", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.81.5", "babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-native-web": "~0.21.0", "babel-plugin-syntax-hermes-parser": "^0.29.1", "babel-plugin-transform-flow-enums": "^0.0.2", "debug": "^4.3.4", "resolve-from": "^5.0.0" }, "peerDependencies": { "@babel/runtime": "^7.20.0", "expo": "*", "react-refresh": ">=0.14.0 <1.0.0" } }, "sha512-6xeSkdaixmQhWSYL7tfLu0pOS0BY+8ftwmdNSHtpEFSizrXYZkCjk/B6Dxr+6nwNRihixMcS0aBlWS1wlDl3pw=="], + "babel-preset-expo": ["babel-preset-expo@55.0.25", "", { "dependencies": { "@babel/generator": "^7.20.5", "@babel/helper-module-imports": "^7.25.9", "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-transform-class-static-block": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.25.9", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.83.10", "babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-native-web": "~0.21.0", "babel-plugin-syntax-hermes-parser": "^0.32.0", "babel-plugin-transform-flow-enums": "^0.0.2", "debug": "^4.3.4", "resolve-from": "^5.0.0" }, "peerDependencies": { "@babel/runtime": "^7.20.0", "expo": "*", "expo-widgets": "^55.0.20", "react-refresh": ">=0.14.0 <1.0.0" }, "optionalPeers": ["@babel/runtime", "expo", "expo-widgets"] }, "sha512-P/CaG4kvSzd8Pb1CiS8m6C9XCwuLptaaw+F/GfDNGUkuF3/DlYCkdj4Ogq1mJ5OJ80qdlaY9TzxB28CrL/dIqg=="], "babel-preset-jest": ["babel-preset-jest@29.6.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], @@ -1465,11 +1661,13 @@ "body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + "bplist-creator": ["bplist-creator@0.1.0", "", { "dependencies": { "stream-buffers": "2.2.x" } }, "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg=="], "bplist-parser": ["bplist-parser@0.5.0", "", {}, "sha512-owgQ0RGbndIGNrQkpHe99HVSslIK59t31nVyAPDRqhq9mNDc2kRl5HwYO292GuKjHqiJRdJNiaI63a02hjLFIQ=="], - "brace-expansion": ["brace-expansion@2.1.7", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g=="], + "brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -1477,12 +1675,14 @@ "bser": ["bser@2.1.1", "", { "dependencies": { "node-int64": "^0.4.0" } }, "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ=="], - "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], @@ -1493,12 +1693,12 @@ "caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="], + "canvaskit-wasm": ["canvaskit-wasm@0.41.0", "", { "dependencies": { "@webgpu/types": "0.1.21" } }, "sha512-cnbL02NFB3yOYMF/MtxViZHgD1vh55Pvy+zR8q4JuFvyCPejZP3eClkt2GuZ0S7jOmGMCJXaHBasbMChbR9JZg=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], - "chrome-launcher": ["chrome-launcher@0.15.2", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0" }, "bin": { "print-chrome-path": "bin/print-chrome-path.js" } }, "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ=="], "chromium-edge-launcher": ["chromium-edge-launcher@0.2.0", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0", "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg=="], @@ -1511,16 +1711,22 @@ "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + "colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="], "command-exists": ["command-exists@1.2.9", "", {}, "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w=="], @@ -1545,6 +1751,12 @@ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], + + "css-tree": ["css-tree@1.1.3", "", { "dependencies": { "mdn-data": "2.0.14", "source-map": "^0.6.1" } }, "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q=="], + + "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "culori": ["culori@4.0.2", "", {}, "sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw=="], @@ -1555,12 +1767,16 @@ "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], - "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + "decode-uri-component": ["decode-uri-component@0.2.2", "", {}, "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="], + + "dedent": ["dedent@1.7.2", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA=="], "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + "define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], @@ -1569,15 +1785,25 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + "dnssd-advertise": ["dnssd-advertise@1.1.6", "", {}, "sha512-Ndrrf6BMPalkQPd/zubL+4YghH2J9NspapQ09uDXwYbvOPkP0oaqf5CkcwJ0b50kS2O3ul6yVu+jz+RY62Cejg=="], + + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + "dompurify": ["dompurify@3.4.17", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w=="], - "dotenv": ["dotenv@16.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="], + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], - "dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="], + "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -1595,7 +1821,7 @@ "enhanced-resolve": ["enhanced-resolve@5.25.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w=="], - "env-editor": ["env-editor@0.4.2", "", {}, "sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA=="], + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], @@ -1629,59 +1855,93 @@ "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + "execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], - "expo": ["expo@54.0.37", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "54.0.27", "@expo/config": "~12.0.14", "@expo/config-plugins": "~54.0.5", "@expo/devtools": "0.1.8", "@expo/fingerprint": "0.15.5", "@expo/metro": "~54.2.0", "@expo/metro-config": "54.0.17", "@expo/vector-icons": "^15.0.3", "@ungap/structured-clone": "^1.3.0", "babel-preset-expo": "~54.0.12", "expo-asset": "~12.0.13", "expo-constants": "~18.0.14", "expo-file-system": "~19.0.24", "expo-font": "~14.0.12", "expo-keep-awake": "~15.0.8", "expo-modules-autolinking": "3.0.27", "expo-modules-core": "3.0.30", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-without-unicode": "8.0.0-3" }, "peerDependencies": { "@expo/dom-webview": "*", "@expo/metro-runtime": "*", "react": "*", "react-native": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/dom-webview", "@expo/metro-runtime"], "bin": { "expo": "bin/cli", "expo-modules-autolinking": "bin/autolinking", "fingerprint": "bin/fingerprint" } }, "sha512-afzbtO4i/K9ZCrzyLgEOaXW56w/yuAK61JkCOMrw/S/VFCAp6YwKWA494X5mIcrVmRAmclv/ec7ga8ndTkzhxw=="], + "expo": ["expo@55.0.32", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "55.0.37", "@expo/config": "~55.0.21", "@expo/config-plugins": "~55.0.12", "@expo/devtools": "55.0.3", "@expo/fingerprint": "0.16.8", "@expo/local-build-cache-provider": "55.0.16", "@expo/log-box": "55.0.13", "@expo/metro": "~55.1.2", "@expo/metro-config": "55.0.27", "@expo/vector-icons": "^15.0.2", "@ungap/structured-clone": "^1.3.0", "babel-preset-expo": "~55.0.25", "expo-asset": "~55.0.20", "expo-constants": "~55.0.17", "expo-file-system": "~55.0.26", "expo-font": "~55.0.8", "expo-keep-awake": "~55.0.8", "expo-modules-autolinking": "55.0.28", "expo-modules-core": "55.0.27", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-minimum": "^0.1.2" }, "peerDependencies": { "@expo/dom-webview": "*", "@expo/metro-runtime": "*", "react": "*", "react-native": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/dom-webview", "@expo/metro-runtime", "react-native-webview"], "bin": { "expo": "bin/cli", "fingerprint": "bin/fingerprint", "expo-modules-autolinking": "bin/autolinking" } }, "sha512-w0Y4tstI6+s67Icsrw8/EHnqLPJEtYAikQq2UlWLN0cSEWmQOD5KuxSwmJmNf+4m5aUuWKExwr4B9pw8vLnpHQ=="], + + "expo-application": ["expo-application@55.0.19", "", { "peerDependencies": { "expo": "*" } }, "sha512-RFer4T46/OaIkNQEweA9PQj6xlXKor71IPNydWxlupx/XG05Yv/269HJJ3Q9zejdY9M7R3jTeTlAYYmXwbLBgA=="], + + "expo-asset": ["expo-asset@55.0.20", "", { "dependencies": { "@expo/image-utils": "^0.8.17", "expo-constants": "~55.0.17" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-BpybBESZj8WxW/WXuQhiY8D1azkJtxLuteIfprokJw0uU5r/g7cD+vzrzKJSlE4hhUlKeUGsoKzSK0nSJJ2sMQ=="], - "expo-asset": ["expo-asset@12.0.13", "", { "dependencies": { "@expo/image-utils": "^0.8.8", "expo-constants": "~18.0.13" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-x/p7WvQUnkn6K43b9eL6SPeq5Vnf1E8BDe9bDrWrvMqzyUvJnUFvl+ctg3034s/+UHe7Ne2pAmc0+yzbl8CrDQ=="], + "expo-audio": ["expo-audio@55.0.18", "", { "peerDependencies": { "expo": "*", "expo-asset": "*", "react": "*", "react-native": "*" } }, "sha512-izDr80hhHXkfIZn0TXdCheUhWEVRV205a6j0gdhgJ9se36C9Yo+REyaqFrEI1La4p95vPXvaH7UkAx/EGRKNHQ=="], - "expo-audio": ["expo-audio@1.1.1", "", { "peerDependencies": { "expo": "*", "expo-asset": "*", "react": "*", "react-native": "*" } }, "sha512-CPCpJ+0AEHdzWROc0f00Zh6e+irLSl2ALos/LPvxEeIcJw1APfBa4DuHPkL4CQCWsVe7EnUjFpdwpqsEUWcP0g=="], + "expo-blur": ["expo-blur@55.0.18", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-eXuVAahonZXLrXa//isvq72ed1to/MmbHUyd2WTkRH2/FjC9X91rZ4BUNIFBZnAxaNXQrKw/ITfG/VMVz1DIhQ=="], - "expo-clipboard": ["expo-clipboard@8.0.8", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-VKoBkHIpZZDJTB0jRO4/PZskHdMNOEz3P/41tmM6fDuODMpqhvyWK053X0ebspkxiawJX9lX33JXHBCvVsTTOA=="], + "expo-clipboard": ["expo-clipboard@55.0.17", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-wVWLvBfsFE3W3MBBSYAjwybkV40XowK65R9PDqfiuU2F9h7grKVjCjWD6FE7I4goZo0hFM8MfZNP+7XOdJMYiQ=="], - "expo-constants": ["expo-constants@18.0.14", "", { "dependencies": { "@expo/config": "~12.0.14", "@expo/env": "~2.0.12" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-BUZm9mkl/TX7zNaN0N4C83ws7mEnesCFiKhqK+rMZwTD2+Q4wWB2kBltqnC/LrCRaMyRi40XxhCRVwuKnJkxXQ=="], + "expo-constants": ["expo-constants@55.0.17", "", { "dependencies": { "@expo/env": "~2.1.3" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-t0SNWmXTjXPCHzjNsv1Okjaj8SpXQcUjDPtYDqvofDS+BhAsvlKNmwaaWXvduBjjmmmJWNH8q1/x9IWQ+upg8g=="], - "expo-crypto": ["expo-crypto@15.0.8", "", { "dependencies": { "base64-js": "^1.3.0" }, "peerDependencies": { "expo": "*" } }, "sha512-aF7A914TB66WIlTJvl5J6/itejfY78O7dq3ibvFltL9vnTALJ/7LYHvLT4fwmx9yUNS6ekLBtDGWivFWnj2Fcw=="], + "expo-crypto": ["expo-crypto@55.0.19", "", { "peerDependencies": { "expo": "*" } }, "sha512-Ip1jQ5GphvrC4Ls5osAmIapi+JZXOqRJYaaMxTFWpkhssNUq88oP45pWGv2uU2K/Ci6j7iLDJzU8eN3v+B7jrA=="], "expo-desktop": ["expo-desktop@1.0.6", "", { "dependencies": { "@clack/prompts": "^1.2.0", "@expo/config": "^56.0.9", "@expo/config-plugins": "^56.0.8", "@expo/env": "^2.3.0", "@expo/fingerprint": "^0.20.12", "@expo/json-file": "^10.2.0", "@expo/metro": "^56.0.2", "@expo/package-manager": "^1.12.0", "@expo/plist": "^0.7.0", "@expo/spawn-async": "^1.8.0", "@expo/xcpretty": "^4.4.4", "@react-native-community/cli-config": "^20.1.3", "arktype": "^2.2.0", "bplist-parser": "^0.5.0", "chalk": "^4.1.2", "citty": "^0.2.2", "debug": "^4.4.3", "expo-desktop-config-plugins": "^1.3.2", "expo-desktop-prebuild-config": "^1.1.3", "getenv": "^2.0.0", "glob": "^10.5.0", "kleur": "^4.1.5", "multitars": "^1.0.0", "mustache": "^4.2.0", "ora": "3.4.0", "picomatch": "^2.3.2", "prompts": "^2.4.2", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, "bin": { "expo-desktop": "build/cli.js" } }, "sha512-cXFwLNirTpou8xEQFoedYrMekVh7FefUBFtdG8tiUvc03T2IbboPrblzn3923eS2mhuNaphBW5EylXYIVRKatQ=="], "expo-desktop-config-plugins": ["expo-desktop-config-plugins@1.3.2", "", { "dependencies": { "@expo/config-plugins": "^56.0.8", "@expo/json-file": "^10.2.0", "@expo/plist": "^0.7.0", "chalk": "^4.1.2", "debug": "^4.3.4", "fast-xml-builder": "^1.2.0", "fast-xml-parser": "^5.8.0", "glob": "^10.5.0", "slash": "^3.0.0", "slugify": "^1.6.6", "xcode": "^3.0.1" } }, "sha512-MmTVhZqyPJDivudUf03XYDa7Wru9WFnMTyp7owsSLWhfpqVTQ84hfsM++2vGG2T/MQ2+Xx+yONkoNKS+pyK/Lg=="], - "expo-desktop-metro-config": ["expo-desktop-metro-config@54.81.3", "", { "dependencies": { "@expo/metro-config": "^54.0.17", "@react-native/metro-config": "~0.81.6", "@rnx-kit/metro-config": "^2.2.6" } }, "sha512-aqQgXJ3KvWmF74BdgttYKO8DFh+pDi74d3yDaJV6N1rii8UmbH07jyMyYv4DzhCBWxmCWobDNalHrolMc7PeNw=="], + "expo-desktop-metro-config": ["expo-desktop-metro-config@55.83.3", "", { "dependencies": { "@expo/metro-config": "^55.0.27", "@react-native/metro-config": "~0.83.10", "@rnx-kit/metro-config": "^2.2.6" } }, "sha512-t9ViGmePfrXxXVgmXjQrMvwmKTQP7f2l8lXsD5WMoUd2WNeE3vNLKiAwIh0jhbZJbsN9Ig/RJS8KCt+UhTM/nA=="], - "expo-desktop-modules-core": ["expo-desktop-modules-core@54.0.14", "", { "peerDependencies": { "react-native": "~0.81.0", "react-native-windows": "~0.81.0" } }, "sha512-TFhMZytNhUwz+fWaHNv+nnylWGMMvTZCiETsetKJKaoe1AwRc3+JMmuIK63pImtXWNlw8hL+Ue5FNSzgpHBgsg=="], + "expo-desktop-modules-core": ["expo-desktop-modules-core@55.0.2", "", { "peerDependencies": { "react-native": "~0.81.0", "react-native-windows": "~0.81.0" } }, "sha512-7x2Hu/VdVEtx8Jqr815hrfjDxPzg4ca888jZoVTWJcxPAOIWYrn1I9TVG/tjVs7PYvgMZVDGvzcyEh4o6NlOHA=="], "expo-desktop-prebuild-config": ["expo-desktop-prebuild-config@1.1.3", "", { "dependencies": { "@expo/config": "^56.0.9", "@expo/config-plugins": "^56.0.8", "@expo/prebuild-config": "^56.0.15", "expo-desktop-config-plugins": "^1.3.2" } }, "sha512-e4Pyisye+LqaJRMByIx//i3cewhoVuf1Kndm3BIF0H6Uf0Zkb96QK6UqwUl+DCijtyfJJ52b0Wj36IhGWtc44g=="], - "expo-desktop-stubs": ["expo-desktop-stubs@54.0.15", "", { "peerDependencies": { "expo-desktop-modules-core": "^54.0.14", "react-native": "*", "react-native-windows": "*" } }, "sha512-OvfOHbwP3oM66CbUpqVZCObjpFfCd+7ipA5uNflvT4UGnhLXS2jfTrt2/ZkCz4CQagfOqpuyeXUfG3ctHOOx/g=="], + "expo-desktop-stubs": ["expo-desktop-stubs@55.0.2", "", { "peerDependencies": { "expo-desktop-modules-core": "^55.0.2", "react-native": "*", "react-native-windows": "*" } }, "sha512-D8WTFSyorrEYYTaRZuXNL5eA/YMycFoA7CBzQdzEJrSi68//zX3Jmflf4KsDLBe3M0TnRIovjCcpjeTsYYQN8g=="], + + "expo-desktop-template-bare-minimum": ["expo-desktop-template-bare-minimum@55.83.10", "", { "dependencies": { "expo": "~55.0.31", "expo-desktop": "^1.0.6", "expo-desktop-modules-core": "~55.0.2", "expo-desktop-stubs": "~55.0.2", "expo-status-bar": "~55.0.6", "react": "19.2.0", "react-native": "0.83.10", "react-native-macos": "0.83.0", "react-native-windows": "0.83.2" } }, "sha512-kx2Ptr8UdKM2kGjssP5ZtMHTkkSgzibYiN/DHu9UQFDzLeiPHmBAmKD+cDzQqdNSdkj9cC1kI7rP6HPyWMC/Ag=="], + + "expo-device": ["expo-device@55.0.21", "", { "dependencies": { "ua-parser-js": "^0.7.33" }, "peerDependencies": { "expo": "*" } }, "sha512-W2DNWWnjk3KSdAy2BtK9ie1lByO6IsJIpriQAnvS1+d/e12wXhq14iZLUB5iA0CCVftU3eliQRIzkyT2cdL9Ng=="], + + "expo-document-picker": ["expo-document-picker@55.0.17", "", { "peerDependencies": { "expo": "*" } }, "sha512-tyrPB+k3lTI03umNP8cc912yZF7tgyo9v/+rEeW9h/u0fmMJAvegyaQl/YNW2MeKpjYa4+WwiLBGXVj54XHDog=="], + + "expo-file-system": ["expo-file-system@55.0.26", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-KBOQWqxumCxJBN29Lu13QpccwAhcjsgDYCJSVVjfGydaK/UbEkH/1rT/WZmdfoZ4G8mUJlfRbWfnJtfHUXxsdw=="], + + "expo-font": ["expo-font@55.0.8", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-WyP75pnKqhLNktYwDn3xKAUNt5rLihRDv8XWGhhz6VEhVqypixpT86NA3uGtiDTlM3gGjhrYCY7o7ypXgCUOZg=="], + + "expo-glass-effect": ["expo-glass-effect@55.0.11", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-wqq7GUOqSkfoFJzreZvBG0jzjsq5c582m3glhWSjcmIuByxXXWp6j6GY6hyFuYKzpOXhbuvusVxGCQi0yWnp3g=="], + + "expo-haptics": ["expo-haptics@55.0.18", "", { "peerDependencies": { "expo": "*" } }, "sha512-bEH0bf5bBGn7U2eTZVdiXBUk4vTfNGbfv9/9PCYLg3gL7L/WE2g8WKs2vYg1B4/BDSucyO8prZofH3IgvLJojQ=="], + + "expo-image": ["expo-image@55.0.11", "", { "dependencies": { "sf-symbols-typescript": "^2.2.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-PVIBYQJW/h1f6Zb9xnoWlgfqyOPVm2yb6eo6ZogaKbvMrhb/Q/fiERbagi4oqmR6IPljWPEpkXXQyFBUh7TjpQ=="], + + "expo-image-loader": ["expo-image-loader@55.0.1", "", { "peerDependencies": { "expo": "*" } }, "sha512-o8gCo1j59XpXDh0/llgNYPcnfecYQhafQAO0yw5pb+kukPizvNoEqea8tFQIIQmNYqxd6Ljgs7lLXed0gXpOdQ=="], + + "expo-image-manipulator": ["expo-image-manipulator@55.0.21", "", { "dependencies": { "expo-image-loader": "~55.0.1" }, "peerDependencies": { "expo": "*" } }, "sha512-BlVl692hVDKuWzOCnbUsfp1u0XqujOABm/Kv3ISMg0qRMchvD6vUSJ/XOIj80+6B9CCm1hNgklgeJ0SIEHiCVQ=="], + + "expo-image-picker": ["expo-image-picker@55.0.24", "", { "dependencies": { "expo-image-loader": "~55.0.1" }, "peerDependencies": { "expo": "*" } }, "sha512-scbtvQIgiqiOxQx/lFEocJkWPzYANHjwFLnzfKjdZNWh3RJjjMmtHaEoeu7lM9RzqE3EwF+JQecgy+FATXNfiQ=="], - "expo-desktop-template-bare-minimum": ["expo-desktop-template-bare-minimum@54.81.6", "", { "dependencies": { "expo": "~54.0.35", "expo-desktop": "^1.0.6", "expo-desktop-modules-core": "~54.0.14", "expo-desktop-stubs": "~54.0.15", "expo-status-bar": "~3.0.9", "react": "19.1.4", "react-native": "0.81.6", "react-native-macos": "0.81.9", "react-native-windows": "0.81.37" } }, "sha512-X9YwsEMQBobxS1wig0soOM/m/vRyPRZLFvaWWqJOl7UyQyLWyBg2gzhnAjbjV6cF+JJ/1ttDwzke54/Mm/YmlQ=="], + "expo-keep-awake": ["expo-keep-awake@55.0.8", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-PfIpMfM+STOBwkR5XOE+yVtER86c44MD+W8QD8JxuO0sT9pF7Y1SJYakWlpvX8xsGA+bjKLxftm9403s9kQhKA=="], - "expo-document-picker": ["expo-document-picker@14.0.8", "", { "peerDependencies": { "expo": "*" } }, "sha512-3tyQKpPqWWFlI8p9RiMX1+T1Zge5mEKeBuXWp1h8PEItFMUDSiOJbQ112sfdC6Hxt8wSxreV9bCRl/NgBdt+fA=="], + "expo-linear-gradient": ["expo-linear-gradient@55.0.18", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-dlc3US7bFhGDLGvKTVt8tvs9KMEpZl6v9fE76pah5H48DyU0WxWD/ub3dvpDy94Py47rn14AptMn5kjGxjRfIw=="], - "expo-file-system": ["expo-file-system@19.0.24", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-hZs0Ng+coNjx7YJ8MPwfJpoU1SRul6OZ9LZycFzozUeoR+denbyr944YrMP9NVn+Zf1tdC7p0K0zgah3ZySOYg=="], + "expo-linking": ["expo-linking@55.0.17", "", { "dependencies": { "expo-constants": "~55.0.17", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-rc41rUbFYdJ4IdPxWQiTDtkOlEbtYQkvsCASzW1pqskfRShAd4tYyyAw1BTez3Cq1IgWkK3wcGR+HbFdzZ7ImQ=="], - "expo-font": ["expo-font@14.0.12", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-QQzunE2Mxk45AsCWm3tK7OpVljbtVnKD58q4/qliev+cbye1IOduUnRIdD+P7DyButw17G9MTX795kgaQiz5hQ=="], + "expo-localization": ["expo-localization@55.0.19", "", { "dependencies": { "rtl-detect": "^1.0.2" }, "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-rw8pcC4QL8HC3K8Vs5pdVCB+ayFkaVD0f+Ul+jHgM6W7XdzGtCzd0x7rI3M/9PzP1YpAgQ4jYZid/g+UKi9IIA=="], - "expo-keep-awake": ["expo-keep-awake@15.0.8", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-YK9M1VrnoH1vLJiQzChZgzDvVimVoriibiDIFLbQMpjYBnvyfUeHJcin/Gx1a+XgupNXy92EQJLgI/9ZuXajYQ=="], + "expo-location": ["expo-location@55.1.15", "", { "dependencies": { "@expo/image-utils": "^0.8.17" }, "peerDependencies": { "expo": "*" } }, "sha512-4wTDpVZg2/ZH4FnLkobY+PeIrfPz0uFTQqx7LIkxgviucNcRUPGstODBd+XRBOd8mIU1tN1vf6TSs8qEzOOJAw=="], - "expo-linking": ["expo-linking@8.0.12", "", { "dependencies": { "expo-constants": "~18.0.13", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-FpXeIpFgZuxihwT9lBo86YD3y6LphBuAhN680MMxm/Y7fmsc57vimn2d3vFu68VI0+Z9w457t494mu2wvlgWTQ=="], + "expo-modules-autolinking": ["expo-modules-autolinking@55.0.28", "", { "dependencies": { "@expo/require-utils": "^55.0.8", "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-kPdjouLw5QHofEIlpCxAyAcXTDXAL5xiz9QRVF8VUJhBqlekQ/9BSA9/0FafeJVt86gT0TkX45zG88AHp+gi0Q=="], - "expo-modules-autolinking": ["expo-modules-autolinking@3.0.27", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0" }, "bin": "bin/expo-modules-autolinking.js" }, "sha512-Ge216xmfXcba+aUQssfmUo9Hf92wXX3xcXJmOsC/wpuV5B5ozm4BTkRKpoCXEkdD8eJVmFy7ATS8yD3upFOPQA=="], + "expo-modules-core": ["expo-modules-core@55.0.27", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-worklets": "^0.7.4 || ^0.8.0" }, "optionalPeers": ["react-native-worklets"] }, "sha512-YaYxKyL3nSMbsWqFCdDIaOBVwNCtTpJAiX5losi74M8ArEt6P9cetFPSwVfcoocAzjjkvSwGi8xw/gSxLglD0g=="], - "expo-modules-core": ["expo-modules-core@3.0.30", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-a6IrpAn/Jbmwxi9L+hMmXKpNqnkUpoF7WHOpn02rVLyax2J0gB1vvCVE5rNydplEnt41Q6WxQwvcOjZaIkcSUg=="], + "expo-router": ["expo-router@55.0.18", "", { "dependencies": { "@expo/metro-runtime": "^55.0.12", "@expo/schema-utils": "^55.0.5", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.12", "@react-navigation/bottom-tabs": "^7.15.5", "@react-navigation/native": "^7.1.33", "@react-navigation/native-stack": "^7.14.5", "client-only": "^0.0.1", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "expo-glass-effect": "^55.0.11", "expo-image": "^55.0.11", "expo-server": "^55.0.12", "expo-symbols": "^55.0.9", "fast-deep-equal": "^3.1.3", "invariant": "^2.2.4", "nanoid": "^3.3.8", "query-string": "^7.1.3", "react-fast-compare": "^3.2.2", "react-native-is-edge-to-edge": "^1.2.1", "semver": "~7.6.3", "server-only": "^0.0.1", "sf-symbols-typescript": "^2.1.0", "shallowequal": "^1.1.0", "use-latest-callback": "^0.2.1", "vaul": "^1.1.2" }, "peerDependencies": { "@expo/log-box": "55.0.13", "@react-navigation/drawer": "^7.9.4", "@testing-library/react-native": ">= 13.2.0", "expo": "*", "expo-constants": "^55.0.17", "expo-linking": "^55.0.17", "react": "*", "react-dom": "*", "react-native": "*", "react-native-gesture-handler": "*", "react-native-reanimated": "*", "react-native-safe-area-context": ">= 5.4.0", "react-native-screens": "*", "react-native-web": "*", "react-server-dom-webpack": "~19.0.4 || ~19.1.5 || ~19.2.4" }, "optionalPeers": ["@react-navigation/drawer", "@testing-library/react-native", "react-dom", "react-native-gesture-handler", "react-native-reanimated", "react-native-web", "react-server-dom-webpack"] }, "sha512-KfGkHUgPsstqYD5kEyUtCKIMzPsKeuS+4C+xZ8sj8lEmL1Liy4JYuDoDSEhBVXWoQN82rYAqbBKgr9n+G1mk+g=="], - "expo-secure-store": ["expo-secure-store@15.0.8", "", { "peerDependencies": { "expo": "*" } }, "sha512-lHnzvRajBu4u+P99+0GEMijQMFCOYpWRO4dWsXSuMt77+THPIGjzNvVKrGSl6mMrLsfVaKL8BpwYZLGlgA+zAw=="], + "expo-secure-store": ["expo-secure-store@55.0.18", "", { "peerDependencies": { "expo": "*" } }, "sha512-shKZYQf7/KA45Z3oEIXUkXdrfRNFxsc+fJC182bbpuKqS4nLPZslsJUqruQ937Zkkbev126N/QJv+zmd6FpZjA=="], - "expo-server": ["expo-server@1.0.7", "", {}, "sha512-mcmyML3oXcqFUXUxtdtCL1O00ztNI2v76d+MdniXRUgHNxIcHZ05zo+DqBaOOT6LQnPk4vA4YHqQl7iGUfRb3g=="], + "expo-server": ["expo-server@55.0.12", "", {}, "sha512-yuNHbxobUKCXWn9Z/H+lgYk6heXEFub6q1h8Yo9kCLxgL2afluWZ/YylbtxgHgmFwDVUmi6milQYfL+wwQMlzw=="], - "expo-sharing": ["expo-sharing@14.0.8", "", { "peerDependencies": { "expo": "*" } }, "sha512-A1pPr2iBrxypFDCWVAESk532HK+db7MFXbvO2sCV9ienaFXAk7lIBm6bkqgE6vzRd9O3RGdEGzYx80cYlc089Q=="], + "expo-sharing": ["expo-sharing@55.0.25", "", { "dependencies": { "@expo/config-plugins": "^55.0.12", "@expo/config-types": "^55.0.6", "@expo/plist": "^0.5.4" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-DCbHXLbLChwwEc6vAyZkGVlzfJohXtcSR+Tf2qsczBtby0GS3cVqeBcH3UmJSG2ogJS18qLj2AYpn+iN/rpfNQ=="], - "expo-status-bar": ["expo-status-bar@3.0.9", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-xyYyVg6V1/SSOZWh4Ni3U129XHCnFHBTcUo0dhWtFDrZbNp/duw5AGsQfb2sVeU0gxWHXSY1+5F0jnKYC7WuOw=="], + "expo-splash-screen": ["expo-splash-screen@55.0.25", "", { "dependencies": { "@expo/prebuild-config": "^55.0.22" }, "peerDependencies": { "expo": "*" } }, "sha512-Jl3VPbNqbgGjXlkH93hi4Y4p6GB5DasLNbpL2Gb4kl8grujKaIGi4rXL25qYbYxIzefSlt96NqG9czanxxooPg=="], - "expo-web-browser": ["expo-web-browser@15.0.10", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-fvDhW4bhmXAeWFNFiInmsGCK83PAqAcQaFyp/3pE/jbdKmFKoRCWr46uZGIfN4msLK/OODhaQ/+US7GSJNDHJg=="], + "expo-status-bar": ["expo-status-bar@55.0.6", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-ijOUptfdiqYt7rObZ6jrPQ8sE5YN/8MxKCIJx0b7TY4nGkSJxhPIxeoW4GXcXCA8mTQ9PiOHH/ThLZgRVZvUlQ=="], + + "expo-symbols": ["expo-symbols@55.0.9", "", { "dependencies": { "@expo-google-fonts/material-symbols": "^0.4.1", "sf-symbols-typescript": "^2.0.0" }, "peerDependencies": { "expo": "*", "expo-font": "*", "react": "*", "react-native": "*" } }, "sha512-F85C/8ExQjd2gYjasLVKMT8wPj+1+19TVTqg4jAeVjVZklqiQtLO72io9Ji1xAjYNgmDeUI0diVHlFMMTC4Ekg=="], + + "expo-system-ui": ["expo-system-ui@55.0.22", "", { "dependencies": { "@react-native/normalize-colors": "0.83.10", "debug": "^4.3.2" }, "peerDependencies": { "expo": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-/ma8XGdwXvWkeZN++pHQt66jEQ4ZsaV9B5nP/+PAmXa4hAukPxG2bXjZaNOOOlTssWlaJRclJgGeC+Wo2uJlXQ=="], + + "expo-web-browser": ["expo-web-browser@55.0.20", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-masGrfbi/jqMezm/8cHHZ8chW+ifxumzt7QOCPiL+9aix0BSwuwDIBryEm2QPVmqJX51jckS0n245vWhwheQuQ=="], "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], @@ -1707,12 +1967,18 @@ "fastq": ["fastq@1.20.3", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw=="], + "fb-dotslash": ["fb-dotslash@0.5.8", "", { "bin": { "dotslash": "bin/dotslash" } }, "sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA=="], + "fb-watchman": ["fb-watchman@2.0.2", "", { "dependencies": { "bser": "2.1.1" } }, "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fetch-nodeshim": ["fetch-nodeshim@0.4.10", "", {}, "sha512-m6I8ALe4L4XpdETy7MJZWs6L1IVMbjs99bwbpIKphxX+0CTns4IKDWJY0LWfr4YsFjfg+z1TjzTMU8lKl8rG0w=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "filter-obj": ["filter-obj@1.1.0", "", {}, "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ=="], + "finalhandler": ["finalhandler@1.1.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "on-finished": "~2.3.0", "parseurl": "~1.3.3", "statuses": "~1.5.0", "unpipe": "~1.0.0" } }, "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA=="], "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], @@ -1721,9 +1987,9 @@ "fontfaceobserver": ["fontfaceobserver@2.3.0", "", {}, "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg=="], - "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], - "freeport-async": ["freeport-async@2.0.0", "", {}, "sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ=="], + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], @@ -1735,14 +2001,20 @@ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-east-asian-width": ["get-east-asian-width@1.7.0", "", {}, "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-monorepo-packages": ["get-monorepo-packages@1.3.0", "", { "dependencies": { "globby": "^7.1.1", "load-json-file": "^4.0.0" } }, "sha512-A/s881nNcKhoM7RgkvYFTOtGO+dy4EWbyRaatncPEhhlJAaZRlpfHwuT68p5GJenEt81nnjJOwGg0WKLkR5ZdQ=="], + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + "get-package-type": ["get-package-type@0.1.0", "", {}, "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], @@ -1763,13 +2035,21 @@ "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], - "hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="], + "hermes-compiler": ["hermes-compiler@0.14.1", "", {}, "sha512-+RPPQlayoZ9n6/KXKt5SFILWXCGJ/LV5d24L5smXrvTDrPS4L6dSctPczXauuvzFP3QEJbD1YO7Z3Ra4a+4IhA=="], + + "hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="], - "hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="], + "hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="], + + "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], "hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], @@ -1785,8 +2065,6 @@ "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - "image-size": ["image-size@1.2.1", "", { "dependencies": { "queue": "6.0.2" }, "bin": { "image-size": "bin/image-size.js" } }, "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw=="], - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], @@ -1795,15 +2073,17 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - "interpret": ["interpret@1.4.0", "", {}, "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA=="], "invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="], "invert-kv": ["invert-kv@3.0.1", "", {}, "sha512-CYdFeFexxhv/Bcny+Q0BfOV+ltRlJcd4BBZBYFX/O0u4npJrgZtIcjokegtiSMAvlMTJ+Koq0GBCc//3bueQxw=="], - "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + "is-arguments": ["is-arguments@1.2.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="], + + "is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], "is-core-module": ["is-core-module@2.17.0", "", { "dependencies": { "hasown": "^2.0.4" } }, "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA=="], @@ -1813,6 +2093,8 @@ "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], @@ -1821,8 +2103,12 @@ "is-plain-obj": ["is-plain-obj@2.1.0", "", {}, "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA=="], + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], "is-unsafe": ["is-unsafe@2.0.2", "", {}, "sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ=="], @@ -1947,6 +2233,8 @@ "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "mdn-data": ["mdn-data@2.0.14", "", {}, "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="], + "media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="], "mem": ["mem@4.3.0", "", { "dependencies": { "map-age-cleaner": "^0.1.1", "mimic-fn": "^2.0.0", "p-is-promise": "^2.0.0" } }, "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w=="], @@ -1959,52 +2247,50 @@ "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - "metro": ["metro@0.83.3", "", { "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "@babel/types": "^7.25.2", "accepts": "^1.3.7", "chalk": "^4.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.32.0", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.83.3", "metro-cache": "0.83.3", "metro-cache-key": "0.83.3", "metro-config": "0.83.3", "metro-core": "0.83.3", "metro-file-map": "0.83.3", "metro-resolver": "0.83.3", "metro-runtime": "0.83.3", "metro-source-map": "0.83.3", "metro-symbolicate": "0.83.3", "metro-transform-plugins": "0.83.3", "metro-transform-worker": "0.83.3", "mime-types": "^2.1.27", "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": "src/cli.js" }, "sha512-+rP+/GieOzkt97hSJ0MrPOuAH/jpaS21ZDvL9DJ35QYRDlQcwzcvUlGUf79AnQxq/2NPiS/AULhhM4TKutIt8Q=="], + "metro": ["metro@0.83.8", "", { "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.83.8", "metro-cache": "0.83.8", "metro-cache-key": "0.83.8", "metro-config": "0.83.8", "metro-core": "0.83.8", "metro-file-map": "0.83.8", "metro-resolver": "0.83.8", "metro-runtime": "0.83.8", "metro-source-map": "0.83.8", "metro-symbolicate": "0.83.8", "metro-transform-plugins": "0.83.8", "metro-transform-worker": "0.83.8", "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" } }, "sha512-ZbJDJCDlvv0O3QHVbhZkyP2/DtebfEoDZ3wVyYHgu6Uoy6DmmE/bis6lIDBYh+kWPuaueJDegyutPjkIBLtZDQ=="], - "metro-babel-transformer": ["metro-babel-transformer@0.83.3", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.32.0", "nullthrows": "^1.1.1" } }, "sha512-1vxlvj2yY24ES1O5RsSIvg4a4WeL7PFXgKOHvXTXiW0deLvQr28ExXj6LjwCCDZ4YZLhq6HddLpZnX4dEdSq5g=="], + "metro-babel-transformer": ["metro-babel-transformer@0.83.8", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.35.0", "metro-cache-key": "0.83.8", "nullthrows": "^1.1.1" } }, "sha512-tnn0J5wzgTgTx2OJy3Cwr1y79bJz4eNgFQd+2HENOs5Vz6QOMnt05z7J+BedIo9wIbpEa0iN9U1nerxyvMRE9g=="], - "metro-cache": ["metro-cache@0.83.3", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.3" } }, "sha512-3jo65X515mQJvKqK3vWRblxDEcgY55Sk3w4xa6LlfEXgQ9g1WgMh9m4qVZVwgcHoLy0a2HENTPCCX4Pk6s8c8Q=="], + "metro-cache": ["metro-cache@0.83.8", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.8" } }, "sha512-aogMG5WbKzW5000otNjYrS9hIoORzkCI1faPJK+vxQLaf2BorJKBBFe/jl2Tfsi1mZUglS2EBuBt8B3JB7MYDQ=="], - "metro-cache-key": ["metro-cache-key@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-59ZO049jKzSmvBmG/B5bZ6/dztP0ilp0o988nc6dpaDsU05Cl1c/lRf+yx8m9WW/JVgbmfO5MziBU559XjI5Zw=="], + "metro-cache-key": ["metro-cache-key@0.83.8", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-I38PtcjT4crS5HY9UQ8i6z8S7tJ2WewtPGr/OwS6FcLKfy5T/1hlTaFw+wozUZkEtNpR6Gc0oJuvuKCbSoSN5A=="], - "metro-config": ["metro-config@0.83.3", "", { "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.83.3", "metro-cache": "0.83.3", "metro-core": "0.83.3", "metro-runtime": "0.83.3", "yaml": "^2.6.1" } }, "sha512-mTel7ipT0yNjKILIan04bkJkuCzUUkm2SeEaTads8VfEecCh+ltXchdq6DovXJqzQAXuR2P9cxZB47Lg4klriA=="], + "metro-config": ["metro-config@0.83.8", "", { "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.83.8", "metro-cache": "0.83.8", "metro-core": "0.83.8", "metro-runtime": "0.83.8", "yaml": "^2.6.1" } }, "sha512-crNbNy+/B4tCne2+HjUshwvC57gNBQj+V9fFSy3lHH2RlKcLHib3Mil/SfTX8Pfh2fCY5pPuSu2OKa7YiadB+Q=="], - "metro-core": ["metro-core@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.83.3" } }, "sha512-M+X59lm7oBmJZamc96usuF1kusd5YimqG/q97g4Ac7slnJ3YiGglW5CsOlicTR5EWf8MQFxxjDoB6ytTqRe8Hw=="], + "metro-core": ["metro-core@0.83.8", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.83.8" } }, "sha512-NTyOUOQaQKvQgJG9VI2ymN6KTM7gHEqkVFhkPc9bK4BsHSTz/EaXuFBXtC+wtwINLeH9tbusL/jfsSmdEmuU7A=="], - "metro-file-map": ["metro-file-map@0.83.3", "", { "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" } }, "sha512-jg5AcyE0Q9Xbbu/4NAwwZkmQn7doJCKGW0SLeSJmzNB9Z24jBe0AL2PHNMy4eu0JiKtNWHz9IiONGZWq7hjVTA=="], + "metro-file-map": ["metro-file-map@0.83.8", "", { "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" } }, "sha512-+W++EUuzEXIfWQEFTWQMVThzhWbnJL4gRNJ9WSHzIAM5pT7gsprUxo9+2hrfirURm/TLvrKwhq37oCECJcDSyQ=="], - "metro-minify-terser": ["metro-minify-terser@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-O2BmfWj6FSfzBLrNCXt/rr2VYZdX5i6444QJU0fFoc7Ljg+Q+iqebwE3K0eTvkI6TRjELsXk1cjU+fXwAR4OjQ=="], + "metro-minify-terser": ["metro-minify-terser@0.83.8", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-7tU0J5/c7LZaZJwTlOb1xq0NepTFvGzRxigDhZOq4jSE6g0BRHmBqe7XvLH3WcRiJNGy+eshcZr34RpMjb6mmg=="], - "metro-resolver": ["metro-resolver@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-0js+zwI5flFxb1ktmR///bxHYg7OLpRpWZlBBruYG8OKYxeMP7SV0xQ/o/hUelrEMdK4LJzqVtHAhBm25LVfAQ=="], + "metro-resolver": ["metro-resolver@0.83.8", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-piU0NVTI9i37YztDVF5rtn9uxP3NebVT0xZM9NKJ9z0jbigrctUQksi3NoYIeJMvL6Wn2dgAehpcmmnfn+gUwA=="], - "metro-runtime": ["metro-runtime@0.83.3", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw=="], + "metro-runtime": ["metro-runtime@0.83.8", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-f7FfeM0pamq8vrvs8aO9KvIUabbUKe0WkHFpLt6Q9yIIIsORqNFwlgJeHGraOFPU7Cxqj5yLXkJu5bT1uwDXvw=="], - "metro-source-map": ["metro-source-map@0.83.3", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.3", "nullthrows": "^1.1.1", "ob1": "0.83.3", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg=="], + "metro-source-map": ["metro-source-map@0.83.8", "", { "dependencies": { "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.8", "nullthrows": "^1.1.1", "ob1": "0.83.8", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-60Uor7bM+KsVewLkLCcZfkPFCbqjPDdoSmeBuT3+ye+ac80BuLWbbR8DpyxWpUqQeZPdaAT5ZqFgDIs8BNEcVA=="], - "metro-symbolicate": ["metro-symbolicate@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.3", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": "src/index.js" }, "sha512-F/YChgKd6KbFK3eUR5HdUsfBqVsanf5lNTwFd4Ca7uuxnHgBC3kR/Hba/RGkenR3pZaGNp5Bu9ZqqP52Wyhomw=="], + "metro-symbolicate": ["metro-symbolicate@0.83.8", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.8", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-ZLrbqOqQ+m+Mpv7zo65XEXkNqVYvgUbY7tIsb9e1zSSmU/4C3D6DjaJvRHqcAdl0mkg8TI/csKooAEa5xcZNwg=="], - "metro-transform-plugins": ["metro-transform-plugins@0.83.3", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-eRGoKJU6jmqOakBMH5kUB7VitEWiNrDzBHpYbkBXW7C5fUGeOd2CyqrosEzbMK5VMiZYyOcNFEphvxk3OXey2A=="], + "metro-transform-plugins": ["metro-transform-plugins@0.83.8", "", { "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" } }, "sha512-9JRPkvi+m0QH2Y/w5RjCF9mHqOUNFpMFLDDLhKUjhcKESh8Wm3HKDdHXHVldTN1lTToCIabv81nF0zyJzKEJ5g=="], - "metro-transform-worker": ["metro-transform-worker@0.83.3", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "metro": "0.83.3", "metro-babel-transformer": "0.83.3", "metro-cache": "0.83.3", "metro-cache-key": "0.83.3", "metro-minify-terser": "0.83.3", "metro-source-map": "0.83.3", "metro-transform-plugins": "0.83.3", "nullthrows": "^1.1.1" } }, "sha512-Ztekew9t/gOIMZX1tvJOgX7KlSLL5kWykl0Iwu2cL2vKMKVALRl1hysyhUw0vjpAvLFx+Kfq9VLjnHIkW32fPA=="], + "metro-transform-worker": ["metro-transform-worker@0.83.8", "", { "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.83.8", "metro-babel-transformer": "0.83.8", "metro-cache": "0.83.8", "metro-cache-key": "0.83.8", "metro-minify-terser": "0.83.8", "metro-source-map": "0.83.8", "metro-transform-plugins": "0.83.8", "nullthrows": "^1.1.1" } }, "sha512-Pa2hOfhUmWpI/dmkhsLq8uGyFHK2opoEK7j/YCiRtqNSe0YzzxYguXTNgg8AMiuZfc9LPcB1AhGENS10O5wcIw=="], "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], "mime": ["mime@2.6.0", "", { "bin": "cli.js" }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], - "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - "minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + "minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], - "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], - "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": "bin/cmd.js" }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -2013,18 +2299,16 @@ "mustache": ["mustache@4.2.0", "", { "bin": "bin/mustache" }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], - "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], - "nanoid": ["nanoid@3.3.20", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ=="], "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], - "nested-error-stacks": ["nested-error-stacks@2.0.1", "", {}, "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A=="], - "nice-try": ["nice-try@1.0.5", "", {}, "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="], "nocache": ["nocache@3.0.4", "", {}, "sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw=="], + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + "node-forge": ["node-forge@1.4.0", "", {}, "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ=="], "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], @@ -2041,11 +2325,11 @@ "npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], - "nullthrows": ["nullthrows@1.1.1", "", {}, "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="], + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], - "ob1": ["ob1@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA=="], + "nullthrows": ["nullthrows@1.1.1", "", {}, "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="], - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "ob1": ["ob1@0.83.8", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-pk7el+eTOzfSKMAY4QBiiwKzegXn633JQj13y+pW5E5IdS+yV2CfDJ9Hf20K/LKy8nCrP9dAmd7XOk52OJxifA=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], @@ -2115,14 +2399,16 @@ "pngjs": ["pngjs@3.4.0", "", {}, "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w=="], - "postcss": ["postcss@8.5.31", "", { "dependencies": { "nanoid": "^3.3.20", "picocolors": "^1.1.1", "source-map-js": "^1.2.2" } }, "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg=="], + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - "pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="], + "postcss": ["postcss@8.5.31", "", { "dependencies": { "nanoid": "^3.3.20", "picocolors": "^1.1.1", "source-map-js": "^1.2.2" } }, "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg=="], "pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], "proc-log": ["proc-log@4.2.0", "", {}, "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA=="], + "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], + "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], "promise": ["promise@8.3.0", "", { "dependencies": { "asap": "~2.0.6" } }, "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg=="], @@ -2155,15 +2441,13 @@ "prosemirror-view": ["prosemirror-view@1.42.6", "", { "dependencies": { "prosemirror-model": "^1.25.12", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "sha512-3g25f6hKV2bBxo3puH3EcYWmmVNSH0iWmNsMVT7sugDSpA025jgpOLarcDWr39SdzPjYMaiYtRdNtXialPSjEQ=="], - "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], - - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], - "qrcode-terminal": ["qrcode-terminal@0.11.0", "", { "bin": "bin/qrcode-terminal.js" }, "sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ=="], + "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], "qs": ["qs@6.16.0", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA=="], - "queue": ["queue@6.0.2", "", { "dependencies": { "inherits": "~2.0.3" } }, "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA=="], + "query-string": ["query-string@7.1.3", "", { "dependencies": { "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" } }, "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], @@ -2171,44 +2455,112 @@ "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": "cli.js" }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], - - "react": ["react@19.1.4", "", {}, "sha512-DHINL3PAmPUiK1uszfbKiXqfE03eszdt5BpVSuEAHb5nfmNPwnsy7g39h2t8aXFc/Bv99GH81s+j8dobtD+jOw=="], + "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], "react-devtools-core": ["react-devtools-core@6.1.5", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA=="], - "react-dom": ["react-dom@19.1.4", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.4" } }, "sha512-s2868ab/xo2SI6H4106A7aFI8Mrqa4xC6HZT/pBzYyQ3cBLqa88hu47xYD8xf+uECleN698Awn7RCWlkTiKnqQ=="], + "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], + + "react-fast-compare": ["react-fast-compare@3.2.2", "", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="], + + "react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="], "react-is": ["react-is@19.3.0", "", {}, "sha512-UpMYezM4v5/18F28aC66AEsjXIgE02kyEMH6yLdgLXu/UTfa1Ntwck/nNLrbqJsEXW7gPb0coNO9FQse9WTovA=="], - "react-native": ["react-native@0.81.6", "", { "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.81.6", "@react-native/codegen": "0.81.6", "@react-native/community-cli-plugin": "0.81.6", "@react-native/gradle-plugin": "0.81.6", "@react-native/js-polyfills": "0.81.6", "@react-native/normalize-colors": "0.81.6", "@react-native/virtualized-lists": "0.81.6", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "0.29.1", "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "invariant": "^2.2.4", "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.83.1", "metro-source-map": "^0.83.1", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.26.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^6.2.3", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^19.1.4", "react": "^19.1.4" }, "bin": "cli.js" }, "sha512-X/tI8GqfzVaa+zfbE4+lySNN5UzwBIAVRHVZPKymOny9Acc5GYYcjAcEVfG3AM4h920YALWoSl8favnDmQEWIg=="], + "react-native": ["react-native@0.83.10", "", { "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.83.10", "@react-native/codegen": "0.83.10", "@react-native/community-cli-plugin": "0.83.10", "@react-native/gradle-plugin": "0.83.10", "@react-native/js-polyfills": "0.83.10", "@react-native/normalize-colors": "0.83.10", "@react-native/virtualized-lists": "0.83.10", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "0.32.0", "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "hermes-compiler": "0.14.1", "invariant": "^2.2.4", "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.83.6", "metro-source-map": "^0.83.6", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.27.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^19.1.1", "react": "^19.2.0" }, "optionalPeers": ["@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-4gbmtAyfC0F4jU2hl/l/HZbRIfRl0d5RdHNpJLf7sT/0zhYDEhiFcFk7ii2utl7O8BIHRUmGe/anfWP3DNPTZw=="], + + "react-native-app-logs": ["react-native-app-logs@0.3.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-Nyxwx92vJRTIBdIRiHlFrz0IFJq+rKIsq6XFRiYT7ZBA1jQIPBYX/kx9kCRwqLDGnSBzec2oYwPScGEllRpNCw=="], + + "react-native-blurhash": ["react-native-blurhash@2.1.3", "", { "peerDependencies": { "react": ">=16.8.1", "react-native": ">=0.60.0-rc.0 <1.0.x" } }, "sha512-tYyFketZkrrEIABJ5Z1Pt6N2dqPqtSp7w4Lce17sx1bvspdppgVu/ehtZoPRZu02fYyX92RkyCeGXtCbtUNntg=="], + + "react-native-drawer-layout": ["react-native-drawer-layout@4.2.12", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*", "react-native-gesture-handler": ">= 2.0.0", "react-native-reanimated": ">= 2.0.0" } }, "sha512-qYmntEctkMSodN7pnC2llV21nswImLFPHJeT3PJXSJqZdU4jzDATR8iVi66sXkrtoOLJ2+EvXxgd7sMMpV4ViA=="], "react-native-enriched-html": ["react-native-enriched-html@1.1.1", "", { "dependencies": { "@tiptap/core": "3.27.1", "@tiptap/extension-blockquote": "3.27.1", "@tiptap/extension-bold": "3.27.1", "@tiptap/extension-code": "3.27.1", "@tiptap/extension-document": "3.27.1", "@tiptap/extension-heading": "3.27.1", "@tiptap/extension-history": "3.27.1", "@tiptap/extension-image": "3.27.1", "@tiptap/extension-italic": "3.27.1", "@tiptap/extension-link": "3.27.1", "@tiptap/extension-list": "3.27.1", "@tiptap/extension-paragraph": "3.27.1", "@tiptap/extension-strike": "3.27.1", "@tiptap/extension-text": "3.27.1", "@tiptap/extension-text-align": "3.27.1", "@tiptap/extension-underline": "3.27.1", "@tiptap/extensions": "3.27.1", "@tiptap/pm": "3.27.1", "@tiptap/react": "3.27.1", "dompurify": "3.4.11" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-OlJLESOgA4eqgWQe3BXOK9kFk7iypOnQo22sIce5k/x2eGsOtnAwDPqHnaVfnOZlzzNWh+wXRzxuSL1tSBOkgA=="], "react-native-enriched-markdown": ["react-native-enriched-markdown@1.1.1", "", { "peerDependencies": { "katex": ">=0.16.0", "react": "*", "react-native": "*" }, "optionalPeers": ["katex"] }, "sha512-JvbRIVy75MAoGCv8V6sSscu11/Ian588wfIgH+j64XFpf6gy5sNNkMyG087MH88jNCf3p7fTjgEAGxZMhEqL4A=="], + "react-native-fast-image": ["react-native-fast-image@10.9.9", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-Dev69p1kcTxx7qLLFwax5hB/3X5wMNpviUIXS6K6KnITyhu+O2AEUhenZoq2OmGnT9UzHBIYHkEKoFebZNK/lA=="], + + "react-native-gesture-handler": ["react-native-gesture-handler@2.30.0", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-5YsnKHGa0X9C8lb5oCnKm0fLUPM6CRduvUUw2Bav4RIj/C3HcFh4RIUnF8wgG6JQWCL1//gRx4v+LVWgcIQdGA=="], + + "react-native-graph": ["react-native-graph@1.4.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-reanimated": "*" } }, "sha512-Mh0P3kXWrbg9WxPAgce3a+XzKSE36sE9lSi+VwXw/5Ps1VChsa4eApI8LHqpcdvlWH7qjCNk5cSdFkGDuA8Kgw=="], + "react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.3.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA=="], - "react-native-macos": ["react-native-macos@0.81.7", "", { "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native-macos/virtualized-lists": "0.81.7", "@react-native/assets-registry": "0.81.6", "@react-native/codegen": "0.81.6", "@react-native/community-cli-plugin": "0.81.6", "@react-native/gradle-plugin": "0.81.6", "@react-native/js-polyfills": "0.81.6", "@react-native/normalize-colors": "0.81.6", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "0.29.1", "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "invariant": "^2.2.4", "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.83.1", "metro-source-map": "^0.83.1", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.26.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^6.2.3", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^19.1.4", "react": "^19.1.4", "react-native": "0.81.6" }, "bin": { "react-native": "cli.js", "react-native-macos": "cli.js" } }, "sha512-wV7jyw4aP7dD5h5Ik/+17hWC56xZQ26YhGs7okW7bAUCZmDKgCFcY5H6Vb2Wkr2yIl0oQIbatSOCPGAlHPk38A=="], + "react-native-keyboard-controller": ["react-native-keyboard-controller@1.22.9", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.2.1" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-reanimated": ">=3.0.0" } }, "sha512-nLoXvTbIy/9cI4p/bAXUP7Vfi6Mf2Q7g/gsh2H29C7hgUSQQLdoQjl8inhKC25oxaEibB0CMR++h3HP/k/+BVw=="], + + "react-native-macos": ["react-native-macos@0.83.0", "", { "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native-macos/virtualized-lists": "0.83.0", "@react-native/assets-registry": "0.83.10", "@react-native/codegen": "0.83.10", "@react-native/community-cli-plugin": "0.83.10", "@react-native/gradle-plugin": "0.83.10", "@react-native/js-polyfills": "0.83.10", "@react-native/normalize-colors": "0.83.10", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "0.32.0", "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "hermes-compiler": "0.14.1", "invariant": "^2.2.4", "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", "metro": "^0.83.6", "metro-runtime": "^0.83.6", "metro-source-map": "^0.83.6", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.27.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^19.1.1", "react": "^19.2.0", "react-native": "0.83.10" }, "optionalPeers": ["@types/react"], "bin": { "react-native": "cli.js", "react-native-macos": "cli.js" } }, "sha512-8hRb3b1QuKNC0xoHOq3BEErlsjrgJACyxXL5ny7dUr5zGxBCle5Tu9gPysCCITp1iyIINLEApHB/NkoRaKsU7Q=="], + + "react-native-mmkv": ["react-native-mmkv@4.3.2", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": "*" } }, "sha512-49OAyfkg0/TMWiWELZN6VuVQPZPhizwL4DTmp8b7B1md3dB/s3LH3mGfC3T+lp9W0y/rqxZMEnotLFTIbOAenQ=="], + + "react-native-nitro-fetch": ["react-native-nitro-fetch@1.9.0", "", { "dependencies": { "web-streams-polyfill": "^4.2.0" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": ">=0.37.1", "react-native-worklets": ">=0.8.0" }, "optionalPeers": ["react-native-worklets"] }, "sha512-4rxaPcXKudZW1Ma1Y63rHF8/7hbanOyqr01Gac6ROUYApcCH8/YtS0d02cm0ZEbSzwPvmcvFRzPoT4cHWxhJGA=="], "react-native-nitro-image": ["react-native-nitro-image@0.15.2", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": "*" }, "optionalPeers": ["react-native-nitro-modules"] }, "sha512-WiaGnORmfNIIqtRJW76TWYNet1YZx1MWDlSOxd0jfcIBN4KjkVVMDVpvO01eCfpxKVG0ez9nA2iLQkPVlGFj+A=="], "react-native-nitro-modules": ["react-native-nitro-modules@0.37.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-ULS2CBZcdwGw5d4Y35EV/tbCOsNsQOnQ9LC9CehMhDNm5c90lwF/LxRamzKuFpjI547G7teTQgD0peqYSGnP9g=="], + "react-native-nitro-sqlite": ["react-native-nitro-sqlite@10.2.0", "", { "dependencies": { "typeorm": "1.1.1" }, "peerDependencies": { "better-sqlite3": "^12.11.1", "react": ">=17.0.0", "react-native": ">=0.75.0", "react-native-nitro-modules": ">=0.35.0" }, "optionalPeers": ["better-sqlite3"] }, "sha512-lB0yc4SzVNTZoe0ItyBJqOQ9qAc2SY3ku7PzFwP97e4NpZIOX0ec7p49sLK04MdWRmInt96Q7NU4rPfbNyg7Nw=="], + + "react-native-nitro-zxing": ["react-native-nitro-zxing@0.3.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-image": "*", "react-native-nitro-modules": "*", "react-native-vision-camera": "*" }, "optionalPeers": ["react-native-nitro-modules"] }, "sha512-SQ/qwmfakdPcOMVzph99vFOPbPkVg3JZaTGrjVVXG6RS8ZUqMocHqbeM4V17qPDTzFK+rsAp6T7u2aFe3Hot/w=="], + + "react-native-pressable-opacity": ["react-native-pressable-opacity@1.0.10", "", { "peerDependencies": { "react": "*", "react-native": ">= 0.63", "react-native-gesture-handler": "*", "react-native-reanimated": ">= 2" } }, "sha512-Py9YH9TlS3Lv1so5JCj6bgiqkeYYGupF4ZImlpoyhhId/t/RiSqR68LlASOHgdctqQuqVJObQiFfzX8oZI9+6w=="], + + "react-native-quick-base64": ["react-native-quick-base64@3.0.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-EjUP2U7WqKmlMmoY7XGyHomy8bM0q4+yCDCRg4ZezQ6zedYRwc7yVk4V2O/iSftKaLEzhuW98lpyPMdk1iPHXQ=="], + + "react-native-quick-crypto": ["react-native-quick-crypto@1.1.9", "", { "dependencies": { "@craftzdog/react-native-buffer": "^6.1.2", "events": "3.3.0", "readable-stream": "4.7.0", "safe-buffer": "^5.2.1", "string_decoder": "^1.3.0", "util": "0.12.5" }, "peerDependencies": { "expo": ">=48.0.0", "react": "*", "react-native": "*", "react-native-nitro-modules": ">=0.31.2", "react-native-quick-base64": ">=3.0.0" }, "optionalPeers": ["expo"] }, "sha512-u60Kr4EdAYI1ksGq7uwJfOXNJR/iWkCQ1AxfoVbjM/pEpiydddzsFXFYqwyv7EeB7eTDgg5/LiCGwNxA7W7NKA=="], + + "react-native-reanimated": ["react-native-reanimated@4.2.1", "", { "dependencies": { "react-native-is-edge-to-edge": "1.2.1", "semver": "7.7.3" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-worklets": ">=0.7.0" } }, "sha512-/NcHnZMyOvsD/wYXug/YqSKw90P9edN0kEPL5lP4PFf1aQ4F1V7MKe/E0tvfkXKIajy3Qocp5EiEnlcrK/+BZg=="], + + "react-native-release-profiler": ["react-native-release-profiler@0.4.5", "", { "dependencies": { "@margelo/hermes-profile-transformer": "^0.0.10", "commander": "^11.1.0" }, "peerDependencies": { "@react-native-community/cli-tools": "*", "react": "*", "react-native": "*" }, "bin": { "react-native-release-profiler": "lib/commonjs/cli.js" } }, "sha512-1w05jDlKRdIMxTuSPep67eMc+FW805SJZjWJwBVsPIQHmRYttxBdrdbzF0mG5XVvMVRNji4RHLidu54+V54lRQ=="], + + "react-native-safe-area-context": ["react-native-safe-area-context@5.6.2", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg=="], + + "react-native-screen-corner-radius": ["react-native-screen-corner-radius@0.2.3", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-8JgiHI/S2Xf6swquEjopHW//wI4a/nV/dEImJMR8YXQVWVdnjbhcMi0NGk1s1cW8xuunwlFT173PozsS3Gvgxg=="], + + "react-native-screens": ["react-native-screens@4.23.0", "", { "dependencies": { "react-freeze": "^1.0.0", "warn-once": "^0.1.0" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-XhO3aK0UeLpBn4kLecd+J+EDeRRJlI/Ro9Fze06vo1q163VeYtzfU9QS09/VyDFMWR1qxDC1iazCArTPSFFiPw=="], + + "react-native-skia-android": ["react-native-skia-android@154.0.0", "", {}, "sha512-brjJaJcrYguSOwdbvMBlh+tHJdIvzOMUZSjTmIpiIe4/V9x0HsBZM9sy3q32+WGoSPWibylR7hbDmaO12EYAUA=="], + + "react-native-skia-apple-ios": ["react-native-skia-apple-ios@154.0.0", "", {}, "sha512-7xq2XYEbcfQYcqyXEnTDXF9GjQzKHuDiUep6wOvOXzoK62108DR8dM3VYgOqJqCTqspPHC9fEJeT52fK3UU5Qg=="], + + "react-native-skia-apple-macos": ["react-native-skia-apple-macos@154.0.0", "", {}, "sha512-0BE6/oVMXYOKsxI4JJP1MwwrgmaDnZAl9l6N+7q434MFiT+w0z96a0gTilZukzo1y+35sDi/sA+iAdTz16ElYA=="], + + "react-native-skia-apple-tvos": ["react-native-skia-apple-tvos@154.0.0", "", {}, "sha512-W06tUa+O3TJTYg5G+Lly/pwxE+2zKsmwJ/23yeR+25zSu7VmfdSz3VqFN3BM0jEioN4ASMm/0cMECocV7vxbMQ=="], + + "react-native-svg": ["react-native-svg@15.15.3", "", { "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", "warn-once": "0.1.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-/k4KYwPBLGcx2f5d4FjE+vCScK7QOX14cl2lIASJ28u4slHHtIhL0SZKU7u9qmRBHxTCKPoPBtN6haT1NENJNA=="], + + "react-native-tracking-transparency": ["react-native-tracking-transparency@0.1.2", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-oe+BFutdq+fnTgyC9pAm/NEz6nuYkEHpFQUU8swj+E5oOOPqGmrNXmseNatKob521Zh7w0ovxXkoQNTV+PKTFA=="], + + "react-native-unistyles": ["react-native-unistyles@3.6.0", "", { "dependencies": { "@babel/types": "7.29.8" }, "peerDependencies": { "@react-native/normalize-colors": "*", "react": "*", "react-native": ">=0.81.0", "react-native-edge-to-edge": "*", "react-native-nitro-modules": "*", "react-native-reanimated": "*" }, "optionalPeers": ["react-native-edge-to-edge", "react-native-reanimated"] }, "sha512-eYqixPy6auBj6k/ica9fCYIPgFQsLzUwRoxGZ04hX3hkANTNjaz3/LzNfXPfgJbwF/fQrhaK0qII4nhtSqUPfA=="], + "react-native-vision-camera": ["react-native-vision-camera@5.2.3", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-image": "*", "react-native-nitro-modules": "*" }, "optionalPeers": ["react-native-nitro-modules"] }, "sha512-OHjgM8BPzxppTqbF1FEltj+fxvisj4xdghM7ypoT1p6cOe28HByhOJP1c7LkBiUiZuotbsdbQwGdJ8nU7ZvPzA=="], "react-native-webview": ["react-native-webview@16.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0", "invariant": "2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-KIg5Yhy04lQfHdXC1JQPM+0kftMfPnH6AqDH7v6YskQ9kzfUacooofOFRmbaZHASq/0WY87Fx0peiFETFjOcCg=="], - "react-native-windows": ["react-native-windows@0.81.35", "", { "dependencies": { "@babel/runtime": "^7.0.0", "@jest/create-cache-key-function": "^29.7.0", "@react-native-community/cli": "17.0.0", "@react-native-community/cli-platform-android": "17.0.0", "@react-native-community/cli-platform-ios": "17.0.0", "@react-native-windows/cli": "0.81.9", "@react-native/assets": "1.0.0", "@react-native/assets-registry": "0.81.6", "@react-native/codegen": "0.81.6", "@react-native/community-cli-plugin": "0.81.6", "@react-native/gradle-plugin": "0.81.6", "@react-native/js-polyfills": "0.81.6", "@react-native/new-app-screen": "0.81.5", "@react-native/normalize-colors": "0.81.6", "@react-native/virtualized-lists": "0.81.6", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "0.29.1", "base64-js": "^1.5.1", "chalk": "^4.0.0", "commander": "^12.0.0", "event-target-shim": "^5.0.1", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "invariant": "^2.2.4", "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.83.1", "metro-source-map": "^0.83.1", "mkdirp": "^0.5.1", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.26.0", "semver": "^7.1.3", "source-map-support": "^0.5.19", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^6.2.3", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^19.1.4", "react": "^19.1.4", "react-native": "^0.81.0" } }, "sha512-hCY4jnrLYEzXMZ3KFfWPOEinY8hLm/XUKotGTjXcokU2AvPsWtpYE2dxbMOpz2vqjNvV0+jqeKYS5M0u/Mc76w=="], + "react-native-windows": ["react-native-windows@0.83.2", "", { "dependencies": { "@babel/runtime": "^7.0.0", "@jest/create-cache-key-function": "^29.7.0", "@react-native-community/cli": "20.0.0", "@react-native-community/cli-platform-android": "20.0.0", "@react-native-community/cli-platform-ios": "20.0.0", "@react-native-windows/cli": "0.83.2", "@react-native/assets": "1.0.0", "@react-native/assets-registry": "0.83.4", "@react-native/codegen": "0.83.4", "@react-native/community-cli-plugin": "0.83.4", "@react-native/gradle-plugin": "0.83.4", "@react-native/js-polyfills": "0.83.4", "@react-native/new-app-screen": "0.83.4", "@react-native/normalize-colors": "0.83.4", "@react-native/virtualized-lists": "0.83.4", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "0.32.0", "base64-js": "^1.5.1", "chalk": "^4.0.0", "commander": "^12.0.0", "event-target-shim": "^5.0.1", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "hermes-compiler": "0.14.1", "invariant": "^2.2.4", "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.83.3", "metro-source-map": "^0.83.3", "mkdirp": "^0.5.1", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.27.0", "semver": "^7.1.3", "source-map-support": "^0.5.19", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^19.1.1", "react": "^19.2.0", "react-native": "^0.83.0" } }, "sha512-Zm/oS/YUK19Ba1G814HD+sSmmEMR/gQuhVb/TCpM+fqlqAKukF/XyNVoC/8CFboGCfj1+7AvKW267p9jiyKnbw=="], + + "react-native-worklets": ["react-native-worklets@0.7.4", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "7.27.1", "@babel/plugin-transform-class-properties": "7.27.1", "@babel/plugin-transform-classes": "7.28.4", "@babel/plugin-transform-nullish-coalescing-operator": "7.27.1", "@babel/plugin-transform-optional-chaining": "7.27.1", "@babel/plugin-transform-shorthand-properties": "7.27.1", "@babel/plugin-transform-template-literals": "7.27.1", "@babel/plugin-transform-unicode-regex": "7.27.1", "@babel/preset-typescript": "7.27.1", "convert-source-map": "2.0.0", "semver": "7.7.3" }, "peerDependencies": { "@babel/core": "*", "react": "*", "react-native": "*" } }, "sha512-NYOdM1MwBb3n+AtMqy1tFy3Mn8DliQtd8sbzAVRf9Gc+uvQ0zRfxN7dS8ZzoyX7t6cyQL5THuGhlnX+iFlQTag=="], + + "react-native-worklets-core": ["react-native-worklets-core@1.6.3", "", { "dependencies": { "string-hash-64": "^1.0.3" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-r3Q40XQBccx/iAI5tlyiua+micvO1UGzzUOskNweZUXyfrrE+rb5aqxqruBPqXf90rO+bBiplylLMEAXCLTyGA=="], + + "react-reconciler": ["react-reconciler@0.31.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ=="], "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], - "react-test-renderer": ["react-test-renderer@19.1.4", "", { "dependencies": { "react-is": "^19.1.4", "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.4" } }, "sha512-dmcR2lSwouaNJKNRd53eeZHuWjF4YYZIq/q6wLiVz50ENrkU/Pds+pS+4XyJ/nwWmjA1ZhHxAp+vfR8A9PEcYQ=="], + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + + "react-test-renderer": ["react-test-renderer@19.2.0", "", { "dependencies": { "react-is": "^19.2.0", "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-zLCFMHFE9vy/w3AxO0zNxy6aAupnCuLSVOJYDe/Tp+ayGI1f2PLQsFVPANSD42gdSbmYx5oN+1VWDhcXtq7hAQ=="], - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], "rechoir": ["rechoir@0.6.2", "", { "dependencies": { "resolve": "^1.1.6" } }, "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw=="], + "reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="], + "regenerate": ["regenerate@1.4.2", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="], "regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], @@ -2227,16 +2579,12 @@ "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], - "requireg": ["requireg@0.2.2", "", { "dependencies": { "nested-error-stacks": "~2.0.1", "rc": "~1.2.7", "resolve": "~1.7.1" } }, "sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg=="], - "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], "resolve-workspace-root": ["resolve-workspace-root@2.0.1", "", {}, "sha512-nR23LHAvaI6aHtMg6RWoaHpdR4D881Nydkzi2CixINyg9T00KgaJdJI6Vwty+Ps8WLxZHuxsS0BseWjxSA4C+w=="], - "resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="], - "restore-cursor": ["restore-cursor@2.0.0", "", { "dependencies": { "onetime": "^2.0.0", "signal-exit": "^3.0.2" } }, "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q=="], "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], @@ -2247,15 +2595,19 @@ "rope-sequence": ["rope-sequence@1.3.4", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="], + "rtl-detect": ["rtl-detect@1.1.2", "", {}, "sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="], - "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "semver": ["semver@7.8.5", "", { "bin": "bin/semver.js" }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], @@ -2265,12 +2617,18 @@ "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], + "server-only": ["server-only@0.0.1", "", {}, "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA=="], + "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], "sf-symbols-typescript": ["sf-symbols-typescript@2.2.0", "", {}, "sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw=="], + "shallowequal": ["shallowequal@1.1.0", "", {}, "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], @@ -2293,6 +2651,8 @@ "simple-plist": ["simple-plist@1.3.1", "", { "dependencies": { "bplist-creator": "0.1.0", "bplist-parser": "0.3.1", "plist": "^3.0.5" } }, "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw=="], + "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], @@ -2307,8 +2667,12 @@ "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "split-on-first": ["split-on-first@1.1.0", "", {}, "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="], + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + "sql-highlight": ["sql-highlight@6.1.0", "", {}, "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA=="], + "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], @@ -2317,14 +2681,20 @@ "stacktrace-parser": ["stacktrace-parser@0.1.11", "", { "dependencies": { "type-fest": "^0.7.1" } }, "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg=="], + "standard-navigation": ["standard-navigation@0.0.8", "", { "peerDependencies": { "react": "*" } }, "sha512-TyVbo7INUDWtsUWDFn8RR7kwR87U0S4xHfLfbbnyeC581TmmyqQ+eM+nPw8rQTSD8QitRVcYfPaSHr/QJiUy1g=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="], "stream-buffers": ["stream-buffers@2.2.0", "", {}, "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg=="], + "strict-uri-encode": ["strict-uri-encode@2.0.0", "", {}, "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="], + "strict-url-sanitise": ["strict-url-sanitise@0.0.1", "", {}, "sha512-nuFtF539K8jZg3FjaWH/L8eocCR6gegz5RDOsaWxfdbF5Jqr2VXWxZayjTwUzsWJDC91k2EbnJXp6FuWW+Z4hg=="], + "string-hash-64": ["string-hash-64@1.0.3", "", {}, "sha512-D5OKWKvDhyVWWn2x5Y9b+37NUllks34q1dCDhk/vYcso9fmhs+Tl3KR/gE4v5UNj2UA35cnX4KdVVGkG1deKqw=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -2347,8 +2717,6 @@ "structured-headers": ["structured-headers@0.4.1", "", {}, "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg=="], - "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-hyperlinks": ["supports-hyperlinks@2.3.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA=="], @@ -2361,18 +2729,12 @@ "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], - "tar": ["tar@7.5.22", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA=="], - "terminal-link": ["terminal-link@2.1.1", "", { "dependencies": { "ansi-escapes": "^4.2.1", "supports-hyperlinks": "^2.0.0" } }, "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ=="], "terser": ["terser@5.51.2", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": "bin/terser" }, "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q=="], "test-exclude": ["test-exclude@6.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" } }, "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w=="], - "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], - - "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], - "throat": ["throat@5.0.0", "", {}, "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA=="], "tinybench": ["tinybench@6.1.4", "", {}, "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ=="], @@ -2387,7 +2749,9 @@ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + "toqr": ["toqr@0.1.1", "", {}, "sha512-FWAPzCIHZHnrE/5/w9MPk0kK25hSQSH2IKhYh9PyjS3SG/+IEMvlwIHbhz+oF7xl54I+ueZlVnMjyzdSwLmAwA=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -2397,8 +2761,12 @@ "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], + "typeorm": ["typeorm@1.1.1", "", { "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^4.3.1", "dayjs": "^1.11.21", "debug": "^4.4.3", "dedent": "^1.7.2", "reflect-metadata": "^0.2.2", "sql-highlight": "^6.1.0", "tinyglobby": "^0.2.17", "tslib": "^2.8.1", "yargs": "^18.0.0" }, "peerDependencies": { "@google-cloud/spanner": "^8.0.0", "@sap/hana-client": "^2.14.22", "better-sqlite3": "^12.0.0", "ioredis": "^5.0.4", "mongodb": "^7.0.0", "mssql": "^12.0.0", "mysql2": "^3.15.3", "oracledb": "^6.3.0 || ^7.0.0", "pg": "^8.5.1", "pg-native": "^3.0.0", "pg-query-stream": "^4.0.0", "redis": "^5.0.0 || ^6.0.0", "sql.js": "^1.4.0", "ts-node": "^10.9.2", "typeorm-aurora-data-api-driver": "^3.0.0" }, "optionalPeers": ["@google-cloud/spanner", "@sap/hana-client", "better-sqlite3", "ioredis", "mongodb", "mssql", "mysql2", "oracledb", "pg", "pg-native", "pg-query-stream", "redis", "sql.js", "ts-node", "typeorm-aurora-data-api-driver"], "bin": { "typeorm": "cli.js", "typeorm-ts-node-esm": "cli-ts-node-esm.js", "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js" } }, "sha512-og9mG4Lwlvj6MfvYd8yRCzXy73gETtwgTKQ/ISLYvEOpJPy4IrjU/89HK4ZAFmRIbEg1hkIk04v8phTrxfgPmQ=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "ua-parser-js": ["ua-parser-js@0.7.41", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg=="], + "undici": ["undici@6.28.1", "", {}, "sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA=="], "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], @@ -2419,10 +2787,18 @@ "update-browserslist-db": ["update-browserslist-db@1.3.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ=="], + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-latest-callback": ["use-latest-callback@0.2.6", "", { "peerDependencies": { "react": ">=16.8" } }, "sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + "use-sync-external-store": ["use-sync-external-store@1.7.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A=="], "username": ["username@5.1.0", "", { "dependencies": { "execa": "^1.0.0", "mem": "^4.3.0" } }, "sha512-PCKbdWw85JsYMvmCv5GH3kXmM66rCd9m1hBEDutPNv94b/pqCMT4NtcKyeWYvLFiE8b+ha1Jdl8XAaUdPn5QTg=="], + "util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], @@ -2433,6 +2809,8 @@ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="], + "vite": ["vite@8.3.0", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.28", "rolldown": "~1.2.6", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@vitejs/devtools", "esbuild", "less", "sass", "sass-embedded", "stylus", "sugarss", "tsx"], "bin": "bin/vite.js" }, "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ=="], "vitest": ["vitest@5.0.1", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/mocker": "5.0.1", "chai": "^6.2.2", "es-module-lexer": "^2.3.2", "expect-type": "^1.4.0", "magic-string": "^1.2.3", "obug": "^2.1.4", "picomatch": "^4.0.7", "std-env": "^4.2.0", "tinybench": "6.1.4", "tinyexec": "1.3.0", "tinyglobby": "^0.2.17", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "5.0.1", "@vitest/browser-preview": "5.0.1", "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", "@vitest/coverage-istanbul": "5.0.1", "@vitest/coverage-v8": "5.0.1", "@vitest/ui": "5.0.1", "happy-dom": "*", "jsdom": "*", "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": "vitest.mjs" }, "sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg=="], @@ -2443,21 +2821,27 @@ "walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="], + "warn-once": ["warn-once@0.1.1", "", {}, "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q=="], + "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], - "webidl-conversions": ["webidl-conversions@5.0.0", "", {}, "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA=="], + "web-streams-polyfill": ["web-streams-polyfill@4.3.0", "", {}, "sha512-/Gnggvj9oSrEvJbDyyPtAnxBt5fGQM2iWOKQNu7ie1OxDgK40iZpyV3TKaRiEzVj1oA1UxKnEy9XPXh6PW3eVw=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], "whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="], - "whatwg-url-without-unicode": ["whatwg-url-without-unicode@8.0.0-3", "", { "dependencies": { "buffer": "^5.4.3", "punycode": "^2.1.1", "webidl-conversions": "^5.0.0" } }, "sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig=="], + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "whatwg-url-minimum": ["whatwg-url-minimum@0.1.2", "", {}, "sha512-XPEm0XFQWNVG292lII1PrRRJl3sItrs7CettZ4ncYxuDVpLyy+NwlGyut2hXI0JswcJUxeCH+CyOJK0ZzAXD6A=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], - "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": "cli.js" }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "which-typed-array": ["which-typed-array@1.1.24", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ=="], - "wonka": ["wonka@6.3.6", "", {}, "sha512-MXH+6mDHAZ2GuMpgKS055FR6v0xVP3XwquxIMYXgiW+FejHQlMGlvVRZT4qMCxR+bEo/FCtIdKxwej9WV3YQag=="], + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": "cli.js" }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], @@ -2469,7 +2853,7 @@ "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], - "xcode": ["xcode@3.0.1", "", { "dependencies": { "simple-plist": "^1.1.0", "uuid": "^7.0.3" } }, "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA=="], + "xcode": ["xcode@4.0.0", "", { "dependencies": { "simple-plist": "^1.3.1" } }, "sha512-dDaoWzc9EP2fp9FS9PVZUkyvFxY4y1TOV9pcKaKLk++Ei/qb97XDFEVP7GnW7Osii/TzVABJ0jq2F+9/lq0TVA=="], "xml-formatter": ["xml-formatter@2.6.1", "", { "dependencies": { "xml-parser-xo": "^3.2.0" } }, "sha512-dOiGwoqm8y22QdTNI7A+N03tyVfBlQ0/oehAzxIZtwnFAHGeSlrfjF73YQvzSsa/Kt6+YZasKsrdu6OIpuBggw=="], @@ -2487,7 +2871,7 @@ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "yaml": ["yaml@2.9.1", "", { "bin": "bin.mjs" }, "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw=="], @@ -2497,6 +2881,8 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@babel/core/@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="], "@babel/core/@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="], @@ -2545,13 +2931,9 @@ "@babel/template/@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="], - "@babel/traverse--for-generate-function-map/@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="], - - "@expo/cli/@expo/env": ["@expo/env@2.0.12", "", { "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", "dotenv": "~16.4.5", "dotenv-expand": "~11.0.6", "getenv": "^2.0.0" } }, "sha512-wVfzeBGlUohZG5kS8QCqXurpuWZFJEkBB1wXCifai3EZ/Llcg/VMTiUCpAgHImD3lI7GIU3V1uI64c04XIo98Q=="], + "@expo/cli/@expo/env": ["@expo/env@2.1.3", "", { "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", "getenv": "^2.0.0" } }, "sha512-Rhu/lJ1kOhqzJvLwW0iB4WKUzB1nFa8WBwXFL44qkSTNwibjrbI8lA46Ix6W2MMTdlUqL1m85Gdyx0T7nGpREg=="], - "@expo/cli/@expo/plist": ["@expo/plist@0.4.9", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.2.3", "xmlbuilder": "^15.1.1" } }, "sha512-MPVpmKGfnQEnrCzgxuXcmPP/y/t6AVm+DcSb2Myp21LKWv1N3l8uFxMggesfF4ixAxkRlGmMMx9GyDC9M+XklQ=="], - - "@expo/cli/@expo/prebuild-config": ["@expo/prebuild-config@54.0.9", "", { "dependencies": { "@expo/config": "~12.0.14", "@expo/config-plugins": "~54.0.5", "@expo/config-types": "^54.0.10", "@expo/image-utils": "^0.8.8", "@expo/json-file": "^10.0.16", "@react-native/normalize-colors": "0.81.5", "debug": "^4.3.1", "resolve-from": "^5.0.0", "semver": "^7.6.0", "xml2js": "0.6.0" }, "peerDependencies": { "expo": "*" } }, "sha512-3/Rmyzt8vduPjnSVHbnc0wYFrlhwLWn2g596rDyKcLGeqN2WTLJbzVeznsrUwyzhBNXgnTomZWO5HDzbZ/4E7g=="], + "@expo/cli/@expo/prebuild-config": ["@expo/prebuild-config@55.0.22", "", { "dependencies": { "@expo/config": "~55.0.21", "@expo/config-plugins": "~55.0.11", "@expo/config-types": "^55.0.6", "@expo/image-utils": "^0.8.17", "@expo/json-file": "^10.0.15", "@react-native/normalize-colors": "0.83.10", "debug": "^4.3.1", "resolve-from": "^5.0.0", "semver": "^7.6.0", "xml2js": "0.6.0" }, "peerDependencies": { "expo": "*" } }, "sha512-2p1lE2hOCy9HUV9qxbEqmetxUgcBnVY8jR+j/SYRXgaBeHalyQ5d09Ol5XL36/HO2fViK8VmSnTbS7C3t8eFGg=="], "@expo/cli/bplist-parser": ["bplist-parser@0.3.2", "", { "dependencies": { "big-integer": "1.6.x" } }, "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ=="], @@ -2559,25 +2941,17 @@ "@expo/cli/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], - "@expo/config/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="], - "@expo/config/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], "@expo/config-plugins/@expo/json-file": ["@expo/json-file@10.0.16", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "json5": "^2.2.3" } }, "sha512-fcVkWEj+hLuP2yt5W0aw6LmDRqSPWDLUSxOMcmFeV+algmIF59sQVKCwB9btjQLd4V6x9N0pISkQEkBubUHrCw=="], - "@expo/config-plugins/@expo/plist": ["@expo/plist@0.4.9", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.2.3", "xmlbuilder": "^15.1.1" } }, "sha512-MPVpmKGfnQEnrCzgxuXcmPP/y/t6AVm+DcSb2Myp21LKWv1N3l8uFxMggesfF4ixAxkRlGmMMx9GyDC9M+XklQ=="], - "@expo/config-plugins/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], "@expo/devcert/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "@expo/fingerprint/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], - "@expo/fingerprint/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], - - "@expo/image-utils/@expo/require-utils": ["@expo/require-utils@55.0.8", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8" }, "peerDependencies": { "typescript": "^5.0.0 || ^5.0.0-0" } }, "sha512-DDS5R67iz2SJwncbpcUMHtnsq31dSfllhaEncKxGlK0/ewgc2CaXUCkDA2AumfT7ubnSXiPkU9Gn8v2xCrvj+A=="], - - "@expo/metro-config/@expo/env": ["@expo/env@2.0.12", "", { "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", "dotenv": "~16.4.5", "dotenv-expand": "~11.0.6", "getenv": "^2.0.0" } }, "sha512-wVfzeBGlUohZG5kS8QCqXurpuWZFJEkBB1wXCifai3EZ/Llcg/VMTiUCpAgHImD3lI7GIU3V1uI64c04XIo98Q=="], + "@expo/metro-config/@expo/env": ["@expo/env@2.1.3", "", { "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", "getenv": "^2.0.0" } }, "sha512-Rhu/lJ1kOhqzJvLwW0iB4WKUzB1nFa8WBwXFL44qkSTNwibjrbI8lA46Ix6W2MMTdlUqL1m85Gdyx0T7nGpREg=="], "@expo/metro-config/@expo/json-file": ["@expo/json-file@10.0.16", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "json5": "^2.2.3" } }, "sha512-fcVkWEj+hLuP2yt5W0aw6LmDRqSPWDLUSxOMcmFeV+algmIF59sQVKCwB9btjQLd4V6x9N0pISkQEkBubUHrCw=="], @@ -2611,9 +2985,11 @@ "@istanbuljs/load-nyc-config/js-yaml": ["js-yaml@3.15.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": "bin/js-yaml.js" }, "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w=="], + "@margelo/hermes-profile-transformer/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + "@react-native-community/cli/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], - "@react-native-community/cli-config-android/@react-native-community/cli-tools": ["@react-native-community/cli-tools@17.0.0", "", { "dependencies": { "@vscode/sudo-prompt": "^9.0.0", "appdirsjs": "^1.2.4", "chalk": "^4.1.2", "execa": "^5.0.0", "find-up": "^5.0.0", "launch-editor": "^2.9.1", "mime": "^2.4.1", "ora": "^5.4.1", "prompts": "^2.4.2", "semver": "^7.5.2" } }, "sha512-mVXH7U/uXd7yizqm2iE+W4SSVc4FGYYEafAu29HihA+FHttonqdg35zVAnIX2FKbyla+TotR1ACNSgo7KFDq+w=="], + "@react-native-community/cli-config-android/@react-native-community/cli-tools": ["@react-native-community/cli-tools@20.0.0", "", { "dependencies": { "@vscode/sudo-prompt": "^9.0.0", "appdirsjs": "^1.2.4", "chalk": "^4.1.2", "execa": "^5.0.0", "find-up": "^5.0.0", "launch-editor": "^2.9.1", "mime": "^2.4.1", "ora": "^5.4.1", "prompts": "^2.4.2", "semver": "^7.5.2" } }, "sha512-akSZGxr1IajJ8n0YCwQoA3DI0HttJ0WB7M3nVpb0lOM+rJpsBN7WG5Ft+8ozb6HyIPX+O+lLeYazxn5VNG/Xhw=="], "@react-native-community/cli-doctor/@react-native-community/cli-platform-android": ["@react-native-community/cli-platform-android@20.1.3", "", { "dependencies": { "@react-native-community/cli-config-android": "20.1.3", "@react-native-community/cli-tools": "20.1.3", "execa": "^5.0.0", "logkitty": "^0.7.1", "picocolors": "^1.1.1" } }, "sha512-bzB9ELPOISuqgtDZXFPQlkuxx1YFkNx3cNgslc5ElCrk+5LeCLQLIBh/dmIuK8rwUrPcrramjeBj++Noc+TaAA=="], @@ -2621,36 +2997,64 @@ "@react-native-community/cli-doctor/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], - "@react-native-community/cli-platform-android/@react-native-community/cli-tools": ["@react-native-community/cli-tools@17.0.0", "", { "dependencies": { "@vscode/sudo-prompt": "^9.0.0", "appdirsjs": "^1.2.4", "chalk": "^4.1.2", "execa": "^5.0.0", "find-up": "^5.0.0", "launch-editor": "^2.9.1", "mime": "^2.4.1", "ora": "^5.4.1", "prompts": "^2.4.2", "semver": "^7.5.2" } }, "sha512-mVXH7U/uXd7yizqm2iE+W4SSVc4FGYYEafAu29HihA+FHttonqdg35zVAnIX2FKbyla+TotR1ACNSgo7KFDq+w=="], + "@react-native-community/cli-platform-android/@react-native-community/cli-tools": ["@react-native-community/cli-tools@20.0.0", "", { "dependencies": { "@vscode/sudo-prompt": "^9.0.0", "appdirsjs": "^1.2.4", "chalk": "^4.1.2", "execa": "^5.0.0", "find-up": "^5.0.0", "launch-editor": "^2.9.1", "mime": "^2.4.1", "ora": "^5.4.1", "prompts": "^2.4.2", "semver": "^7.5.2" } }, "sha512-akSZGxr1IajJ8n0YCwQoA3DI0HttJ0WB7M3nVpb0lOM+rJpsBN7WG5Ft+8ozb6HyIPX+O+lLeYazxn5VNG/Xhw=="], - "@react-native-community/cli-platform-ios/@react-native-community/cli-platform-apple": ["@react-native-community/cli-platform-apple@17.0.0", "", { "dependencies": { "@react-native-community/cli-config-apple": "17.0.0", "@react-native-community/cli-tools": "17.0.0", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-xml-parser": "^4.4.1" } }, "sha512-2wJzZNDx3fzp0nDy/A/IKjhXrH/ouVMhEvzMD4kjbBp2v4CqASggKXcyFxqTeXccepl/anRcU0IIFlsfaNBACg=="], + "@react-native-community/cli-platform-ios/@react-native-community/cli-platform-apple": ["@react-native-community/cli-platform-apple@20.0.0", "", { "dependencies": { "@react-native-community/cli-config-apple": "20.0.0", "@react-native-community/cli-tools": "20.0.0", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-xml-parser": "^4.4.1" } }, "sha512-rZZCnAjUHN1XBgiWTAMwEKpbVTO4IHBSecdd1VxJFeTZ7WjmstqA6L/HXcnueBgxrzTCRqvkRIyEQXxC1OfhGw=="], "@react-native-community/cli-server-api/ws": ["ws@6.2.6", "", { "dependencies": { "async-limiter": "~1.0.0" } }, "sha512-XTrf1gv7kXoVf1hbC3PAyAiPgR8Wz1blcrYIjEsUmr08BLksT41R8KbjmS9408C2ERx7v1JDLD/BkpLEttjfKA=="], "@react-native-community/cli-tools/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], + "@react-native-windows/cli/@react-native-windows/fs": ["@react-native-windows/fs@0.83.1", "", { "dependencies": { "graceful-fs": "^4.2.8", "minimatch": "^10.0.3" } }, "sha512-BJMH6lI5OFdG0ccdSvHRiYp5+i2RPUDmIE0g10194lGQ/clg9Z+G+yz0DPKMgcubJCZwlCpW+BT7/mWJk0LFaA=="], + + "@react-native-windows/cli/@xmldom/xmldom": ["@xmldom/xmldom@0.7.13", "", {}, "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g=="], + "@react-native-windows/cli/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + "@react-native-windows/codegen/@react-native-windows/fs": ["@react-native-windows/fs@0.83.1", "", { "dependencies": { "graceful-fs": "^4.2.8", "minimatch": "^10.0.3" } }, "sha512-BJMH6lI5OFdG0ccdSvHRiYp5+i2RPUDmIE0g10194lGQ/clg9Z+G+yz0DPKMgcubJCZwlCpW+BT7/mWJk0LFaA=="], + "@react-native-windows/codegen/yargs": ["yargs@16.2.2", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w=="], + "@react-native-windows/find-repo-root/@react-native-windows/fs": ["@react-native-windows/fs@0.83.1", "", { "dependencies": { "graceful-fs": "^4.2.8", "minimatch": "^10.0.3" } }, "sha512-BJMH6lI5OFdG0ccdSvHRiYp5+i2RPUDmIE0g10194lGQ/clg9Z+G+yz0DPKMgcubJCZwlCpW+BT7/mWJk0LFaA=="], + "@react-native-windows/find-repo-root/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + "@react-native-windows/package-utils/@react-native-windows/fs": ["@react-native-windows/fs@0.83.1", "", { "dependencies": { "graceful-fs": "^4.2.8", "minimatch": "^10.0.3" } }, "sha512-BJMH6lI5OFdG0ccdSvHRiYp5+i2RPUDmIE0g10194lGQ/clg9Z+G+yz0DPKMgcubJCZwlCpW+BT7/mWJk0LFaA=="], + + "@react-native-windows/telemetry/@react-native-windows/fs": ["@react-native-windows/fs@0.83.1", "", { "dependencies": { "graceful-fs": "^4.2.8", "minimatch": "^10.0.3" } }, "sha512-BJMH6lI5OFdG0ccdSvHRiYp5+i2RPUDmIE0g10194lGQ/clg9Z+G+yz0DPKMgcubJCZwlCpW+BT7/mWJk0LFaA=="], + + "@react-native-windows/telemetry/@xmldom/xmldom": ["@xmldom/xmldom@0.7.13", "", {}, "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g=="], + "@react-native/babel-plugin-codegen/@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="], - "@react-native/babel-plugin-codegen/@react-native/codegen": ["@react-native/codegen@0.81.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.29.1", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "yargs": "^17.6.2" }, "peerDependencies": { "@babel/core": "*" } }, "sha512-a2TDA03Up8lpSa9sh5VRGCQDXgCTOyDOFH+aqyinxp1HChG8uk89/G+nkJ9FPd0rqgi25eCTR16TWdS3b+fA6g=="], + "@react-native/babel-preset/@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ=="], - "@react-native/codegen/@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="], + "@react-native/babel-preset/@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA=="], - "@react-native/community-cli-plugin/@react-native/dev-middleware": ["@react-native/dev-middleware@0.81.6", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.81.6", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^4.4.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "serve-static": "^1.16.2", "ws": "^6.2.3" } }, "sha512-mK2M3gJ25LtgtqxS1ZXe1vHrz8APOA79Ot/MpbLeovFgLu6YJki0kbO5MRpJagTd+HbesVYSZb/BhAsGN7QAXA=="], + "@react-native/babel-preset/@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g=="], - "@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="], + "@react-native/babel-preset/@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg=="], + + "@react-native/babel-preset/@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ=="], - "@react-native/dev-middleware/ws": ["ws@6.2.6", "", { "dependencies": { "async-limiter": "~1.0.0" } }, "sha512-XTrf1gv7kXoVf1hbC3PAyAiPgR8Wz1blcrYIjEsUmr08BLksT41R8KbjmS9408C2ERx7v1JDLD/BkpLEttjfKA=="], + "@react-native/babel-preset/@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg=="], - "@react-native/metro-babel-transformer/@react-native/babel-preset": ["@react-native/babel-preset@0.81.6", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-arrow-functions": "^7.24.7", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-function-name": "^7.25.1", "@babel/plugin-transform-literals": "^7.25.2", "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-numeric-separator": "^7.24.7", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-shorthand-properties": "^7.24.7", "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", "@react-native/babel-plugin-codegen": "0.81.6", "babel-plugin-syntax-hermes-parser": "0.29.1", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" }, "peerDependencies": { "@babel/core": "*" } }, "sha512-RtIr82ccPoyMLmIC3/vCG96MzRmIT+IzQkjCgTS5b93uAxiER9BS7+d1l7+zD00VanClt6CTBM4K4n6RCnAykg=="], + "@react-native/babel-preset/@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA=="], + + "@react-native/codegen/@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="], + + "@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="], + + "@react-native/dev-middleware/ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="], "@rnx-kit/tools-workspaces/js-yaml": ["js-yaml@5.4.2", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.mjs" }, "sha512-m+aqu+LwO1O6sIopafj8HUVl5aawITwZQe/yHpMCKjaWBaA/d07B/QdMb3529REftiU+RMMHL3Vlsw3hON7vWg=="], + "@sentry/bundler-plugins/@sentry/cli": ["@sentry/cli@2.58.6", "", { "dependencies": { "https-proxy-agent": "^5.0.0", "node-fetch": "^2.6.7", "progress": "^2.0.3", "proxy-from-env": "^1.1.0", "which": "^2.0.2" }, "optionalDependencies": { "@sentry/cli-darwin": "2.58.6", "@sentry/cli-linux-arm": "2.58.6", "@sentry/cli-linux-arm64": "2.58.6", "@sentry/cli-linux-i686": "2.58.6", "@sentry/cli-linux-x64": "2.58.6", "@sentry/cli-win32-arm64": "2.58.6", "@sentry/cli-win32-i686": "2.58.6", "@sentry/cli-win32-x64": "2.58.6" }, "bin": { "sentry-cli": "bin/sentry-cli" } }, "sha512-baBcNPLLfUi9WuL+Tpri9BFaAdvugZIKelC5X0tt0Zdy+K0K+PCVSrnNmwMWU/HyaF/SEv6b6UHnXIdqanBlcg=="], + + "@sentry/bundler-plugins/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + + "@sentry/bundler-plugins/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "@tailwindcss/node/lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], "@tailwindcss/node/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -2661,14 +3065,22 @@ "@types/babel__template/@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="], + "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "babel-preset-expo/@babel/preset-typescript": ["@babel/preset-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "@babel/plugin-syntax-jsx": "^7.29.7", "@babel/plugin-transform-modules-commonjs": "^7.29.7", "@babel/plugin-transform-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ=="], + "better-opn/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], + "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "chrome-launcher/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], @@ -2679,15 +3091,17 @@ "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "compressible/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "compression/negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], "connect/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "expo-constants/@expo/env": ["@expo/env@2.0.12", "", { "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", "dotenv": "~16.4.5", "dotenv-expand": "~11.0.6", "getenv": "^2.0.0" } }, "sha512-wVfzeBGlUohZG5kS8QCqXurpuWZFJEkBB1wXCifai3EZ/Llcg/VMTiUCpAgHImD3lI7GIU3V1uI64c04XIo98Q=="], + "css-tree/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "error-ex/is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "expo-constants/@expo/env": ["@expo/env@2.1.3", "", { "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", "getenv": "^2.0.0" } }, "sha512-Rhu/lJ1kOhqzJvLwW0iB4WKUzB1nFa8WBwXFL44qkSTNwibjrbI8lA46Ix6W2MMTdlUqL1m85Gdyx0T7nGpREg=="], "expo-desktop/@expo/config": ["@expo/config@56.0.14", "", { "dependencies": { "@expo/config-plugins": "~56.0.16", "@expo/config-types": "^56.0.7", "@expo/json-file": "^10.2.0", "@expo/require-utils": "^56.1.7", "deepmerge": "^4.3.1", "getenv": "^2.0.0", "glob": "^13.0.0", "resolve-workspace-root": "^2.0.0", "semver": "^7.6.0", "slugify": "^1.3.4" } }, "sha512-OxKFAjFiOoWIBornS+QeACEt1HyIidZhLL1RKe+lyY/afk+c2H3BT8ftMLTp7NgQ28Af4MuKu8sJznUEEQFs1A=="], @@ -2697,20 +3111,30 @@ "expo-desktop/@expo/metro": ["@expo/metro@56.1.0", "", { "dependencies": { "metro": "0.84.6", "metro-babel-transformer": "0.84.6", "metro-cache": "0.84.6", "metro-cache-key": "0.84.6", "metro-config": "0.84.6", "metro-core": "0.84.6", "metro-file-map": "0.84.6", "metro-minify-terser": "0.84.6", "metro-resolver": "0.84.6", "metro-runtime": "0.84.6", "metro-source-map": "0.84.6", "metro-symbolicate": "0.84.6", "metro-transform-plugins": "0.84.6", "metro-transform-worker": "0.84.6" } }, "sha512-nHJNWRMkZffytNiikCWJ/ufVY2FP2vIFrp9Y+kl8rl88de0zYT9OX3WuEJUGry7TGWl94TYZqHSDbRU+8qK9Yw=="], + "expo-desktop/@expo/plist": ["@expo/plist@0.7.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-vrpryU1GoqSIRNqRB2D3IjXDmzNYfiQpEF6AH/xknlD7eiYmEDt3mb26V7cLcedcPG8PY/1xWHdBXVQJfEAh6Q=="], + "expo-desktop/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "expo-desktop/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "expo-desktop-config-plugins/@expo/config-plugins": ["@expo/config-plugins@56.0.16", "", { "dependencies": { "@expo/config-types": "^56.0.7", "@expo/json-file": "~10.2.0", "@expo/plist": "^0.7.0", "@expo/require-utils": "^56.1.7", "@expo/sdk-runtime-versions": "^1.0.0", "chalk": "^4.1.2", "debug": "^4.3.5", "getenv": "^2.0.0", "glob": "^13.0.0", "semver": "^7.5.4", "slugify": "^1.6.6", "xcode": "^3.0.1", "xml2js": "0.6.0" } }, "sha512-IfdCDxIGpaeaBN+r1DFg3j8Vmg/pN0G58WMaJwJw++dopSU0CLmXL8348llPKvvUpSEQQhx6TyVi5au6spZCbw=="], + "expo-desktop-config-plugins/@expo/plist": ["@expo/plist@0.7.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-vrpryU1GoqSIRNqRB2D3IjXDmzNYfiQpEF6AH/xknlD7eiYmEDt3mb26V7cLcedcPG8PY/1xWHdBXVQJfEAh6Q=="], + "expo-desktop-config-plugins/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + "expo-desktop-config-plugins/xcode": ["xcode@3.0.1", "", { "dependencies": { "simple-plist": "^1.1.0", "uuid": "^7.0.3" } }, "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA=="], + "expo-desktop-prebuild-config/@expo/config": ["@expo/config@56.0.14", "", { "dependencies": { "@expo/config-plugins": "~56.0.16", "@expo/config-types": "^56.0.7", "@expo/json-file": "^10.2.0", "@expo/require-utils": "^56.1.7", "deepmerge": "^4.3.1", "getenv": "^2.0.0", "glob": "^13.0.0", "resolve-workspace-root": "^2.0.0", "semver": "^7.6.0", "slugify": "^1.3.4" } }, "sha512-OxKFAjFiOoWIBornS+QeACEt1HyIidZhLL1RKe+lyY/afk+c2H3BT8ftMLTp7NgQ28Af4MuKu8sJznUEEQFs1A=="], "expo-desktop-prebuild-config/@expo/config-plugins": ["@expo/config-plugins@56.0.16", "", { "dependencies": { "@expo/config-types": "^56.0.7", "@expo/json-file": "~10.2.0", "@expo/plist": "^0.7.0", "@expo/require-utils": "^56.1.7", "@expo/sdk-runtime-versions": "^1.0.0", "chalk": "^4.1.2", "debug": "^4.3.5", "getenv": "^2.0.0", "glob": "^13.0.0", "semver": "^7.5.4", "slugify": "^1.6.6", "xcode": "^3.0.1", "xml2js": "0.6.0" } }, "sha512-IfdCDxIGpaeaBN+r1DFg3j8Vmg/pN0G58WMaJwJw++dopSU0CLmXL8348llPKvvUpSEQQhx6TyVi5au6spZCbw=="], "expo-modules-autolinking/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], + "expo-router/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], + + "expo-splash-screen/@expo/prebuild-config": ["@expo/prebuild-config@55.0.22", "", { "dependencies": { "@expo/config": "~55.0.21", "@expo/config-plugins": "~55.0.11", "@expo/config-types": "^55.0.6", "@expo/image-utils": "^0.8.17", "@expo/json-file": "^10.0.15", "@react-native/normalize-colors": "0.83.10", "debug": "^4.3.1", "resolve-from": "^5.0.0", "semver": "^7.6.0", "xml2js": "0.6.0" }, "peerDependencies": { "expo": "*" } }, "sha512-2p1lE2hOCy9HUV9qxbEqmetxUgcBnVY8jR+j/SYRXgaBeHalyQ5d09Ol5XL36/HO2fViK8VmSnTbS7C3t8eFGg=="], + "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "finalhandler/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], @@ -2725,6 +3149,8 @@ "glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "istanbul-lib-instrument/@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="], @@ -2747,13 +3173,15 @@ "metro/@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="], + "metro/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + "metro/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="], - "metro/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="], + "metro/hermes-parser": ["hermes-parser@0.35.0", "", { "dependencies": { "hermes-estree": "0.35.0" } }, "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA=="], "metro/ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="], - "metro-babel-transformer/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="], + "metro-babel-transformer/hermes-parser": ["hermes-parser@0.35.0", "", { "dependencies": { "hermes-estree": "0.35.0" } }, "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA=="], "metro-source-map/@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="], @@ -3063,19 +3491,41 @@ "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], - "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], - "react-devtools-core/ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="], - "react-native/ws": ["ws@6.2.6", "", { "dependencies": { "async-limiter": "~1.0.0" } }, "sha512-XTrf1gv7kXoVf1hbC3PAyAiPgR8Wz1blcrYIjEsUmr08BLksT41R8KbjmS9408C2ERx7v1JDLD/BkpLEttjfKA=="], + "react-native/ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="], + + "react-native-macos/ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="], + + "react-native-reanimated/react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.2.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q=="], + + "react-native-reanimated/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "react-native-release-profiler/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], + + "react-native-windows/@react-native-community/cli": ["@react-native-community/cli@20.0.0", "", { "dependencies": { "@react-native-community/cli-clean": "20.0.0", "@react-native-community/cli-config": "20.0.0", "@react-native-community/cli-doctor": "20.0.0", "@react-native-community/cli-server-api": "20.0.0", "@react-native-community/cli-tools": "20.0.0", "@react-native-community/cli-types": "20.0.0", "chalk": "^4.1.2", "commander": "^9.4.1", "deepmerge": "^4.3.0", "execa": "^5.0.0", "find-up": "^5.0.0", "fs-extra": "^8.1.0", "graceful-fs": "^4.1.3", "prompts": "^2.4.2", "semver": "^7.5.2" }, "bin": { "rnc-cli": "build/bin.js" } }, "sha512-/cMnGl5V1rqnbElY1Fvga1vfw0d3bnqiJLx2+2oh7l9ulnXfVRWb5tU2kgBqiMxuDOKA+DQoifC9q/tvkj5K2w=="], + + "react-native-windows/@react-native/assets-registry": ["@react-native/assets-registry@0.83.4", "", {}, "sha512-aqKtpbJDSQeSX/Dwv0yMe1/Rd2QfXi12lnyZDXNn/OEKz59u6+LuPBVgO/9CRyclHmdlvwg8c7PJ9eX2ZMnjWg=="], + + "react-native-windows/@react-native/codegen": ["@react-native/codegen@0.83.4", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.32.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "yargs": "^17.6.2" } }, "sha512-CJ7XutzIqJPz3Lp/5TOiRWlU/JAjTboMT1BHNLSXjYHXwTmgHM3iGEbpCOtBMjWvsojRTJyRO/G3ghInIIXEYg=="], + + "react-native-windows/@react-native/community-cli-plugin": ["@react-native/community-cli-plugin@0.83.4", "", { "dependencies": { "@react-native/dev-middleware": "0.83.4", "debug": "^4.4.0", "invariant": "^2.2.4", "metro": "^0.83.3", "metro-config": "^0.83.3", "metro-core": "^0.83.3", "semver": "^7.1.3" }, "peerDependencies": { "@react-native-community/cli": "*", "@react-native/metro-config": "*" }, "optionalPeers": ["@react-native-community/cli", "@react-native/metro-config"] }, "sha512-8os0weQEnjUhWy7Db881+JKRwNHVGM40VtTRvltAyA/YYkrGg4kPCqiTybMxQDEcF3rnviuxHyI+ITiglfmgmQ=="], + + "react-native-windows/@react-native/gradle-plugin": ["@react-native/gradle-plugin@0.83.4", "", {}, "sha512-AhaSWw2k3eMKqZ21IUdM7rpyTYOpAfsBbIIiom1QQii3QccX0uW2AWTcRhfuWRxqr2faGFaOBYedWl2fzp5hgw=="], - "react-native-macos/ws": ["ws@6.2.6", "", { "dependencies": { "async-limiter": "~1.0.0" } }, "sha512-XTrf1gv7kXoVf1hbC3PAyAiPgR8Wz1blcrYIjEsUmr08BLksT41R8KbjmS9408C2ERx7v1JDLD/BkpLEttjfKA=="], + "react-native-windows/@react-native/js-polyfills": ["@react-native/js-polyfills@0.83.4", "", {}, "sha512-wYUdv0rt4MjhKhQloO1AnGDXhZQOFZHDxm86dEtEA0WcsCdVrFdRULFM+rKUC/QQtJW2rS6WBqtBusgtrsDADg=="], - "react-native-windows/@react-native-community/cli": ["@react-native-community/cli@17.0.0", "", { "dependencies": { "@react-native-community/cli-clean": "17.0.0", "@react-native-community/cli-config": "17.0.0", "@react-native-community/cli-doctor": "17.0.0", "@react-native-community/cli-server-api": "17.0.0", "@react-native-community/cli-tools": "17.0.0", "@react-native-community/cli-types": "17.0.0", "chalk": "^4.1.2", "commander": "^9.4.1", "deepmerge": "^4.3.0", "execa": "^5.0.0", "find-up": "^5.0.0", "fs-extra": "^8.1.0", "graceful-fs": "^4.1.3", "prompts": "^2.4.2", "semver": "^7.5.2" }, "bin": { "rnc-cli": "build/bin.js" } }, "sha512-c2SiCEGh0rK3BgCfTmWon968LJuJhWc3ZhDNh5MGvaMk0RuYt2K5MSXbSQ5kaRI1xdgzhXteDnoQlDp1PnyejQ=="], + "react-native-windows/@react-native/normalize-colors": ["@react-native/normalize-colors@0.83.4", "", {}, "sha512-9ezxaHjxqTkTOLg62SGg7YhFaE+fxa/jlrWP0nwf7eGFHlGOiTAaRR2KUfiN3K05e+EMbEhgcH/c7bgaXeGyJw=="], - "react-native-windows/ws": ["ws@6.2.6", "", { "dependencies": { "async-limiter": "~1.0.0" } }, "sha512-XTrf1gv7kXoVf1hbC3PAyAiPgR8Wz1blcrYIjEsUmr08BLksT41R8KbjmS9408C2ERx7v1JDLD/BkpLEttjfKA=="], + "react-native-windows/@react-native/virtualized-lists": ["@react-native/virtualized-lists@0.83.4", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.2.0", "react": "*", "react-native": "*" }, "optionalPeers": ["@types/react"] }, "sha512-vNF/8kokMW8JEjG4n+j7veLTjHRRABlt4CaTS6+wtqzvWxCJHNIC8fhCqrDPn9fIn8sNePd8DyiFVX5L9TBBRA=="], - "requireg/resolve": ["resolve@1.7.1", "", { "dependencies": { "path-parse": "^1.0.5" } }, "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw=="], + "react-native-windows/ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="], + + "react-native-worklets/@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg=="], + + "react-native-worklets/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "react-reconciler/scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], "restore-cursor/onetime": ["onetime@2.0.1", "", { "dependencies": { "mimic-fn": "^1.0.0" } }, "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ=="], @@ -3099,13 +3549,11 @@ "strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="], - "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "test-exclude/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - "type-is/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "typeorm/yargs": ["yargs@18.2.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^8.2.1", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w=="], "username/execa": ["execa@1.0.0", "", { "dependencies": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" } }, "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA=="], @@ -3123,8 +3571,6 @@ "xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], - "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="], "@babel/helper-member-expression-to-functions/@babel/traverse/@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="], @@ -3157,30 +3603,18 @@ "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="], - "@expo/cli/@expo/prebuild-config/@react-native/normalize-colors": ["@react-native/normalize-colors@0.81.5", "", {}, "sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g=="], - - "@expo/cli/glob/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], - "@expo/cli/glob/path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], "@expo/config-plugins/@expo/json-file/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="], - "@expo/config-plugins/glob/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], - "@expo/config-plugins/glob/path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], - "@expo/config/glob/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], - "@expo/config/glob/path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], "@expo/fingerprint/glob/path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], - "@expo/fingerprint/minimatch/brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], - "@expo/metro-config/@expo/json-file/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="], - "@expo/metro-config/glob/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], - "@expo/metro-config/glob/path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], "@expo/metro-config/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], @@ -3203,10 +3637,22 @@ "@expo/metro-config/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], + "@expo/prebuild-config/@expo/config/@expo/require-utils": ["@expo/require-utils@56.1.7", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8" }, "peerDependencies": { "typescript": "^5.0.0 || ^5.0.0-0 || ^6.0.0 || ^7.0.0" } }, "sha512-7Huw3V4VVThWZglueHpHX4gxdYvjhKOO5DzC/1NBuaDY4IPf/iPhqE+diFA6J/ZF0nC+tcC4YmUidXhnr7cmzQ=="], + "@expo/prebuild-config/@expo/config/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + "@expo/prebuild-config/@expo/config-plugins/@expo/plist": ["@expo/plist@0.7.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-vrpryU1GoqSIRNqRB2D3IjXDmzNYfiQpEF6AH/xknlD7eiYmEDt3mb26V7cLcedcPG8PY/1xWHdBXVQJfEAh6Q=="], + + "@expo/prebuild-config/@expo/config-plugins/@expo/require-utils": ["@expo/require-utils@56.1.7", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8" }, "peerDependencies": { "typescript": "^5.0.0 || ^5.0.0-0 || ^6.0.0 || ^7.0.0" } }, "sha512-7Huw3V4VVThWZglueHpHX4gxdYvjhKOO5DzC/1NBuaDY4IPf/iPhqE+diFA6J/ZF0nC+tcC4YmUidXhnr7cmzQ=="], + "@expo/prebuild-config/@expo/config-plugins/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + "@expo/prebuild-config/@expo/config-plugins/xcode": ["xcode@3.0.1", "", { "dependencies": { "simple-plist": "^1.1.0", "uuid": "^7.0.3" } }, "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA=="], + + "@expo/prebuild-config/@expo/image-utils/@expo/require-utils": ["@expo/require-utils@56.1.7", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8" }, "peerDependencies": { "typescript": "^5.0.0 || ^5.0.0-0 || ^6.0.0 || ^7.0.0" } }, "sha512-7Huw3V4VVThWZglueHpHX4gxdYvjhKOO5DzC/1NBuaDY4IPf/iPhqE+diFA6J/ZF0nC+tcC4YmUidXhnr7cmzQ=="], + + "@expo/prebuild-config/expo-modules-autolinking/@expo/require-utils": ["@expo/require-utils@56.1.7", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8" }, "peerDependencies": { "typescript": "^5.0.0 || ^5.0.0-0 || ^6.0.0 || ^7.0.0" } }, "sha512-7Huw3V4VVThWZglueHpHX4gxdYvjhKOO5DzC/1NBuaDY4IPf/iPhqE+diFA6J/ZF0nC+tcC4YmUidXhnr7cmzQ=="], + "@expo/prebuild-config/expo-modules-autolinking/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], @@ -3231,9 +3677,9 @@ "@react-native-community/cli-platform-android/@react-native-community/cli-tools/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], - "@react-native-community/cli-platform-ios/@react-native-community/cli-platform-apple/@react-native-community/cli-config-apple": ["@react-native-community/cli-config-apple@17.0.0", "", { "dependencies": { "@react-native-community/cli-tools": "17.0.0", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-glob": "^3.3.2" } }, "sha512-eMVe4aK2fsS0PcSWx7eI5snLP6J+N6jS4v625F+6K9GRzmxHF7cMFk3feFEmjreQxQm84OeZrJwnRnqhbnImIg=="], + "@react-native-community/cli-platform-ios/@react-native-community/cli-platform-apple/@react-native-community/cli-config-apple": ["@react-native-community/cli-config-apple@20.0.0", "", { "dependencies": { "@react-native-community/cli-tools": "20.0.0", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-glob": "^3.3.2" } }, "sha512-PS1gNOdpeQ6w7dVu1zi++E+ix2D0ZkGC2SQP6Y/Qp002wG4se56esLXItYiiLrJkhH21P28fXdmYvTEkjSm9/Q=="], - "@react-native-community/cli-platform-ios/@react-native-community/cli-platform-apple/@react-native-community/cli-tools": ["@react-native-community/cli-tools@17.0.0", "", { "dependencies": { "@vscode/sudo-prompt": "^9.0.0", "appdirsjs": "^1.2.4", "chalk": "^4.1.2", "execa": "^5.0.0", "find-up": "^5.0.0", "launch-editor": "^2.9.1", "mime": "^2.4.1", "ora": "^5.4.1", "prompts": "^2.4.2", "semver": "^7.5.2" } }, "sha512-mVXH7U/uXd7yizqm2iE+W4SSVc4FGYYEafAu29HihA+FHttonqdg35zVAnIX2FKbyla+TotR1ACNSgo7KFDq+w=="], + "@react-native-community/cli-platform-ios/@react-native-community/cli-platform-apple/@react-native-community/cli-tools": ["@react-native-community/cli-tools@20.0.0", "", { "dependencies": { "@vscode/sudo-prompt": "^9.0.0", "appdirsjs": "^1.2.4", "chalk": "^4.1.2", "execa": "^5.0.0", "find-up": "^5.0.0", "launch-editor": "^2.9.1", "mime": "^2.4.1", "ora": "^5.4.1", "prompts": "^2.4.2", "semver": "^7.5.2" } }, "sha512-akSZGxr1IajJ8n0YCwQoA3DI0HttJ0WB7M3nVpb0lOM+rJpsBN7WG5Ft+8ozb6HyIPX+O+lLeYazxn5VNG/Xhw=="], "@react-native-community/cli-tools/ora/cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], @@ -3251,17 +3697,29 @@ "@react-native/babel-plugin-codegen/@babel/traverse/@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="], - "@react-native/babel-plugin-codegen/@react-native/codegen/@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="], + "@react-native/babel-preset/@babel/plugin-transform-classes/@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="], - "@react-native/community-cli-plugin/@react-native/dev-middleware/@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.81.6", "", {}, "sha512-aGw28yzbtm25GQuuxNeVAT72tLuGoH0yh79uYOIZkvjI+5x1NjZyPrgiLZ2LlZi5dJdxfbz30p1zUcHvcAzEZw=="], + "@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - "@react-native/community-cli-plugin/@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="], + "@sentry/bundler-plugins/@sentry/cli/@sentry/cli-darwin": ["@sentry/cli-darwin@2.58.6", "", { "os": "darwin" }, "sha512-udAVvcyfNa0R+95GvPz/+43/N3TC0TYKdkQ7D7jhPSzbcMc7l2fxRNN5yB3UpCA5fWFnW4toeaqwDBhb/Wh3LA=="], - "@react-native/community-cli-plugin/@react-native/dev-middleware/ws": ["ws@6.2.6", "", { "dependencies": { "async-limiter": "~1.0.0" } }, "sha512-XTrf1gv7kXoVf1hbC3PAyAiPgR8Wz1blcrYIjEsUmr08BLksT41R8KbjmS9408C2ERx7v1JDLD/BkpLEttjfKA=="], + "@sentry/bundler-plugins/@sentry/cli/@sentry/cli-linux-arm": ["@sentry/cli-linux-arm@2.58.6", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "arm" }, "sha512-pD0LAt5PcUzAinBwvDqc66x9+2CabHEv486yP0gRjWO7SakbaxmfVq/EXd8VLq/Tzi39LAu422UYK1lpW3MILw=="], - "@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + "@sentry/bundler-plugins/@sentry/cli/@sentry/cli-linux-arm64": ["@sentry/cli-linux-arm64@2.58.6", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "arm64" }, "sha512-q8mEcNNmeXMy5i+jWT30TVpH7LcP4HD21CD5XRSPAd/a912HF6EpK0ybf/1USO14WOhoXbAGi9txwaWabSe33g=="], + + "@sentry/bundler-plugins/@sentry/cli/@sentry/cli-linux-i686": ["@sentry/cli-linux-i686@2.58.6", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "ia32" }, "sha512-q8vNJi1eOV/4vxAFWBsEwLHoSYapaZHIf4j76KJGJXFKTkEbsjCOOsKbwUIBTQQhRgV4DFWh3ryfsPS/que4Kg=="], + + "@sentry/bundler-plugins/@sentry/cli/@sentry/cli-linux-x64": ["@sentry/cli-linux-x64@2.58.6", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "x64" }, "sha512-DZu956Mhi3ZRjTBe1WdbGV46ldVbA8d2rgp/fh51GsI25zjBHah4wZnPTSzpc+YqxU6pJpg579B/r3jrIK530Q=="], + + "@sentry/bundler-plugins/@sentry/cli/@sentry/cli-win32-arm64": ["@sentry/cli-win32-arm64@2.58.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-nj0Ff/kmAB73EPDhR8B4O9r+NUHK5GkPCkGWC+kXVemqAJWL5jcJ5KdxG0l/S0z6RoEoltID8/43/B+TaMlT7A=="], - "@react-native/metro-babel-transformer/@react-native/babel-preset/@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.81.6", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@react-native/codegen": "0.81.6" } }, "sha512-OBx6/S0h0MEAoUXhRCWBu00+Oz0gCcHzduTHN++qnK7cIZUhxASUdplYzBZTtTdtA+Zr0bDzH05EsXq718WgFA=="], + "@sentry/bundler-plugins/@sentry/cli/@sentry/cli-win32-i686": ["@sentry/cli-win32-i686@2.58.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-WNZiDzPbgsEMQWq4avsQ391v/xWKJDIWWWo9GYl+N/w5qcYKkoDW7wQG7T9FasI6ENn68phChTOAPXXxbfAdOg=="], + + "@sentry/bundler-plugins/@sentry/cli/@sentry/cli-win32-x64": ["@sentry/cli-win32-x64@2.58.6", "", { "os": "win32", "cpu": "x64" }, "sha512-R35WJ17oF4D2eqI1DR2sQQqr0fjRTt5xoP16WrTu91XM2lndRMFsnjh+/GttbxapLCBNlrjzia99MJ0PZHZpgA=="], + + "@sentry/bundler-plugins/@sentry/cli/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + + "@sentry/bundler-plugins/glob/path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], "@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="], @@ -3285,6 +3743,8 @@ "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="], + "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "better-opn/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], @@ -3293,27 +3753,43 @@ "expo-desktop-config-plugins/@expo/config-plugins/@expo/config-types": ["@expo/config-types@56.0.7", "", {}, "sha512-V7bxawNsNned/yMppAHdisIOxniZXgPKRWpIUiQOQBs45/A5MBd2gfDM4Ecq5gnbilnQUTaI6Zxn6JcW7L3TAA=="], + "expo-desktop-config-plugins/@expo/config-plugins/@expo/require-utils": ["@expo/require-utils@56.1.7", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8" }, "peerDependencies": { "typescript": "^5.0.0 || ^5.0.0-0 || ^6.0.0 || ^7.0.0" } }, "sha512-7Huw3V4VVThWZglueHpHX4gxdYvjhKOO5DzC/1NBuaDY4IPf/iPhqE+diFA6J/ZF0nC+tcC4YmUidXhnr7cmzQ=="], + "expo-desktop-config-plugins/@expo/config-plugins/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + "expo-desktop-config-plugins/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + "expo-desktop-prebuild-config/@expo/config/@expo/config-types": ["@expo/config-types@56.0.7", "", {}, "sha512-V7bxawNsNned/yMppAHdisIOxniZXgPKRWpIUiQOQBs45/A5MBd2gfDM4Ecq5gnbilnQUTaI6Zxn6JcW7L3TAA=="], + "expo-desktop-prebuild-config/@expo/config/@expo/require-utils": ["@expo/require-utils@56.1.7", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8" }, "peerDependencies": { "typescript": "^5.0.0 || ^5.0.0-0 || ^6.0.0 || ^7.0.0" } }, "sha512-7Huw3V4VVThWZglueHpHX4gxdYvjhKOO5DzC/1NBuaDY4IPf/iPhqE+diFA6J/ZF0nC+tcC4YmUidXhnr7cmzQ=="], + "expo-desktop-prebuild-config/@expo/config/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], "expo-desktop-prebuild-config/@expo/config-plugins/@expo/config-types": ["@expo/config-types@56.0.7", "", {}, "sha512-V7bxawNsNned/yMppAHdisIOxniZXgPKRWpIUiQOQBs45/A5MBd2gfDM4Ecq5gnbilnQUTaI6Zxn6JcW7L3TAA=="], + "expo-desktop-prebuild-config/@expo/config-plugins/@expo/plist": ["@expo/plist@0.7.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-vrpryU1GoqSIRNqRB2D3IjXDmzNYfiQpEF6AH/xknlD7eiYmEDt3mb26V7cLcedcPG8PY/1xWHdBXVQJfEAh6Q=="], + + "expo-desktop-prebuild-config/@expo/config-plugins/@expo/require-utils": ["@expo/require-utils@56.1.7", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8" }, "peerDependencies": { "typescript": "^5.0.0 || ^5.0.0-0 || ^6.0.0 || ^7.0.0" } }, "sha512-7Huw3V4VVThWZglueHpHX4gxdYvjhKOO5DzC/1NBuaDY4IPf/iPhqE+diFA6J/ZF0nC+tcC4YmUidXhnr7cmzQ=="], + "expo-desktop-prebuild-config/@expo/config-plugins/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + "expo-desktop-prebuild-config/@expo/config-plugins/xcode": ["xcode@3.0.1", "", { "dependencies": { "simple-plist": "^1.1.0", "uuid": "^7.0.3" } }, "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA=="], + "expo-desktop/@expo/config/@expo/config-types": ["@expo/config-types@56.0.7", "", {}, "sha512-V7bxawNsNned/yMppAHdisIOxniZXgPKRWpIUiQOQBs45/A5MBd2gfDM4Ecq5gnbilnQUTaI6Zxn6JcW7L3TAA=="], + "expo-desktop/@expo/config/@expo/require-utils": ["@expo/require-utils@56.1.7", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8" }, "peerDependencies": { "typescript": "^5.0.0 || ^5.0.0-0 || ^6.0.0 || ^7.0.0" } }, "sha512-7Huw3V4VVThWZglueHpHX4gxdYvjhKOO5DzC/1NBuaDY4IPf/iPhqE+diFA6J/ZF0nC+tcC4YmUidXhnr7cmzQ=="], + "expo-desktop/@expo/config/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], "expo-desktop/@expo/config-plugins/@expo/config-types": ["@expo/config-types@56.0.7", "", {}, "sha512-V7bxawNsNned/yMppAHdisIOxniZXgPKRWpIUiQOQBs45/A5MBd2gfDM4Ecq5gnbilnQUTaI6Zxn6JcW7L3TAA=="], + "expo-desktop/@expo/config-plugins/@expo/require-utils": ["@expo/require-utils@56.1.7", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8" }, "peerDependencies": { "typescript": "^5.0.0 || ^5.0.0-0 || ^6.0.0 || ^7.0.0" } }, "sha512-7Huw3V4VVThWZglueHpHX4gxdYvjhKOO5DzC/1NBuaDY4IPf/iPhqE+diFA6J/ZF0nC+tcC4YmUidXhnr7cmzQ=="], + "expo-desktop/@expo/config-plugins/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], - "expo-desktop/@expo/fingerprint/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + "expo-desktop/@expo/config-plugins/xcode": ["xcode@3.0.1", "", { "dependencies": { "simple-plist": "^1.1.0", "uuid": "^7.0.3" } }, "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA=="], - "expo-desktop/@expo/fingerprint/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], + "expo-desktop/@expo/fingerprint/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], "expo-desktop/@expo/metro/metro": ["metro@0.84.6", "", { "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.6", "metro-cache": "0.84.6", "metro-cache-key": "0.84.6", "metro-config": "0.84.6", "metro-core": "0.84.6", "metro-file-map": "0.84.6", "metro-resolver": "0.84.6", "metro-runtime": "0.84.6", "metro-source-map": "0.84.6", "metro-symbolicate": "0.84.6", "metro-transform-plugins": "0.84.6", "metro-transform-worker": "0.84.6", "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": "src/cli.js" }, "sha512-ty/tx/imE5Ph2gvPU788Ckim6RAC2tyZxLz2hWRgC9U7/fGKV7qY/slADy0qHPEw9cpUgc4BfmV3oUsYwm7oeg=="], @@ -3343,6 +3819,8 @@ "expo-desktop/@expo/metro/metro-transform-worker": ["metro-transform-worker@0.84.6", "", { "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.6", "metro-babel-transformer": "0.84.6", "metro-cache": "0.84.6", "metro-cache-key": "0.84.6", "metro-minify-terser": "0.84.6", "metro-source-map": "0.84.6", "metro-transform-plugins": "0.84.6", "nullthrows": "^1.1.1" } }, "sha512-xLTVrOENaHR9DiuhnHS+zcle0rKsCy2CpXSIx7xD+zhJs+qVRNUyV7Z1zIt/0P/d0cSGVx846R+dIjHEudhcww=="], + "expo-desktop/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "get-monorepo-packages/globby/array-union": ["array-union@1.0.2", "", { "dependencies": { "array-uniq": "^1.0.1" } }, "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng=="], @@ -3371,13 +3849,15 @@ "logkitty/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], - "metro-babel-transformer/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="], + "metro-babel-transformer/hermes-parser/hermes-estree": ["hermes-estree@0.35.0", "", {}, "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg=="], "metro-source-map/@babel/traverse/@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="], "metro-transform-plugins/@babel/traverse/@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="], - "metro/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="], + "metro/accepts/negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], + + "metro/hermes-parser/hermes-estree": ["hermes-estree@0.35.0", "", {}, "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg=="], "npm/minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], @@ -3393,20 +3873,24 @@ "os-locale/execa/human-signals": ["human-signals@1.1.1", "", {}, "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw=="], - "react-native-windows/@react-native-community/cli/@react-native-community/cli-clean": ["@react-native-community/cli-clean@17.0.0", "", { "dependencies": { "@react-native-community/cli-tools": "17.0.0", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-glob": "^3.3.2" } }, "sha512-mQUdUDYmQ/1FeYh+bQHsflmWPXqBrxe7ixvEhUFhbYocnM8nkYHrSnq8W8V4+8C7I/3jP2VUk4q1SSxFUqk3KA=="], + "react-native-windows/@react-native-community/cli/@react-native-community/cli-clean": ["@react-native-community/cli-clean@20.0.0", "", { "dependencies": { "@react-native-community/cli-tools": "20.0.0", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-glob": "^3.3.2" } }, "sha512-YmdNRcT+Dp8lC7CfxSDIfPMbVPEXVFzBH62VZNbYGxjyakqAvoQUFTYPgM2AyFusAr4wDFbDOsEv88gCDwR3ig=="], - "react-native-windows/@react-native-community/cli/@react-native-community/cli-config": ["@react-native-community/cli-config@17.0.0", "", { "dependencies": { "@react-native-community/cli-tools": "17.0.0", "chalk": "^4.1.2", "cosmiconfig": "^9.0.0", "deepmerge": "^4.3.0", "fast-glob": "^3.3.2", "joi": "^17.2.1" } }, "sha512-/Wb5zNmcdY4JzHKlHUqyDRXApFYCzxdi7CUIdIFOEaRaUYoKYtp0fUq2Y+US89phLMBO8x5s2IHc6dlFnaErGg=="], + "react-native-windows/@react-native-community/cli/@react-native-community/cli-config": ["@react-native-community/cli-config@20.0.0", "", { "dependencies": { "@react-native-community/cli-tools": "20.0.0", "chalk": "^4.1.2", "cosmiconfig": "^9.0.0", "deepmerge": "^4.3.0", "fast-glob": "^3.3.2", "joi": "^17.2.1" } }, "sha512-5Ky9ceYuDqG62VIIpbOmkg8Lybj2fUjf/5wK4UO107uRqejBgNgKsbGnIZgEhREcaSEOkujWrroJ9gweueLfBg=="], - "react-native-windows/@react-native-community/cli/@react-native-community/cli-doctor": ["@react-native-community/cli-doctor@17.0.0", "", { "dependencies": { "@react-native-community/cli-config": "17.0.0", "@react-native-community/cli-platform-android": "17.0.0", "@react-native-community/cli-platform-apple": "17.0.0", "@react-native-community/cli-platform-ios": "17.0.0", "@react-native-community/cli-tools": "17.0.0", "chalk": "^4.1.2", "command-exists": "^1.2.8", "deepmerge": "^4.3.0", "envinfo": "^7.13.0", "execa": "^5.0.0", "node-stream-zip": "^1.9.1", "ora": "^5.4.1", "semver": "^7.5.2", "wcwidth": "^1.0.1", "yaml": "^2.2.1" } }, "sha512-EqzjvVm1h9qf9iL8qxWxIcgSq3X2me6N8UN/oT9apSspf8QbWu2xTQQT2kHibFt37FTUsK/v3VgPKcMLnpZdrg=="], + "react-native-windows/@react-native-community/cli/@react-native-community/cli-doctor": ["@react-native-community/cli-doctor@20.0.0", "", { "dependencies": { "@react-native-community/cli-config": "20.0.0", "@react-native-community/cli-platform-android": "20.0.0", "@react-native-community/cli-platform-apple": "20.0.0", "@react-native-community/cli-platform-ios": "20.0.0", "@react-native-community/cli-tools": "20.0.0", "chalk": "^4.1.2", "command-exists": "^1.2.8", "deepmerge": "^4.3.0", "envinfo": "^7.13.0", "execa": "^5.0.0", "node-stream-zip": "^1.9.1", "ora": "^5.4.1", "semver": "^7.5.2", "wcwidth": "^1.0.1", "yaml": "^2.2.1" } }, "sha512-cPHspi59+Fy41FDVxt62ZWoicCZ1o34k8LAl64NVSY0lwPl+CEi78jipXJhtfkVqSTetloA8zexa/vSAcJy57Q=="], - "react-native-windows/@react-native-community/cli/@react-native-community/cli-server-api": ["@react-native-community/cli-server-api@17.0.0", "", { "dependencies": { "@react-native-community/cli-tools": "17.0.0", "body-parser": "^1.20.3", "compression": "^1.7.1", "connect": "^3.6.5", "errorhandler": "^1.5.1", "nocache": "^3.0.1", "open": "^6.2.0", "pretty-format": "^26.6.2", "serve-static": "^1.13.1", "ws": "^6.2.3" } }, "sha512-D4LILojRwbtsSAoV6Db6Fp7/FJ+mIeaKHlGmr5AZrelge/0u5quj2JQo2VS+TM5+9rvJOezVsd8k2VYX/ByccQ=="], + "react-native-windows/@react-native-community/cli/@react-native-community/cli-server-api": ["@react-native-community/cli-server-api@20.0.0", "", { "dependencies": { "@react-native-community/cli-tools": "20.0.0", "body-parser": "^1.20.3", "compression": "^1.7.1", "connect": "^3.6.5", "errorhandler": "^1.5.1", "nocache": "^3.0.1", "open": "^6.2.0", "pretty-format": "^29.7.0", "serve-static": "^1.13.1", "ws": "^6.2.3" } }, "sha512-Ves21bXtjUK3tQbtqw/NdzpMW1vR2HvYCkUQ/MXKrJcPjgJnXQpSnTqHXz6ZdBlMbbwLJXOhSPiYzxb5/v4CDg=="], - "react-native-windows/@react-native-community/cli/@react-native-community/cli-tools": ["@react-native-community/cli-tools@17.0.0", "", { "dependencies": { "@vscode/sudo-prompt": "^9.0.0", "appdirsjs": "^1.2.4", "chalk": "^4.1.2", "execa": "^5.0.0", "find-up": "^5.0.0", "launch-editor": "^2.9.1", "mime": "^2.4.1", "ora": "^5.4.1", "prompts": "^2.4.2", "semver": "^7.5.2" } }, "sha512-mVXH7U/uXd7yizqm2iE+W4SSVc4FGYYEafAu29HihA+FHttonqdg35zVAnIX2FKbyla+TotR1ACNSgo7KFDq+w=="], + "react-native-windows/@react-native-community/cli/@react-native-community/cli-tools": ["@react-native-community/cli-tools@20.0.0", "", { "dependencies": { "@vscode/sudo-prompt": "^9.0.0", "appdirsjs": "^1.2.4", "chalk": "^4.1.2", "execa": "^5.0.0", "find-up": "^5.0.0", "launch-editor": "^2.9.1", "mime": "^2.4.1", "ora": "^5.4.1", "prompts": "^2.4.2", "semver": "^7.5.2" } }, "sha512-akSZGxr1IajJ8n0YCwQoA3DI0HttJ0WB7M3nVpb0lOM+rJpsBN7WG5Ft+8ozb6HyIPX+O+lLeYazxn5VNG/Xhw=="], - "react-native-windows/@react-native-community/cli/@react-native-community/cli-types": ["@react-native-community/cli-types@17.0.0", "", { "dependencies": { "joi": "^17.2.1" } }, "sha512-NMOHp+PsA6WF4eCY+06U9X1VU4cjwKPzjbid3hzAQL9OwwcKQVqHTBjAU8xvVPGFQHWz8P/ZwpAwm2TT0k7jrA=="], + "react-native-windows/@react-native-community/cli/@react-native-community/cli-types": ["@react-native-community/cli-types@20.0.0", "", { "dependencies": { "joi": "^17.2.1" } }, "sha512-7J4hzGWOPTBV1d30Pf2NidV+bfCWpjfCOiGO3HUhz1fH4MvBM0FbbBmE9LE5NnMz7M8XSRSi68ZGYQXgLBB2Qw=="], "react-native-windows/@react-native-community/cli/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], + "react-native-windows/@react-native/codegen/@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="], + + "react-native-windows/@react-native/community-cli-plugin/@react-native/dev-middleware": ["@react-native/dev-middleware@0.83.4", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.83.4", "@react-native/debugger-shell": "0.83.4", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^4.4.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "serve-static": "^1.16.2", "ws": "^7.5.10" } }, "sha512-3s9nXZc/kj986nI2RPqxiIJeTS3o7pvZDxbHu7GE9WVIGX9YucA1l/tEiXd7BAm3TBFOfefDOT08xD46wH+R3Q=="], + "restore-cursor/onetime/mimic-fn": ["mimic-fn@1.2.0", "", {}, "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="], "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], @@ -3415,7 +3899,11 @@ "test-exclude/minimatch/brace-expansion": ["brace-expansion@1.1.21", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw=="], - "type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + "typeorm/yargs/cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], + + "typeorm/yargs/string-width": ["string-width@8.3.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ=="], + + "typeorm/yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], "username/execa/cross-spawn": ["cross-spawn@6.0.6", "", { "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw=="], @@ -3451,32 +3939,18 @@ "@babel/highlight/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], - "@expo/cli/glob/minimatch/brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], - "@expo/cli/glob/path-scurry/lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], - "@expo/config-plugins/glob/minimatch/brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], - "@expo/config-plugins/glob/path-scurry/lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], - "@expo/config/glob/minimatch/brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], - "@expo/config/glob/path-scurry/lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], "@expo/fingerprint/glob/path-scurry/lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], - "@expo/fingerprint/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - - "@expo/metro-config/glob/minimatch/brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], - "@expo/metro-config/glob/path-scurry/lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], - "@expo/prebuild-config/@expo/config-plugins/glob/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], - "@expo/prebuild-config/@expo/config-plugins/glob/path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], - "@expo/prebuild-config/@expo/config/glob/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], - "@expo/prebuild-config/@expo/config/glob/path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], @@ -3505,34 +3979,26 @@ "@react-native-windows/find-repo-root/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - "@react-native/community-cli-plugin/@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + "@react-native/babel-preset/@babel/plugin-transform-classes/@babel/traverse/@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="], - "@react-native/metro-babel-transformer/@react-native/babel-preset/@react-native/babel-plugin-codegen/@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="], + "@sentry/bundler-plugins/@sentry/cli/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - "expo-desktop-config-plugins/@expo/config-plugins/glob/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], + "@sentry/bundler-plugins/glob/path-scurry/lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], "expo-desktop-config-plugins/@expo/config-plugins/glob/path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], - "expo-desktop-prebuild-config/@expo/config-plugins/glob/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], + "expo-desktop-config-plugins/glob/minimatch/brace-expansion": ["brace-expansion@2.1.7", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g=="], "expo-desktop-prebuild-config/@expo/config-plugins/glob/path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], - "expo-desktop-prebuild-config/@expo/config/glob/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], - "expo-desktop-prebuild-config/@expo/config/glob/path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], - "expo-desktop/@expo/config-plugins/glob/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], - "expo-desktop/@expo/config-plugins/glob/path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], - "expo-desktop/@expo/config/glob/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], - "expo-desktop/@expo/config/glob/path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], "expo-desktop/@expo/fingerprint/glob/path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], - "expo-desktop/@expo/fingerprint/minimatch/brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], - "expo-desktop/@expo/metro/metro/@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="], "expo-desktop/@expo/metro/metro/@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="], @@ -3543,8 +4009,6 @@ "expo-desktop/@expo/metro/metro/hermes-parser": ["hermes-parser@0.35.0", "", { "dependencies": { "hermes-estree": "0.35.0" } }, "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA=="], - "expo-desktop/@expo/metro/metro/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - "expo-desktop/@expo/metro/metro/ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="], "expo-desktop/@expo/metro/metro-babel-transformer/hermes-parser": ["hermes-parser@0.35.0", "", { "dependencies": { "hermes-estree": "0.35.0" } }, "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA=="], @@ -3557,8 +4021,12 @@ "expo-desktop/@expo/metro/metro-transform-worker/@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="], + "expo-desktop/glob/minimatch/brace-expansion": ["brace-expansion@2.1.7", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g=="], + "get-monorepo-packages/globby/dir-glob/path-type": ["path-type@3.0.0", "", { "dependencies": { "pify": "^3.0.0" } }, "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg=="], + "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "log-symbols/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], "log-symbols/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], @@ -3579,18 +4047,34 @@ "ora/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], - "react-native-windows/@react-native-community/cli/@react-native-community/cli-doctor/@react-native-community/cli-platform-apple": ["@react-native-community/cli-platform-apple@17.0.0", "", { "dependencies": { "@react-native-community/cli-config-apple": "17.0.0", "@react-native-community/cli-tools": "17.0.0", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-xml-parser": "^4.4.1" } }, "sha512-2wJzZNDx3fzp0nDy/A/IKjhXrH/ouVMhEvzMD4kjbBp2v4CqASggKXcyFxqTeXccepl/anRcU0IIFlsfaNBACg=="], + "react-native-windows/@react-native-community/cli/@react-native-community/cli-doctor/@react-native-community/cli-platform-apple": ["@react-native-community/cli-platform-apple@20.0.0", "", { "dependencies": { "@react-native-community/cli-config-apple": "20.0.0", "@react-native-community/cli-tools": "20.0.0", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-xml-parser": "^4.4.1" } }, "sha512-rZZCnAjUHN1XBgiWTAMwEKpbVTO4IHBSecdd1VxJFeTZ7WjmstqA6L/HXcnueBgxrzTCRqvkRIyEQXxC1OfhGw=="], "react-native-windows/@react-native-community/cli/@react-native-community/cli-doctor/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], "react-native-windows/@react-native-community/cli/@react-native-community/cli-server-api/body-parser": ["body-parser@1.20.8", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.16.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA=="], - "react-native-windows/@react-native-community/cli/@react-native-community/cli-server-api/pretty-format": ["pretty-format@26.6.2", "", { "dependencies": { "@jest/types": "^26.6.2", "ansi-regex": "^5.0.0", "ansi-styles": "^4.0.0", "react-is": "^17.0.1" } }, "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg=="], + "react-native-windows/@react-native-community/cli/@react-native-community/cli-server-api/ws": ["ws@6.2.6", "", { "dependencies": { "async-limiter": "~1.0.0" } }, "sha512-XTrf1gv7kXoVf1hbC3PAyAiPgR8Wz1blcrYIjEsUmr08BLksT41R8KbjmS9408C2ERx7v1JDLD/BkpLEttjfKA=="], "react-native-windows/@react-native-community/cli/@react-native-community/cli-tools/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], + "react-native-windows/@react-native/community-cli-plugin/@react-native/dev-middleware/@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.83.4", "", {}, "sha512-mCE2s/S7SEjax3gZb6LFAraAI3x13gRVWJWqT0HIm71e4ITObENNTDuMw4mvZ/wr4Gz2wv4FcBH5/Nla9LXOcg=="], + + "react-native-windows/@react-native/community-cli-plugin/@react-native/dev-middleware/@react-native/debugger-shell": ["@react-native/debugger-shell@0.83.4", "", { "dependencies": { "cross-spawn": "^7.0.6", "fb-dotslash": "0.5.8" } }, "sha512-FtAnrvXqy1xeZ+onwilvxEeeBsvBlhtfrHVIC2R/BOJAK9TbKEtFfjio0wsn3DQIm+UZq48DSa+p9jJZ2aJUww=="], + + "react-native-windows/@react-native/community-cli-plugin/@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="], + "slice-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + "test-exclude/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "typeorm/yargs/cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "typeorm/yargs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "typeorm/yargs/cliui/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + + "typeorm/yargs/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "username/execa/cross-spawn/path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="], "username/execa/cross-spawn/semver": ["semver@5.7.2", "", { "bin": "bin/semver" }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], @@ -3603,20 +4087,8 @@ "@babel/highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - "@expo/cli/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - - "@expo/config-plugins/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - - "@expo/config/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - - "@expo/metro-config/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - - "@expo/prebuild-config/@expo/config-plugins/glob/minimatch/brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], - "@expo/prebuild-config/@expo/config-plugins/glob/path-scurry/lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], - "@expo/prebuild-config/@expo/config/glob/minimatch/brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], - "@expo/prebuild-config/@expo/config/glob/path-scurry/lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], @@ -3635,32 +4107,20 @@ "@react-native-windows/find-repo-root/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], - "@react-native/metro-babel-transformer/@react-native/babel-preset/@react-native/babel-plugin-codegen/@babel/traverse/@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="], - - "expo-desktop-config-plugins/@expo/config-plugins/glob/minimatch/brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], - "expo-desktop-config-plugins/@expo/config-plugins/glob/path-scurry/lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], - "expo-desktop-prebuild-config/@expo/config-plugins/glob/minimatch/brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], + "expo-desktop-config-plugins/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "expo-desktop-prebuild-config/@expo/config-plugins/glob/path-scurry/lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], - "expo-desktop-prebuild-config/@expo/config/glob/minimatch/brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], - "expo-desktop-prebuild-config/@expo/config/glob/path-scurry/lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], - "expo-desktop/@expo/config-plugins/glob/minimatch/brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], - "expo-desktop/@expo/config-plugins/glob/path-scurry/lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], - "expo-desktop/@expo/config/glob/minimatch/brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], - "expo-desktop/@expo/config/glob/path-scurry/lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], "expo-desktop/@expo/fingerprint/glob/path-scurry/lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], - "expo-desktop/@expo/fingerprint/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "expo-desktop/@expo/metro/metro-babel-transformer/hermes-parser/hermes-estree": ["hermes-estree@0.35.0", "", {}, "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg=="], "expo-desktop/@expo/metro/metro-source-map/@babel/traverse/@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="], @@ -3671,7 +4131,7 @@ "expo-desktop/@expo/metro/metro/hermes-parser/hermes-estree": ["hermes-estree@0.35.0", "", {}, "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg=="], - "expo-desktop/@expo/metro/metro/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + "expo-desktop/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "log-symbols/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], @@ -3681,7 +4141,7 @@ "ora/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - "react-native-windows/@react-native-community/cli/@react-native-community/cli-doctor/@react-native-community/cli-platform-apple/@react-native-community/cli-config-apple": ["@react-native-community/cli-config-apple@17.0.0", "", { "dependencies": { "@react-native-community/cli-tools": "17.0.0", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-glob": "^3.3.2" } }, "sha512-eMVe4aK2fsS0PcSWx7eI5snLP6J+N6jS4v625F+6K9GRzmxHF7cMFk3feFEmjreQxQm84OeZrJwnRnqhbnImIg=="], + "react-native-windows/@react-native-community/cli/@react-native-community/cli-doctor/@react-native-community/cli-platform-apple/@react-native-community/cli-config-apple": ["@react-native-community/cli-config-apple@20.0.0", "", { "dependencies": { "@react-native-community/cli-tools": "20.0.0", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-glob": "^3.3.2" } }, "sha512-PS1gNOdpeQ6w7dVu1zi++E+ix2D0ZkGC2SQP6Y/Qp002wG4se56esLXItYiiLrJkhH21P28fXdmYvTEkjSm9/Q=="], "react-native-windows/@react-native-community/cli/@react-native-community/cli-doctor/ora/cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], @@ -3699,35 +4159,25 @@ "react-native-windows/@react-native-community/cli/@react-native-community/cli-server-api/body-parser/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], - "react-native-windows/@react-native-community/cli/@react-native-community/cli-server-api/pretty-format/@jest/types": ["@jest/types@26.6.2", "", { "dependencies": { "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ=="], - - "react-native-windows/@react-native-community/cli/@react-native-community/cli-server-api/pretty-format/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "react-native-windows/@react-native-community/cli/@react-native-community/cli-server-api/pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], - "react-native-windows/@react-native-community/cli/@react-native-community/cli-tools/ora/cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], "react-native-windows/@react-native-community/cli/@react-native-community/cli-tools/ora/log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], "react-native-windows/@react-native-community/cli/@react-native-community/cli-tools/ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "username/execa/cross-spawn/shebang-command/shebang-regex": ["shebang-regex@1.0.0", "", {}, "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ=="], - - "@expo/prebuild-config/@expo/config-plugins/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "react-native-windows/@react-native/community-cli-plugin/@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - "@expo/prebuild-config/@expo/config/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "typeorm/yargs/cliui/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], - "@react-native-community/cli-platform-ios/@react-native-community/cli-platform-apple/@react-native-community/cli-tools/ora/cli-cursor/restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], - - "expo-desktop-config-plugins/@expo/config-plugins/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "typeorm/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="], - "expo-desktop-prebuild-config/@expo/config-plugins/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "typeorm/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - "expo-desktop-prebuild-config/@expo/config/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "typeorm/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="], - "expo-desktop/@expo/config-plugins/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "username/execa/cross-spawn/shebang-command/shebang-regex": ["shebang-regex@1.0.0", "", {}, "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ=="], - "expo-desktop/@expo/config/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "@react-native-community/cli-platform-ios/@react-native-community/cli-platform-apple/@react-native-community/cli-tools/ora/cli-cursor/restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], "logkitty/yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], @@ -3737,8 +4187,10 @@ "react-native-windows/@react-native-community/cli/@react-native-community/cli-server-api/body-parser/type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], - "react-native-windows/@react-native-community/cli/@react-native-community/cli-server-api/pretty-format/@jest/types/@types/yargs": ["@types/yargs@15.0.20", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-KIkX+/GgfFitlASYCGoSF+T4XRXhOubJLhkLVtSfsRTe9jWMmuM2g28zQ41BtPTG7TRBb2xHW+LCNVE9QR/vsg=="], + "react-native-windows/@react-native-community/cli/@react-native-community/cli-server-api/body-parser/type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "react-native-windows/@react-native-community/cli/@react-native-community/cli-tools/ora/cli-cursor/restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], + + "react-native-windows/@react-native-community/cli/@react-native-community/cli-server-api/body-parser/type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], } } diff --git a/docs/e2e-flows.md b/docs/e2e-flows.md index de6f1fac9..136d544c5 100644 --- a/docs/e2e-flows.md +++ b/docs/e2e-flows.md @@ -95,7 +95,8 @@ Every mapping form also takes: Every key in `when.matrix` must be a dimension and value from the header's `matrix`. `assertPaused` uses its own `when` argument (the paused state). To make it conditional, wrap it: `runFlow: { when: {…}, commands: [ … ] }`. -- `timeout`: overrides the implicit wait for the target (default 5s). +- `timeout`: overrides the implicit wait for the target (default 5s). A pointer `tapOn` also waits + within it for the screen to stop mounting, and taps anyway once it is spent. Some commands contain other commands: `repeat.commands`, `runFlow.commands`, `measure.run`, `assertNoLeaks.run`, `interruptAt.with`, and `during` in @@ -324,6 +325,14 @@ source once per run (`expo export:embed`, no Metro) and serves it to each launch build runs its embedded bundle. A flow whose `appId` is not the app's bundle identifier fails with `invalid`. +Add `--pointer` to `e2e:run --backend driver` to attach the macOS OS pointer backend for +`tapOn`. This opts into showing and activating target windows and requires Accessibility access. +It uses driver testIDs for lookup and real Quartz mouse events for input; it does not provide +an AX tree or other gestures. With `--pointer`, visibility checks use the pointer backend's +driver queries, with the same white-box testID semantics. The release gate does not accept +`--pointer`, so flows that need it live in `e2e/pointer/flows/`, outside the gate's `include`. +`bun run gate:pointer` runs them with `--backend driver --pointer` before a release. See [real pointer input](test-driver.md#real-pointer-input-macos). + | Flow | Driver | |---|---| | `launchApp` | Quits a running app, deletes its state for `clearState: true` ([clearState](#clearstate)), then launches in driver mode with the flow's `arguments` (after the driver's own) and `env`. Then it delivers `openUrl` with `navigate` and `openFile` with `openFile`, both through `application:openURLs:` once the first render settles. With `stopApp: false` and the app running, it only delivers `openUrl` and `openFile`; `clearState`, `arguments` and `env` there are `invalid`. `openFile` must be an absolute path to an existing file (`invalid` otherwise). The driver reserves `SPARK_TEST_DRIVER_DIR` and `SPARK_BUNDLE_URL` in `env`, `-RCT_jsLocation` in `arguments`, and `-AppleLanguages`/`-AppleLocale` when the matrix sets `locale`; a flow that sets one is `invalid`. `appId` and `from` are `unsupported`. | diff --git a/docs/expo-desktop-integration.md b/docs/expo-desktop-integration.md index d361e9eda..84b783429 100644 --- a/docs/expo-desktop-integration.md +++ b/docs/expo-desktop-integration.md @@ -1,6 +1,6 @@ # Expo Desktop integration -spark delegates project creation and native generation to the tested Expo Desktop release. The pinned CLI is `expo-desktop@1.0.6`; desktop native generation uses `expo-desktop-template-bare-minimum@54.81.6`. Config plugins are pinned to `expo-desktop-config-plugins@1.3.2`. Metro config is `54.81.3`; modules-core is `54.0.14` and stubs are `54.0.15`. Expo 54 / React Native 0.81 remain unchanged. Spark pins RN macOS `0.81.7` and RN Windows `0.81.35` in overrides as well as direct dependencies, preventing the upstream native template from installing conflicting desktop versions. +spark delegates project creation and native generation to the tested Expo Desktop release. The pinned CLI is `expo-desktop@1.0.6`; desktop native generation uses `expo-desktop-template-bare-minimum@55.83.10`. Config plugins are pinned to `expo-desktop-config-plugins@1.3.2`. Metro config is `55.83.3`; modules-core and stubs are `55.0.2`. The SDK is Expo 55 / React Native 0.83 / React 19.2. Spark pins RN macOS `0.83.0` and RN Windows `0.83.2` in overrides as well as direct dependencies, preventing the upstream native template from installing conflicting desktop versions. ## Ownership @@ -44,7 +44,7 @@ spark therefore retains desktop compilation and process ownership, module select ## React Native macOS Fabric lifecycle compatibility -The pinned `react-native-macos@0.81.7` starts Fabric surfaces asynchronously. An +The pinned `react-native-macos@0.83.0` starts Fabric surfaces asynchronously. An immediate secondary-window close can stop the shadow tree before startup finishes installing its animation driver. spark applies `fabric-lifecycle.cjs` from its config plugin during macOS Podfile generation, before compilation. It serializes @@ -68,7 +68,7 @@ session. See the ## RN macOS keyboard compatibility patch -The config plugin also patches `react-native-macos@0.81.7` keyboard event handling. +The config plugin also patches `react-native-macos@0.83.0` keyboard event handling. An internal legacy `RCTView` hosted inside Fabric (including WebView) can lack a React tag. Untagged keyboard events now return nil rather than constructing an invalid event. `RCTView` only marks JS delivery complete when an event and @@ -83,7 +83,7 @@ the native regression; see [macOS evidence](macos-readiness-2026-09-18.md#webvie ## RN macOS RTL ScrollView patch -In RTL, `react-native-macos@0.81.7` Fabric flips every ScrollView's content container +In RTL, `react-native-macos@0.83.0` Fabric flips every ScrollView's content container with a `scale(-1, 1)` layer transform. The matching flip of the scroll view itself, which cancels it on iOS, is iOS-only, so scroll content renders mirrored on macOS. Mirroring the NSScrollView as well is not an option: AppKit scrolls `NSClipView` by its @@ -100,7 +100,7 @@ compiles the patched methods against AppKit. ## RN macOS RTL text alignment patch -`react-native-macos@0.81.7` Fabric passes natural `textAlign` (`"auto"` or unset) to TextKit +`react-native-macos@0.83.0` Fabric passes natural `textAlign` (`"auto"` or unset) to TextKit as `NSTextAlignmentNatural`. TextKit resolves that from the user's language and the app's localization, not from the Yoga layout direction. So in an RTL subtree of an app that isn't localized for an RTL language (such as `direction: "rtl"`), text in a full-width box aligns @@ -116,7 +116,7 @@ upstream code and lays text out with TextKit. ## RN macOS RTL TextInput alignment patch -A `react-native-macos@0.81.7` TextInput has no layout direction on macOS: its text attributes +A `react-native-macos@0.83.0` TextInput has no layout direction on macOS: its text attributes carry none, and the `semanticContentAttribute` that makes UIKit's natural alignment follow it is iOS-only. So a TextInput with natural `textAlign` drew its text and placeholder from the left in an RTL layout. `rtl-text-input-alignment.cjs` aligns them, single-line and multiline, @@ -128,7 +128,7 @@ compiles the patch's helpers and checks text and placeholders in `NSTextField` a ## RN macOS TextInput accessibility patch -`react-native-macos@0.81.7` Fabric sets a TextInput's `accessibilityLabel` on the backed text input view. A multiline +`react-native-macos@0.83.0` Fabric sets a TextInput's `accessibilityLabel` on the backed text input view. A multiline input's is `RCTWrappedTextView`, a plain view around a scroll view, while the `NSTextView` inside it is the accessibility element, so VoiceOver announced a labelled multiline input without its label. `text-input-accessibility.cjs` keeps the label on that text view. Like the other patches, it checks the version and @@ -137,7 +137,7 @@ exact source, is idempotent, and replaces the file atomically; existing runtimes ## RN macOS accessibility state patch -`react-native-macos@0.81.7` Fabric applies `accessibilityState` only on iOS: in `RCTViewComponentView.mm` it is inside +`react-native-macos@0.83.0` Fabric applies `accessibilityState` only on iOS: in `RCTViewComponentView.mm` it is inside `#if !TARGET_OS_OSX`. So a disabled `Pressable` (`disabled`, `accessibilityState.disabled`) with an `accessibilityRole` told VoiceOver it was enabled, a selected one (`accessibilityState.selected`) that it was not selected, and a view without a role reported `isAccessibilityEnabled` NO, as a plain `NSView` does, enabled or not. diff --git a/docs/external-libraries.md b/docs/external-libraries.md index 92c8b3cdb..b0e6d1372 100644 --- a/docs/external-libraries.md +++ b/docs/external-libraries.md @@ -38,6 +38,55 @@ Our documentation provides a shared place to discover integrations, with attribu | [React Native WebView](https://github.com/react-native-webview/react-native-webview) | `react-native-webview` | macOS WebKit and upstream Windows Fabric WebView2; pinned SDK version with explicit Windows fixes. See [WebView integration](desktop-api-expansion.md#webview-and-sqlite). | | [OP-SQLite](https://github.com/OP-Engineering/op-sqlite) | `@op-engineering/op-sqlite` | Native SQLite; Windows RNW JSI adapter with Unicode paths and per-runtime installation state; macOS production selection. See [SQLite integration](desktop-api-expansion.md#webview-and-sqlite). | | [VisionCamera](https://github.com/mrousavy/react-native-vision-camera) | `react-native-vision-camera` | App-owned dependency with automatic version-pinned macOS patches for VisionCamera, Nitro Image, and shared Nitro. See [Camera](camera.md). | +| [MMKV](https://github.com/mrousavy/react-native-mmkv) | `react-native-mmkv` | Install 4.3.2 in the app. Spark patches its macOS storage folder from `~/Documents/mmkv` to the app's namespaced Application Support folder; other versions build unpatched. | +| [Unistyles](https://github.com/jpudysz/react-native-unistyles) | `react-native-unistyles` | Install 3.6.0 in the app. Spark patches in an AppKit runtime: window size, backing scale, appearance (adaptive themes) and RTL; insets, status/navigation bars and the keyboard report zero. | +| [Quick Crypto](https://github.com/margelo/react-native-quick-crypto) | `react-native-quick-crypto` | Install 1.1.9 with `react-native-quick-base64` 3.0.1. Spark patches Quick Crypto's PBKDF2 `endian.h` include for macOS and adds macOS to Quick Base64's podspec. | +| [Nitro Fetch](https://github.com/margelo/react-native-nitro-fetch) | `react-native-nitro-fetch` | Install 1.9.0. Spark adds macOS to its podspec and runs its launch prefetch on `NSApplicationDidFinishLaunching`. Its peer range asks for Nitro 0.37.1; it runs on Spark's 0.37.0. | +| [Blur](https://github.com/Kureev/react-native-blur) | `@react-native-community/blur` | Install 4.4.1. Spark patches in AppKit `BlurView`/`VibrancyView` on `NSVisualEffectView` materials (`blurAmount` has no macOS equivalent; materials follow the window's active state; `VibrancyView` children mount inside the material, but React Native views don't opt into AppKit vibrancy, so they draw unblended) and declares the components in its codegen config instead of relying on React Native's deprecated source scan. | +| [Worklets Core](https://github.com/margelo/react-native-worklets-core) | `react-native-worklets-core` | Install 1.6.3. Spark adds macOS to its podspec; the native code is platform-neutral, and on the bridgeless host it reaches the runtime through React Native's deprecated `RCTBridgeProxy` interop. Its Babel plugin needs `@babel/plugin-transform-shorthand-properties`, `@babel/plugin-transform-arrow-functions`, `@babel/plugin-proposal-optional-chaining`, `@babel/plugin-proposal-nullish-coalescing-operator` and `@babel/plugin-transform-template-literals` installed, as on iOS; scope it to your worklet files if the app also uses Skia without Reanimated. | +| [FastImage](https://github.com/DylanVann/react-native-fast-image) | `react-native-fast-image` | Install 10.9.9. Its native view is UIKit only, so on macOS Spark adds a JavaScript stand-in on React Native's `Image`: sources, `objectFit`, `tintColor`, placeholders while loading, load events, `reload()` and `preload`. Its own caches, thumbhash/blurhash placeholders, transitions and cache APIs are not available on macOS. | +| [Release Profiler](https://github.com/margelo/react-native-release-profiler) | `react-native-release-profiler` | Install 0.4.5. Spark adds macOS to its podspec and writes traces to the app's namespaced Caches folder instead of the shared `~/Library/Caches`. | +| [Tracking Transparency](https://github.com/mrousavy/react-native-tracking-transparency) | `react-native-tracking-transparency` | Works on macOS without a patch: it reports `unavailable`. App Tracking Transparency is a no-op on the Mac (Apple's header: the status is always `not-determined` and requests never prompt), so there is nothing native to enable. | +| [Skia](https://github.com/Shopify/react-native-skia) | `@shopify/react-native-skia` | Supports macOS upstream (2.14.0 verified, no patch). Without Reanimated, keep other worklet Babel plugins (such as Worklets Core's) off Skia's sources: they read Skia's optional Reanimated proxy at load and the app never renders. | +| [Blurhash](https://github.com/mrousavy/react-native-blurhash) | `react-native-blurhash` | Install 2.1.3. Spark patches in an AppKit view (layer contents with `resizeMode` as contents gravity), the NSImage decoder and encoder, and the codegen component provider. | +| [Screen Corner Radius](https://github.com/mrousavy/react-native-screen-corner-radius) | `react-native-screen-corner-radius` | Install 0.2.3. Its native module is iOS only; Spark's patch makes the import report 0 instead of throwing where the module is missing. | +| [Nitro SQLite](https://github.com/margelo/react-native-nitro-sqlite) | `react-native-nitro-sqlite` | Supports macOS upstream (10.2.0 verified, no patch). Databases live in `~/Library/Application Support/`. Its SQLite symbols are prefixed, so it links beside Spark's OP-SQLite. | +| [App Logs](https://github.com/kirillzyusko/react-native-app-logs) | `react-native-app-logs` | Install 0.3.1. Spark adds macOS 14+ to its podspec and points macOS at its iOS bindings; it reads the app's own entries from OSLogStore. | +| [Nitro ZXing](https://github.com/margelo/react-native-vision-camera-zxing) | `react-native-nitro-zxing` | Install 0.3.0. Spark adds macOS to its podspec; it builds on Spark's patched VisionCamera 5 and Nitro Image (its peers; Spark pins them, so declare them only to import them directly), and scans still images (verified) and camera frames. | +| [Safe Area Context](https://github.com/AppAndFlow/react-native-safe-area-context) | `react-native-safe-area-context` | Supports macOS upstream (5.6.2, Expo SDK 55's pin, verified; no patch). Insets come from the provider view's `NSView.safeAreaInsets`: 0 in a standard window. | +| [Keep Awake](https://docs.expo.dev/versions/latest/sdk/keep-awake/) | `expo-keep-awake` | Declare 55.0.8 in the app. Upstream builds on macOS but does nothing there; Spark's patch holds a `ProcessInfo` activity that disables idle display and system sleep while activated. | +| [Screens](https://github.com/software-mansion/react-native-screens) | `react-native-screens` | Install 4.23.0. Its native containers are UIKit only; on macOS Spark points every component with a web version at that plain-view implementation, and keeps the iOS-only pod out of codegen. | +| [Reanimated](https://github.com/software-mansion/react-native-reanimated) | `react-native-reanimated`, `react-native-worklets` | Supports macOS upstream (4.2.1 and 0.7.4, Expo SDK 55's pins, verified; no patch). `babel-preset-expo` adds the worklets Babel plugin when `react-native-worklets` is installed. | +| [Gesture Handler](https://github.com/software-mansion/react-native-gesture-handler) | `react-native-gesture-handler` | Supports macOS upstream (2.30.0, Expo SDK 55's pin; no patch). Verified registration and mounting; pointer gestures were checked by hand only. | +| [React Navigation](https://reactnavigation.org) | `@react-navigation/native`, `@react-navigation/native-stack`, `@react-navigation/bottom-tabs` | Install 7.5.1 / 7.21.0 / 7.21.0 with Screens and Safe Area Context. Spark points native-stack at its JavaScript stack view on macOS, which draws headers; the native one relies on UIKit headers. | +| [Expo Crypto](https://docs.expo.dev/versions/latest/sdk/crypto/) | `expo-crypto` | Supports macOS upstream (55.0.19 verified; no patch). | +| [Expo Linear Gradient](https://docs.expo.dev/versions/latest/sdk/linear-gradient/) | `expo-linear-gradient` | Supports macOS upstream (55.0.18 verified; no patch). | +| [Expo Web Browser](https://docs.expo.dev/versions/latest/sdk/webbrowser/) | `expo-web-browser` | Declare 55.0.20 in the app. It opens pages in an in-app WebKit window on macOS upstream; Spark's patch makes `dismissBrowser` resolve `openBrowserAsync` with `dismiss`, as on iOS, instead of `cancel`. | +| [Expo Application](https://docs.expo.dev/versions/latest/sdk/application/) | `expo-application` | Declare 55.0.19 in the app. Spark adds macOS to its podspec; installation time comes from the app bundle (not `~/Documents`), the vendor ID is `null` and the release type `UNKNOWN`. | +| [Expo Device](https://docs.expo.dev/versions/latest/sdk/device/) | `expo-device` | Declare 55.0.21 in the app. Spark adds macOS answers: model ID from `hw.model`, `macOS` and its version, the computer name, desktop device type. `modelName` and `deviceYearClass` are `null`. | +| [Expo Localization](https://docs.expo.dev/versions/latest/sdk/localization/) | `expo-localization` | Declare 55.0.19 in the app. Spark adds macOS to its podspec and listens for time-zone changes in place of iOS's significant-time-change notification. | +| [Expo Haptics](https://docs.expo.dev/versions/latest/sdk/haptics/) | `expo-haptics` | Declare 55.0.18 in the app. Unpatched, every call rejects as unavailable on macOS; Spark's patch plays them through `NSHapticFeedbackManager` (felt on Force Touch trackpads). | +| [SVG](https://github.com/software-mansion/react-native-svg) | `react-native-svg` | Install 15.15.3. It targets macOS, but with React Native macOS 0.83 it neither compiled (a missing `RCTAssert` import) nor drew (component views update their layer, so AppKit skipped its `drawRect:`); Spark's patch fixes both. | +| [NetInfo](https://github.com/react-native-netinfo/react-native-netinfo) | `@react-native-community/netinfo` | Supports macOS upstream (11.5.2 verified; no patch). | +| [Expo Font](https://docs.expo.dev/versions/latest/sdk/font/) | `expo-font` | Declare 55.0.8 in the app. On macOS upstream didn't resolve font-family aliases (its swizzle is UIKit-only), so a font loaded as `material-community` fell back to the system font; Spark's patch resolves aliases through `NSFontManager`. | +| [Vector Icons](https://docs.expo.dev/guides/icons/) | `@expo/vector-icons` | Works on macOS (15.1.1) with Spark's expo-font patch; icon sets whose font family differs from their alias (Material Community Icons and others) need it. | +| [Drawer Layout](https://github.com/react-navigation/react-navigation/tree/main/packages/react-native-drawer-layout) | `react-native-drawer-layout` | JavaScript on Reanimated and Gesture Handler; works on macOS (4.2.12 verified). | +| [Bottom Sheet](https://github.com/gorhom/react-native-bottom-sheet) | `@gorhom/bottom-sheet` | JavaScript on Reanimated and Gesture Handler; works on macOS (5.2.14 verified). | +| [Expo Blur](https://docs.expo.dev/versions/latest/sdk/blur-view/) | `expo-blur` | Declare 55.0.18 in the app. Upstream is iOS, Android and web only; Spark's patch backs `BlurView` on macOS with `NSVisualEffectView`, mapping `tint` to a material and appearance and `intensity` to opacity. `blurReductionFactor` and `experimentalBlurMethod` are Android-only. | +| [Expo Image](https://docs.expo.dev/versions/latest/sdk/image/) | `expo-image` | Declare 55.0.11 in the app. Its native view is UIKit only, so on macOS Spark adds a JavaScript stand-in on React Native's `Image`: sources (the largest of several sizes), placeholders while loading, `contentFit`, `tintColor`, load events, `reloadAsync` and `Image.prefetch`. Its SDWebImage caches, blurhash, thumbhash and SF Symbol sources (these report `onError`), `contentPosition`, transitions, `useImage`/`Image.loadAsync` and hash generation are not available on macOS, and a tint fills the whole view rather than only the fitted image. | +| [Expo Symbols](https://docs.expo.dev/versions/latest/sdk/symbols/) | `expo-symbols` | Declare 55.0.9 in the app. Spark's patch draws `SymbolView` with AppKit's SF Symbols: names, sizes, weights, scales, tint, the monochrome, hierarchical, palette and multicolor types, and symbol effects. `NSImageView` only stretches, fits or draws at natural size, so `resizeMode` `scaleAspectFill` fits and the edge-aligned modes draw centered. | +| [Masked View](https://github.com/react-native-masked-view/masked-view) | `@react-native-masked-view/masked-view` | Declare 0.3.2 in the app. AppKit has no `maskView`, so Spark's patch keeps the mask element drawing in an invisible host and renders it into the children's layer mask once per display frame while the view is on screen; masks follow the mask element's alpha, including text and animated masks, at the cost of one offscreen render per frame. | +| [Graph](https://github.com/margelo/react-native-graph) | `react-native-graph` | Declare 1.4.0 in the app. Upstream builds its paths with Skia 1's mutable `SkPath`, which Skia 2 removed, so it threw on any platform with Spark's Skia; Spark's patch builds them with `Skia.PathBuilder`. Static and animated graphs, gradient fills and the pan gesture use the package's own code. | +| [Expo Glass Effect](https://docs.expo.dev/versions/latest/sdk/glass-effect/) | `expo-glass-effect` | Declare 55.0.11 in the app. Spark's patch hosts `GlassView` in AppKit's `NSGlassEffectView` and `GlassContainer` in `NSGlassEffectContainerView` on macOS 26 (earlier macOS shows the children without glass): regular and clear styles, `none`, tint, color scheme, corner radius (one radius; per-corner values use the largest) and container spacing. `isInteractive` and style animations have no AppKit equivalent. Spark's own `@legendapp/spark/ui/glass` covers the same view. | +| [Splash Screen](https://docs.expo.dev/versions/latest/sdk/splash-screen/) | `expo-splash-screen` | Works on macOS (55.0.25) without a patch: a desktop app has no launch splash, so `setOptions`, `preventAutoHideAsync`, `hide` and `hideAsync` resolve without effect. | +| [System UI](https://docs.expo.dev/versions/latest/sdk/system-ui/) | `expo-system-ui` | Declare 55.0.22 in the app. Spark's patch sets and reads the main window's background color on macOS (the app delegate's window, as on iOS); `null` restores the default window background. Other windows keep Spark's `backgroundColor` window option. | +| [Image Picker](https://docs.expo.dev/versions/latest/sdk/imagepicker/) | `expo-image-picker` | Declare 55.0.24 in the app. macOS has no photo library or camera picker, so Spark's patch picks image and movie files with an open panel (sheet on the key window): `mediaTypes`, `allowsMultipleSelection`, `selectionLimit` and `base64` apply, and assets carry the picked file's URI, size, dimensions, MIME type and video duration. The library permission is always granted; camera permission reports AVFoundation's status, and `launchCameraAsync` rejects as unavailable. Editing, quality, EXIF and video export options don't apply. | +| [Image Manipulator](https://docs.expo.dev/versions/latest/sdk/imagemanipulator/) | `expo-image-manipulator` | Declare 55.0.21 in the app. Spark's patch draws with CoreGraphics on macOS: resize, rotate, flip, crop and saving as JPEG, PNG or WebP (with base64) work, EXIF orientation is applied on load, and saved files go to a per-app folder in Caches. Photo library (`ph://`) sources are iOS only. | +| [Location](https://docs.expo.dev/versions/latest/sdk/location/) | `expo-location` | Declare 55.1.15 in the app and add `NSLocationWhenInUseUsageDescription` to the macOS Info.plist before requesting permission; without it, permission reports denied. Spark's patch builds it for macOS's CoreLocation: permissions, Location Services and provider status, current and watched positions, and geocoding. Macs have no compass, so heading rejects; background-only options (`activityType`, `pausesUpdatesAutomatically`, the background indicator) have no macOS equivalent, and macOS reports only "always" authorization. | +| [Date Time Picker](https://github.com/react-native-datetimepicker/datetimepicker) | `@react-native-community/datetimepicker` | Install 9.2.2. Spark's patch hosts the picker in AppKit's `NSDatePicker`: date, time and date-and-time modes, `display="inline"` as a calendar and clock, minimum and maximum dates, locale, time zone, text color, theme variant and `disabled`. Countdown mode shows a time picker; spinner and compact show the stepper field; `minuteInterval`, `accentColor` and `onDismiss` have no macOS equivalent. | +| [Keyboard Controller](https://github.com/kirillzyusko/react-native-keyboard-controller) | `react-native-keyboard-controller` | Install 1.22.9. Its native module is iOS and Android only and threw at import on macOS; Spark's patch selects the package's own web bindings there, so its components and hooks render and report the keyboard hidden. A Mac has no software keyboard to avoid, so keyboard events never fire. | +| [Sentry](https://docs.sentry.io/platforms/react-native/) | `@sentry/react-native` | Declare 8.32.0 in the app. Its native module and sentry-cocoa build for macOS; Spark's patch keeps the session replay mask views as plain containers there, because codegen still registers them and a missing class aborted the app at launch. Errors carry native macOS device and OS context; session replay is iOS only. | +| [Expo Router](https://docs.expo.dev/router/introduction/) | `expo-router` | Works on macOS (55.0.18) without a patch: file-based routes, `Stack`, dynamic segments, search params and `Link` run on React Navigation and Spark's react-native-screens patch. Its native module (native tabs, toolbar, link previews, `expo-router/head`) is iOS only. Wrap layouts in React Navigation's `ThemeProvider` to follow the app's appearance. | The `/webview` export owns its source, props, events and ref types. Its adapter currently delegates to React Native WebView; applications needing the complete upstream surface can import that package directly. Spark does not forward arbitrary backend props. The `/sqlite` adapter owns `Database`, `SqlExecutor`, row/value and mutation result types. It adds project-scoped storage, queued queries, transaction lifetime and safe numeric/blob conversion; OP-SQLite objects do not cross its public boundary. Specialized OP-SQLite features remain direct upstream integrations. Neither adapter is a template for wrapping new libraries automatically. diff --git a/docs/releases.md b/docs/releases.md index d44fcaa97..89b5d5607 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -274,6 +274,8 @@ Promoting npm `latest` does not change the platform support listed above. `e2e/gate.yaml` (see [Release gate](e2e-flows.md#release-gate)). It fails today: no e2e backend exists yet and coverage is incomplete. `bun run release` does not run it yet ([#55](https://github.com/LegendApp/legend-spark/issues/55)). +- On macOS, also run the pointer flows the gate leaves out: + `bun run gate:pointer --app ` (needs Accessibility access). - Verify the configured GitHub repository visibility and npm identity before publishing; do not change either as an implicit release step. - For signed production Runner distribution, configure the authorized Developer diff --git a/docs/test-driver.md b/docs/test-driver.md index 54cd4b2b0..3c8c93ab9 100644 --- a/docs/test-driver.md +++ b/docs/test-driver.md @@ -2,7 +2,7 @@ The release gate drives and screenshots Spark apps from inside the app process. Agents running the gate have no Screen Recording, Accessibility or input-synthesis (TCC) -permissions, so `screencapture`, ScreenCaptureKit, AX and CGEvent are not options. +permissions by default. The optional pointer backend below requires an Accessibility grant. The driver (`packages/desktop-app/macos/SparkTestDriver*.mm`, started by the `@legendapp/spark-desktop-host` app delegate) is compiled into Debug builds of every macOS Spark app, @@ -103,7 +103,7 @@ await driver.close(); ## Focus and window state -A driven app never takes focus from the person using the Mac: +By default, a driven app never takes focus from the person using the Mac: - Activation policy `NSApplicationActivationPolicyProhibited`: no Dock icon, no menu bar, cannot activate. @@ -121,6 +121,39 @@ A driven app never takes focus from the person using the Mac: Windows that app code opens itself (for example with `@legendapp/spark/windows`) are still ordered on screen, without activation. +### Real pointer input (macOS) + +Run `bun run e2e:run --backend driver --pointer` to enable `tapOn`. Pointer flows +live in `e2e/pointer/flows/`, outside the release gate; `bun run gate:pointer` runs them all +before a release. This explicitly +allows the driver to show the target window, change the activation policy from Prohibited +to Regular, and activate the app for foreground clicks. At flow end (including failure), +`restorePointer` hides windows the driver showed, restores their animation behavior and the +activation policy, and reactivates the previously foreground app. Disconnect also restores +this state before quitting. Ordinary driver runs retain the hidden-window behavior above. + +The driver waits for mounted UI updates to settle before preparing pointer input, allowing +native gesture recognizers to attach to their views. `tapOn` waits up to its timeout; on a +screen that keeps mounting, such as a spinner, it taps once the timeout is spent. +The pointer backend resolves `id` selectors through the existing driver, so element lookup +is white-box; the click itself is OS input, posted as Quartz left mouse-down/up events by +a small native helper using `CGEventPostToPid`. It does not invoke a view action or a +JavaScript callback. Modifiers and other gestures are unsupported. The driver translates +its captured-frame coordinates to AppKit window points after showing the window, including +the title bar and Y flip. It constructs and serializes the mouse events in the window-owning +process, where AppKit can resolve the window number and local hit point. The helper decodes +and posts those events to that process without a CGWindow search. +Foreground clicks work with React Native Pressable and gesture-handler targets without +per-view `acceptsFirstMouse` props. Each tap uses one monotonic event number shared by its +down and up events so React Native can pair the press and release. + +Grant the e2e runner Accessibility access in System Settings > Privacy & Security > +Accessibility. The helper is compiled once per run and checks `CGPreflightPostEventAccess` +at each flow start, before launching the app; +missing permission fails with `E_PERMISSION_DENIED`, without prompting or waiting for a grant. +Screen Recording permission is unnecessary. Posting events is not proof that a control +handled them; flows must assert the resulting callback or state change. + ## Protocol Newline-delimited JSON. After `{"token": "..."}` (answered `{"ok": true, "protocol": 1}`), @@ -135,6 +168,8 @@ send one request at a time; each reply echoes `id`. Failures are | `waitFor` | `testID`, `timeoutMs` (10000) | `window` identifier and `frame` of the first visible view whose accessibility identifier (React Native `testID`) matches. `frame` is in points from the top-left of the window's frame view, the area `capture` renders, so frame × `scale` is the view's pixels in a capture. `E_TIMEOUT` otherwise; `timeoutMs: 0` checks once. | | `setAppAppearance` | `appearance` (`light`/`dark`), `timeoutMs` (10000) | Waits for the first rendered UI to settle (an override set while the app is still launching did not last), then sets `NSApp.appearance` (an app override; the OS setting is untouched; per-window appearances still win). If the effective appearance changed, replies `changed: true` once a UI update has mounted and settled, else `E_TIMEOUT`. `timeoutMs` bounds each wait. | | `capture` | `window` (`main`, `key`, `id:`, `title:`), `name`, `timeoutMs` (5000) | `file`, `width`, `height` (pixels), `scale`, once mounted UI updates have settled. `E_NOT_SETTLED` if they never do. | +| `restorePointer` | | Restores window visibility, animation and app activation state changed by `pointerEvents`. Safe to repeat; also runs on disconnect. | +| `pointerEvents` | `window` (same as `capture`), `x`, `y` (top-left captured-frame points), `timeoutMs` (5000; 0 does not wait) | Validates the window and point, waits for mounted UI updates to settle, shows and activates the window, then returns `pid` and base64-encoded `down`/`up` CGEvent data for that window and point. Explicitly opts out of hidden-window behavior. `E_NOT_SETTLED`, `E_NO_WINDOW`, `E_INVALID_ARGUMENT`, or `E_EVENT` on failure. Posts no input itself. | | `quit` | | `ok`, then the session ends and the app quits. | "Settled" means no Fabric mount transaction for 100 ms. The host reports mounts through diff --git a/docs/windows-issues.md b/docs/windows-issues.md index 5813c793f..95af8ced4 100644 --- a/docs/windows-issues.md +++ b/docs/windows-issues.md @@ -145,7 +145,7 @@ owner. The platform runner's prepare-only mode is available on macOS, including fail on destruction/load failure or after 120 seconds, rather than hanging. The current macOS guide does not claim validated threaded UI; Windows threaded surfaces likewise require explicit visual, input and lifecycle acceptance. -- The pinned RNW 0.81.35 geometry adapter exposes hit testing/client bounds for +- The pinned RNW 0.83.2 geometry adapter exposes hit testing/client bounds for drag/drop without changing existing interface IDs. It fails explicitly if the pinned source changes. Native geometry, clipping and scrolling need Windows checks. Expo Desktop beta still owns project generation. diff --git a/e2e/checks/app-lifecycle.yaml b/e2e/checks/app-lifecycle.yaml index 0adc4598e..1e38bf5b0 100644 --- a/e2e/checks/app-lifecycle.yaml +++ b/e2e/checks/app-lifecycle.yaml @@ -46,3 +46,8 @@ checks: spec: "#60 LIFE.api" covers: - ./app#getAppContext + LIFE-SPLASH-01: + title: expo-splash-screen's API resolves without effect on macOS, which has no launch splash + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/expo-splash-screen.md" diff --git a/e2e/checks/appearance-theming.yaml b/e2e/checks/appearance-theming.yaml index 59c4d3100..392bf88b4 100644 --- a/e2e/checks/appearance-theming.yaml +++ b/e2e/checks/appearance-theming.yaml @@ -23,3 +23,13 @@ checks: spec: "#97 APPR.api" covers: - ./system#getSystemAvailability + APPR-UNISTYLES-01: + title: react-native-unistyles reports the window size and pixel ratio on macOS and its adaptive theme follows the app's appearance (dark, then light) + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/react-native-unistyles.md" + APPR-SYSTEMUI-01: + title: expo-system-ui sets, reads and resets the main window's background color on macOS + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/expo-system-ui.md" diff --git a/e2e/checks/component-library.yaml b/e2e/checks/component-library.yaml new file mode 100644 index 000000000..5549bdfce --- /dev/null +++ b/e2e/checks/component-library.yaml @@ -0,0 +1,53 @@ +area: component-library +prefix: COMP +checks: + COMP-SCREENS-01: + title: react-native-screens' Screen, ScreenContainer and ScreenStack render on macOS as plain views; an inactive screen stays hidden until it becomes active + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/react-native-screens.md" + COMP-REANIMATED-01: + title: react-native-reanimated runs a timing animation and frame callbacks on the UI thread on macOS and reports back to JavaScript + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/react-native-reanimated.md" + COMP-GESTURE-01: + title: react-native-gesture-handler's native module and button component are registered on macOS and a GestureDetector handles a tap in a GestureHandlerRootView + platforms: [macos] + blocking: false + spec: "vault tasks/spark/desktop-libs/react-native-gesture-handler.md" + COMP-NAVIGATION-01: + title: React Navigation's native stack shows a header and navigates forward and back on macOS, and bottom tabs switch tabs + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/react-navigation.md" + COMP-DRAWER-01: + title: react-native-drawer-layout opens and closes a drawer on macOS, reporting onOpen and onClose + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/react-native-drawer-layout.md" + COMP-SHEET-01: + title: "@gorhom/bottom-sheet snaps to its second point on macOS and reports the index" + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/gorhom-bottom-sheet.md" + COMP-PRESSABLE-01: + title: react-native-pressable-opacity's PressableOpacity and NativePressableOpacity render on macOS + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/react-native-pressable-opacity.md" + COMP-STATUSBAR-01: + title: expo-status-bar's component renders and its setters return on macOS, where there is no status bar + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/expo-status-bar.md" + COMP-EXPOROUTER-01: + title: expo-router renders file-based routes on macOS and pushes a dynamic route with search params + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/expo-router.md" + COMP-POINTER-01: + title: React Native Pressable handles successive OS mouse clicks on macOS, pairing each down and up + platforms: [macos] + blocking: false + spec: "docs/test-driver.md#real-pointer-input-macos" diff --git a/e2e/checks/devtools.yaml b/e2e/checks/devtools.yaml new file mode 100644 index 000000000..f1e51b062 --- /dev/null +++ b/e2e/checks/devtools.yaml @@ -0,0 +1,18 @@ +area: devtools +prefix: DEVT +checks: + DEVT-PROFILER-01: + title: react-native-release-profiler starts and stops the Hermes sampling profiler on macOS and writes a trace with events to the app's namespaced Caches folder + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/react-native-release-profiler.md" + DEVT-APPLOGS-01: + title: react-native-app-logs reads the app's own unified-log entries on macOS and delivers ones matching a filter to JavaScript + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/react-native-app-logs.md" + DEVT-SENTRY-01: + title: "@sentry/react-native captures an error with a stack trace and native macOS context on macOS" + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/sentry-react-native.md" diff --git a/e2e/checks/displays-graphics.yaml b/e2e/checks/displays-graphics.yaml new file mode 100644 index 000000000..069526e89 --- /dev/null +++ b/e2e/checks/displays-graphics.yaml @@ -0,0 +1,73 @@ +area: displays-graphics +prefix: DISP +checks: + DISP-BLUR-01: + title: "@react-native-community/blur registers native BlurView and VibrancyView components on macOS and lays out blur materials over content, with VibrancyView children inside the material" + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/react-native-community-blur.md" + DISP-FASTIMAGE-01: + title: react-native-fast-image renders through Spark's macOS stand-in, reports onLoad with the image size, reloads through its ref and preloads with the size + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/react-native-fast-image.md" + DISP-SKIA-01: + title: "@shopify/react-native-skia draws a declarative Canvas in its macOS native view and an offscreen raster surface, both read back with the expected pixels" + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/shopify-react-native-skia.md" + DISP-BLURHASH-01: + title: react-native-blurhash decodes a Blurhash in its native view on macOS and encodes a local image to a valid Blurhash with the image's color + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/react-native-blurhash.md" + DISP-CORNER-01: + title: react-native-screen-corner-radius imports on macOS without its iOS-only native module and reports a 0 radius and square corners + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/react-native-screen-corner-radius.md" + DISP-GRADIENT-01: + title: expo-linear-gradient's native module is linked on macOS and its gradient view lays out + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/expo-linear-gradient.md" + DISP-SVG-01: + title: react-native-svg's native components are registered on macOS and an SVG with shapes, a path, a gradient and text lays out + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/react-native-svg.md" + DISP-ICONS-01: + title: "@expo/vector-icons loads its icon fonts through expo-font on macOS and draws the glyphs" + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/expo-vector-icons.md" + DISP-EXPOBLUR-01: + title: expo-blur's native module is linked on macOS and its BlurViews lay out as visual-effect materials + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/expo-blur.md" + DISP-EXPOIMAGE-01: + title: expo-image renders on macOS through Spark's stand-in, with load events, reloadAsync and prefetch + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/expo-image.md" + DISP-SYMBOLS-01: + title: expo-symbols draws SF Symbols natively on macOS in monochrome, hierarchical, palette and multicolor styles + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/expo-symbols.md" + DISP-MASKED-01: + title: "@react-native-masked-view/masked-view masks its children by the mask element's alpha on macOS" + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/masked-view.md" + DISP-EXPOGLASS-01: + title: expo-glass-effect hosts GlassView and GlassContainer children in AppKit glass on macOS 26 + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/expo-glass-effect.md" + DISP-GRAPH-01: + title: react-native-graph renders static and animated line graphs on macOS with Skia 2 + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/react-native-graph.md" diff --git a/e2e/checks/internationalization.yaml b/e2e/checks/internationalization.yaml index 7e7153b51..d019a5bca 100644 --- a/e2e/checks/internationalization.yaml +++ b/e2e/checks/internationalization.yaml @@ -6,3 +6,8 @@ checks: platforms: [macos] blocking: true spec: "docs/e2e-flows.md Visual goldens; #227" + I18N-EXPOLOCALIZATION-01: + title: expo-localization returns the user's preferred locale and calendar with a time zone on macOS + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/expo-localization.md" diff --git a/e2e/checks/keyboard.yaml b/e2e/checks/keyboard.yaml index 9f5e647de..04bfd48d2 100644 --- a/e2e/checks/keyboard.yaml +++ b/e2e/checks/keyboard.yaml @@ -6,3 +6,8 @@ checks: platforms: [macos] blocking: true spec: "docs/e2e-flows.md Visual goldens; #227" + KEY-CONTROLLER-01: + title: react-native-keyboard-controller's provider, keyboard-aware scroll view and toolbar render on macOS with the keyboard reported hidden + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/react-native-keyboard-controller.md" diff --git a/e2e/checks/media.yaml b/e2e/checks/media.yaml new file mode 100644 index 000000000..34891951b --- /dev/null +++ b/e2e/checks/media.yaml @@ -0,0 +1,18 @@ +area: media +prefix: MEDIA +checks: + MEDIA-ZXING-01: + title: react-native-nitro-zxing scans a QR code in a still Nitro Image on macOS and returns its format and value + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/react-native-nitro-zxing.md" + MEDIA-IMAGEPICKER-01: + title: expo-image-picker links on macOS, grants file picking without a library permission and reports the camera picker unavailable + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/expo-image-picker.md" + MEDIA-IMAGEMANIP-01: + title: expo-image-manipulator rotates, flips, resizes and crops images on macOS with correct pixels + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/expo-image-manipulator.md" diff --git a/e2e/checks/native-controls.yaml b/e2e/checks/native-controls.yaml index f35c9a811..fc745a6f2 100644 --- a/e2e/checks/native-controls.yaml +++ b/e2e/checks/native-controls.yaml @@ -19,3 +19,8 @@ checks: - ./ui/symbol#SFSymbol - ./ui/search#TextInputSearch - ./ui/glass#GlassView + CTRL-DATETIMEPICKER-01: + title: "@react-native-community/datetimepicker renders date, time and inline calendar pickers in NSDatePicker on macOS" + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/datetimepicker.md" diff --git a/e2e/checks/networking-webview.yaml b/e2e/checks/networking-webview.yaml new file mode 100644 index 000000000..77aadfc62 --- /dev/null +++ b/e2e/checks/networking-webview.yaml @@ -0,0 +1,18 @@ +area: networking-webview +prefix: WEB +checks: + WEB-NITROFETCH-01: + title: react-native-nitro-fetch performs a native HTTPS request on macOS with status, headers and JSON, reports a 404, serves a prefetched response, and keeps its URL cache in the app's Caches folder + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/react-native-nitro-fetch.md" + WEB-EXPOBROWSER-01: + title: expo-web-browser opens a page in its macOS web window and dismissBrowser closes it, resolving openBrowserAsync with dismiss + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/expo-web-browser.md" + WEB-NETINFO-01: + title: "@react-native-community/netinfo reports a connected wifi or ethernet state on macOS" + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/react-native-community-netinfo.md" diff --git a/e2e/checks/processes-ipc.yaml b/e2e/checks/processes-ipc.yaml new file mode 100644 index 000000000..1deac0f08 --- /dev/null +++ b/e2e/checks/processes-ipc.yaml @@ -0,0 +1,8 @@ +area: processes-ipc +prefix: PROC +checks: + PROC-WORKLETS-01: + title: react-native-worklets-core runs a worklet in a separate Hermes context on macOS, shares a value with it and calls back into the main runtime + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/react-native-worklets-core.md" diff --git a/e2e/checks/security.yaml b/e2e/checks/security.yaml new file mode 100644 index 000000000..e927838b2 --- /dev/null +++ b/e2e/checks/security.yaml @@ -0,0 +1,18 @@ +area: security +prefix: SEC +checks: + SEC-QUICKCRYPTO-01: + title: react-native-quick-crypto computes SHA-256, HMAC, PBKDF2 and WebCrypto digests matching published vectors, round-trips AES-256-GCM and produces random bytes and UUIDs on macOS + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/react-native-quick-crypto.md" + SEC-ATT-01: + title: react-native-tracking-transparency imports on macOS and reports unavailable, since App Tracking Transparency is a no-op on the Mac + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/react-native-tracking-transparency.md" + SEC-EXPOCRYPTO-01: + title: expo-crypto computes SHA digests matching published vectors and produces random bytes and v4 UUIDs on macOS + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/expo-crypto.md" diff --git a/e2e/checks/storage-data.yaml b/e2e/checks/storage-data.yaml new file mode 100644 index 000000000..f96b33a3e --- /dev/null +++ b/e2e/checks/storage-data.yaml @@ -0,0 +1,18 @@ +area: storage-data +prefix: DATA +checks: + DATA-MMKV-01: + title: react-native-mmkv round-trips strings, numbers, booleans and buffers, lists and removes keys, and notifies value-change listeners on macOS + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/react-native-mmkv.md" + DATA-MMKV-02: + title: MMKV data persists across launches in the app's namespaced Application Support folder, so clearState removes it + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/react-native-mmkv.md" + DATA-NITROSQLITE-01: + title: react-native-nitro-sqlite runs parameterized, batched and async queries on macOS alongside Spark's OP-SQLite + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/react-native-nitro-sqlite.md" diff --git a/e2e/checks/system-integration.yaml b/e2e/checks/system-integration.yaml index ff5100354..79e1cc364 100644 --- a/e2e/checks/system-integration.yaml +++ b/e2e/checks/system-integration.yaml @@ -55,3 +55,28 @@ checks: spec: "#123 SYS.api-dock" covers: - ./system#getSystemAvailability().available + SYS-KEEPAWAKE-01: + title: expo-keep-awake holds a display-sleep power assertion on macOS while activated and releases it on deactivate + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/expo-keep-awake.md" + SYS-EXPOAPP-01: + title: expo-application reports the bundle identifier, version and an installation time from the app bundle on macOS, and Mac answers (no vendor ID, unknown release type) for iOS-only queries + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/expo-application.md" + SYS-EXPODEVICE-01: + title: expo-device reports the Mac's model identifier, macOS version and build, memory, CPU architecture, desktop device type, name and uptime + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/expo-device.md" + SYS-EXPOHAPTICS-01: + title: expo-haptics notification, impact and selection calls resolve on macOS through NSHapticFeedbackManager instead of rejecting as unavailable + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/expo-haptics.md" + SYS-EXPOLOCATION-01: + title: expo-location reports permission, Location Services and provider status from CoreLocation on macOS, and no compass heading + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/expo-location.md" diff --git a/e2e/checks/windows.yaml b/e2e/checks/windows.yaml index 592ca7282..8cb336eab 100644 --- a/e2e/checks/windows.yaml +++ b/e2e/checks/windows.yaml @@ -6,3 +6,8 @@ checks: platforms: [macos] blocking: true spec: "docs/e2e-flows.md Visual goldens; #227" + WIN-SAFEAREA-01: + title: react-native-safe-area-context's provider reports the window frame and finite insets on macOS, and SafeAreaView lays out its content + platforms: [macos] + blocking: true + spec: "vault tasks/spark/desktop-libs/react-native-safe-area-context.md" diff --git a/e2e/flows/app-lifecycle/splash-screen.yaml b/e2e/flows/app-lifecycle/splash-screen.yaml new file mode 100644 index 000000000..73c1fe4b3 --- /dev/null +++ b/e2e/flows/app-lifecycle/splash-screen.yaml @@ -0,0 +1,11 @@ +appId: so.legend.spark.examples.kitchensink +name: expo-splash-screen on macOS +intent: > + The Splash screen screen calls setOptions, preventAutoHideAsync, hide and hideAsync; with no native splash on + macOS they resolve without throwing, so the screen shows passed. +checks: [LIFE-SPLASH-01] +tags: [app-lifecycle] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://app-lifecycle/splash-screen" } +- extendedWaitUntil: { visible: { id: app-lifecycle-splash-screen-passed }, timeout: 10s } diff --git a/e2e/flows/appearance-theming/system-ui.yaml b/e2e/flows/appearance-theming/system-ui.yaml new file mode 100644 index 000000000..914d6c6cb --- /dev/null +++ b/e2e/flows/appearance-theming/system-ui.yaml @@ -0,0 +1,11 @@ +appId: so.legend.spark.examples.kitchensink +name: expo-system-ui on macOS +intent: > + The System UI screen sets the main window background to #336699, reads the same color back, then resets it + to the default, so the screen shows passed. +checks: [APPR-SYSTEMUI-01] +tags: [appearance-theming] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://appearance-theming/system-ui" } +- extendedWaitUntil: { visible: { id: appearance-theming-system-ui-passed }, timeout: 10s } diff --git a/e2e/flows/appearance-theming/unistyles.yaml b/e2e/flows/appearance-theming/unistyles.yaml new file mode 100644 index 000000000..7b3b21d22 --- /dev/null +++ b/e2e/flows/appearance-theming/unistyles.yaml @@ -0,0 +1,16 @@ +appId: so.legend.spark.examples.kitchensink +name: react-native-unistyles adaptive themes on macOS +intent: > + The Unistyles screen styles a card from Unistyles themes. Its native runtime reports the window size and + pixel ratio. Overriding the app's appearance to dark switches the adaptive theme to dark, and light + switches it back, with no reload. +checks: [APPR-UNISTYLES-01] +tags: [appearance-theming] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://appearance-theming/unistyles" } +- extendedWaitUntil: { visible: { id: appearance-theming-unistyles-runtime-ready }, timeout: 10s } +- setAppAppearance: dark +- extendedWaitUntil: { visible: { id: appearance-theming-unistyles-theme-dark }, timeout: 5s } +- setAppAppearance: light +- extendedWaitUntil: { visible: { id: appearance-theming-unistyles-theme-light }, timeout: 5s } diff --git a/e2e/flows/component-library/bottom-sheet.yaml b/e2e/flows/component-library/bottom-sheet.yaml new file mode 100644 index 000000000..12d11379b --- /dev/null +++ b/e2e/flows/component-library/bottom-sheet.yaml @@ -0,0 +1,10 @@ +appId: so.legend.spark.examples.kitchensink +name: "@gorhom/bottom-sheet on macOS" +intent: > + The Bottom sheet screen snaps its sheet from the first snap point to the second; onChange reports index 1. +checks: [COMP-SHEET-01] +tags: [component-library] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://component-library/bottom-sheet" } +- extendedWaitUntil: { visible: { id: component-library-bottom-sheet-index-1 }, timeout: 10s } diff --git a/e2e/flows/component-library/drawer-layout.yaml b/e2e/flows/component-library/drawer-layout.yaml new file mode 100644 index 000000000..c2bff91c4 --- /dev/null +++ b/e2e/flows/component-library/drawer-layout.yaml @@ -0,0 +1,12 @@ +appId: so.legend.spark.examples.kitchensink +name: react-native-drawer-layout on macOS +intent: > + The Drawer layout screen opens its drawer after 0.3 s and closes it after 1.5 s; onOpen and onClose both + report. +checks: [COMP-DRAWER-01] +tags: [component-library] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://component-library/drawer-layout" } +- extendedWaitUntil: { visible: { id: component-library-drawer-layout-opened }, timeout: 10s } +- extendedWaitUntil: { visible: { id: component-library-drawer-layout-closed }, timeout: 10s } diff --git a/e2e/flows/component-library/expo-router.yaml b/e2e/flows/component-library/expo-router.yaml new file mode 100644 index 000000000..8cea64526 --- /dev/null +++ b/e2e/flows/component-library/expo-router.yaml @@ -0,0 +1,12 @@ +appId: so.legend.spark.examples.kitchensink +name: expo-router on macOS +intent: > + The Expo Router screen mounts ExpoRoot on routes from a folder: the home route renders, then pushes + /details/42?from=home, and the details route shows its dynamic id and search param. +checks: [COMP-EXPOROUTER-01] +tags: [component-library] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://component-library/expo-router" } +- extendedWaitUntil: { visible: { id: component-library-expo-router-details-42-home }, timeout: 15s } +- takeScreenshot: expo-router diff --git a/e2e/flows/component-library/expo-status-bar.yaml b/e2e/flows/component-library/expo-status-bar.yaml new file mode 100644 index 000000000..1a48730f7 --- /dev/null +++ b/e2e/flows/component-library/expo-status-bar.yaml @@ -0,0 +1,10 @@ +appId: so.legend.spark.examples.kitchensink +name: expo-status-bar on macOS +intent: > + The Expo Status Bar screen renders the StatusBar component and calls its setters, which return without a status bar to change. +checks: [COMP-STATUSBAR-01] +tags: [component-library] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://component-library/expo-status-bar" } +- extendedWaitUntil: { visible: { id: component-library-expo-status-bar-ran }, timeout: 10s } diff --git a/e2e/flows/component-library/navigation.yaml b/e2e/flows/component-library/navigation.yaml new file mode 100644 index 000000000..2bf2021cf --- /dev/null +++ b/e2e/flows/component-library/navigation.yaml @@ -0,0 +1,15 @@ +appId: so.legend.spark.examples.kitchensink +name: React Navigation on macOS +intent: > + The React Navigation screen runs a native stack that navigates from Home to Details and back by + itself, showing each screen's header title, and bottom tabs that switch to the second tab. +checks: [COMP-NAVIGATION-01] +tags: [component-library] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://component-library/navigation" } +- extendedWaitUntil: { visible: { id: component-library-navigation-stack-done }, timeout: 10s } +- assertVisible: { id: component-library-navigation-stack-header-Home } +- assertVisible: { id: component-library-navigation-stack-Home } +- assertVisible: { id: component-library-navigation-tab-Second } +- takeScreenshot: navigation diff --git a/e2e/flows/component-library/pressable-opacity.yaml b/e2e/flows/component-library/pressable-opacity.yaml new file mode 100644 index 000000000..8292017ce --- /dev/null +++ b/e2e/flows/component-library/pressable-opacity.yaml @@ -0,0 +1,11 @@ +appId: so.legend.spark.examples.kitchensink +name: react-native-pressable-opacity on macOS +intent: > + The Pressable opacity screen renders both button kinds; pressing them needs a pointer. +checks: [COMP-PRESSABLE-01] +tags: [component-library] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://component-library/pressable-opacity" } +- extendedWaitUntil: { visible: { id: component-library-pressable-opacity-js }, timeout: 10s } +- extendedWaitUntil: { visible: { id: component-library-pressable-opacity-native }, timeout: 10s } diff --git a/e2e/flows/component-library/reanimated.yaml b/e2e/flows/component-library/reanimated.yaml new file mode 100644 index 000000000..b0e36c4f8 --- /dev/null +++ b/e2e/flows/component-library/reanimated.yaml @@ -0,0 +1,13 @@ +appId: so.legend.spark.examples.kitchensink +name: react-native-reanimated on macOS +intent: > + The Reanimated screen animates a box with withTiming and counts frame callbacks on the UI thread. The + timing callback reports it finished at 1, and frame callbacks report at least ten frames. +checks: [COMP-REANIMATED-01] +tags: [component-library] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://component-library/reanimated" } +- extendedWaitUntil: { visible: { id: component-library-reanimated-finished }, timeout: 10s } +- extendedWaitUntil: { visible: { id: component-library-reanimated-frames }, timeout: 10s } +- takeScreenshot: reanimated diff --git a/e2e/flows/component-library/screens.yaml b/e2e/flows/component-library/screens.yaml new file mode 100644 index 000000000..1b814389a --- /dev/null +++ b/e2e/flows/component-library/screens.yaml @@ -0,0 +1,14 @@ +appId: so.legend.spark.examples.kitchensink +name: react-native-screens on macOS +intent: > + The Screens screen renders a ScreenContainer with an active and an inactive Screen, and a ScreenStack + with one Screen. The active and stack screens lay out; the inactive one does not. +checks: [COMP-SCREENS-01] +tags: [component-library] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://component-library/screens" } +- extendedWaitUntil: { visible: { id: component-library-screens-first-visible }, timeout: 10s } +- assertVisible: { id: component-library-screens-stack-visible } +- assertNotVisible: { id: component-library-screens-second-visible } +- takeScreenshot: screens diff --git a/e2e/flows/devtools/app-logs.yaml b/e2e/flows/devtools/app-logs.yaml new file mode 100644 index 000000000..124a3b9bc --- /dev/null +++ b/e2e/flows/devtools/app-logs.yaml @@ -0,0 +1,11 @@ +appId: so.legend.spark.examples.kitchensink +name: react-native-app-logs on macOS +intent: > + The App logs screen registers a filtered handler, writes a marker to the console every half second, and + shows the first matching entry that react-native-app-logs reads back from the app's unified log. +checks: [DEVT-APPLOGS-01] +tags: [devtools] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://devtools/app-logs" } +- extendedWaitUntil: { visible: { id: devtools-app-logs-received }, timeout: 20s } diff --git a/e2e/flows/devtools/release-profiler.yaml b/e2e/flows/devtools/release-profiler.yaml new file mode 100644 index 000000000..021322761 --- /dev/null +++ b/e2e/flows/devtools/release-profiler.yaml @@ -0,0 +1,12 @@ +appId: so.legend.spark.examples.kitchensink +name: react-native-release-profiler on macOS +intent: > + The Release profiler screen samples 300 ms of busy JavaScript, stops the profiler and reads the + .cpuprofile trace it wrote; the trace has events and sits in the app's Caches folder, so the screen shows + passed. +checks: [DEVT-PROFILER-01] +tags: [devtools] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://devtools/release-profiler" } +- extendedWaitUntil: { visible: { id: devtools-release-profiler-passed }, timeout: 15s } diff --git a/e2e/flows/devtools/sentry.yaml b/e2e/flows/devtools/sentry.yaml new file mode 100644 index 000000000..e102e3c07 --- /dev/null +++ b/e2e/flows/devtools/sentry.yaml @@ -0,0 +1,12 @@ +appId: so.legend.spark.examples.kitchensink +name: "@sentry/react-native on macOS" +intent: > + The Sentry screen initializes the SDK with a placeholder DSN, captures an error and keeps the event in + beforeSend without sending it; the event has the message, a stack trace and the native module's macOS os + context, so the screen shows passed. +checks: [DEVT-SENTRY-01] +tags: [devtools] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://devtools/sentry" } +- extendedWaitUntil: { visible: { id: devtools-sentry-passed }, timeout: 15s } diff --git a/e2e/flows/displays-graphics/blur.yaml b/e2e/flows/displays-graphics/blur.yaml new file mode 100644 index 000000000..f74889ca9 --- /dev/null +++ b/e2e/flows/displays-graphics/blur.yaml @@ -0,0 +1,20 @@ +appId: so.legend.spark.examples.kitchensink +name: "@react-native-community/blur on macOS" +intent: > + The Blur screen finds BlurView and VibrancyView in the native component registry and shows four blur + materials (light, dark, adaptive material, thin dark) and a vibrancy view over colored stripes. Each + blur view reports a native frame and the vibrancy view shows its child inside the material. The capture + records how the materials look; it cannot show the live backdrop blur. +checks: [DISP-BLUR-01] +tags: [displays-graphics] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://displays-graphics/blur" } +- extendedWaitUntil: { visible: { id: displays-graphics-blur-BlurView-registered }, timeout: 10s } +- assertVisible: { id: displays-graphics-blur-VibrancyView-registered } +- assertVisible: { id: displays-graphics-blur-light-laid-out } +- assertVisible: { id: displays-graphics-blur-dark-laid-out } +- assertVisible: { id: displays-graphics-blur-material-laid-out } +- assertVisible: { id: displays-graphics-blur-thinMaterialDark-laid-out } +- assertVisible: { id: displays-graphics-blur-vibrancy-child } +- takeScreenshot: blur-materials diff --git a/e2e/flows/displays-graphics/blurhash.yaml b/e2e/flows/displays-graphics/blurhash.yaml new file mode 100644 index 000000000..c5afa9313 --- /dev/null +++ b/e2e/flows/displays-graphics/blurhash.yaml @@ -0,0 +1,13 @@ +appId: so.legend.spark.examples.kitchensink +name: react-native-blurhash on macOS +intent: > + The Blurhash screen decodes a Blurhash in the native view, which reports onLoadEnd, and encodes a 4×2 red + PNG from the app's data folder; the encoded Blurhash is valid and its average color is red. +checks: [DISP-BLURHASH-01] +tags: [displays-graphics] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://displays-graphics/blurhash" } +- extendedWaitUntil: { visible: { id: displays-graphics-blurhash-decoded }, timeout: 10s } +- extendedWaitUntil: { visible: { id: displays-graphics-blurhash-encoded }, timeout: 10s } +- takeScreenshot: blurhash diff --git a/e2e/flows/displays-graphics/expo-blur.yaml b/e2e/flows/displays-graphics/expo-blur.yaml new file mode 100644 index 000000000..6ef70c39f --- /dev/null +++ b/e2e/flows/displays-graphics/expo-blur.yaml @@ -0,0 +1,15 @@ +appId: so.legend.spark.examples.kitchensink +name: expo-blur on macOS +intent: > + The Expo Blur screen finds the native module linked and lays out light, dark and system-material blur + views over colored stripes; the capture records how the materials look. +checks: [DISP-EXPOBLUR-01] +tags: [displays-graphics] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://displays-graphics/expo-blur" } +- extendedWaitUntil: { visible: { id: displays-graphics-expo-blur-native }, timeout: 10s } +- assertVisible: { id: displays-graphics-expo-blur-light-laid-out } +- assertVisible: { id: displays-graphics-expo-blur-dark-laid-out } +- assertVisible: { id: displays-graphics-expo-blur-systemMaterial-laid-out } +- takeScreenshot: expo-blur diff --git a/e2e/flows/displays-graphics/expo-glass-effect.yaml b/e2e/flows/displays-graphics/expo-glass-effect.yaml new file mode 100644 index 000000000..89cb4c9f8 --- /dev/null +++ b/e2e/flows/displays-graphics/expo-glass-effect.yaml @@ -0,0 +1,19 @@ +appId: so.legend.spark.examples.kitchensink +name: expo-glass-effect on macOS +intent: > + The Expo Glass screen finds the native module linked and the glass API available (macOS 26), and lays out + the children of regular, clear and tinted glass views and of two views in a glass container. The capture + records the glass. +checks: [DISP-EXPOGLASS-01] +tags: [displays-graphics] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://displays-graphics/expo-glass-effect" } +- extendedWaitUntil: { visible: { id: displays-graphics-expo-glass-native }, timeout: 10s } +- assertVisible: { id: displays-graphics-expo-glass-available-yes } +- assertVisible: { id: displays-graphics-expo-glass-regular-child } +- assertVisible: { id: displays-graphics-expo-glass-clear-child } +- assertVisible: { id: displays-graphics-expo-glass-tinted-child } +- assertVisible: { id: displays-graphics-expo-glass-merged-a-child } +- assertVisible: { id: displays-graphics-expo-glass-merged-b-child } +- takeScreenshot: expo-glass-effect diff --git a/e2e/flows/displays-graphics/expo-image.yaml b/e2e/flows/displays-graphics/expo-image.yaml new file mode 100644 index 000000000..6ef1e5e36 --- /dev/null +++ b/e2e/flows/displays-graphics/expo-image.yaml @@ -0,0 +1,17 @@ +appId: so.legend.spark.examples.kitchensink +name: expo-image on macOS +intent: > + The Expo Image screen writes a 4×2 PNG to the app's data folder and shows it with expo-image. onLoad reports + 4×2, reloadAsync through the ref loads it again, and Image.prefetch resolves true. An unsupported blurhash + reports start/error/end exactly once. A bundled 160×80 asset fills a styled box and contentFit none crops + its natural size at the center, with both samples captured for visual inspection. +checks: [DISP-EXPOIMAGE-01] +tags: [displays-graphics] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://displays-graphics/expo-image" } +- extendedWaitUntil: { visible: { id: displays-graphics-expo-image-passed }, timeout: 10s } +- extendedWaitUntil: { visible: { id: displays-graphics-expo-image-asset-loaded }, timeout: 10s } +- assertVisible: { id: displays-graphics-expo-image-unsupported-passed } +- assertNotVisible: { id: displays-graphics-expo-image-unsupported-failed } +- takeScreenshot: expo-image diff --git a/e2e/flows/displays-graphics/fast-image.yaml b/e2e/flows/displays-graphics/fast-image.yaml new file mode 100644 index 000000000..f93e57138 --- /dev/null +++ b/e2e/flows/displays-graphics/fast-image.yaml @@ -0,0 +1,18 @@ +appId: so.legend.spark.examples.kitchensink +name: react-native-fast-image on macOS +intent: > + The FastImage screen writes a 4×2 PNG to the app's data folder and shows it with FastImage. onLoad reports + 4×2, a reload through the ref loads it again, and FastImage.preload resolves ok with the size, so the + screen shows passed. configureCache resolves a specific not-ok result. A bundled image loads in a + FastImageBackground whose testID and layout callback belong to the 120×60 container, and a URI object + placeholder remains visible when the main source fails; the screenshot captures their sizing. +checks: [DISP-FASTIMAGE-01] +tags: [displays-graphics] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://displays-graphics/fast-image" } +- extendedWaitUntil: { visible: { id: displays-graphics-fast-image-passed }, timeout: 10s } +- assertVisible: { id: displays-graphics-fast-image-background } +- extendedWaitUntil: { visible: { id: displays-graphics-fast-image-background-passed }, timeout: 10s } +- extendedWaitUntil: { visible: { id: displays-graphics-fast-image-placeholder-ready }, timeout: 10s } +- takeScreenshot: fast-image diff --git a/e2e/flows/displays-graphics/graph.yaml b/e2e/flows/displays-graphics/graph.yaml new file mode 100644 index 000000000..22230b1c2 --- /dev/null +++ b/e2e/flows/displays-graphics/graph.yaml @@ -0,0 +1,12 @@ +appId: so.legend.spark.examples.kitchensink +name: react-native-graph on macOS +intent: > + The Graph screen renders a static and an animated line graph; both lay out instead of the screen's error + boundary catching Skia 1 path calls that Skia 2 removed. Skia content isn't in driver captures. +checks: [DISP-GRAPH-01] +tags: [displays-graphics] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://displays-graphics/graph" } +- extendedWaitUntil: { visible: { id: displays-graphics-graph-static-laid-out }, timeout: 10s } +- assertVisible: { id: displays-graphics-graph-animated-laid-out } diff --git a/e2e/flows/displays-graphics/linear-gradient.yaml b/e2e/flows/displays-graphics/linear-gradient.yaml new file mode 100644 index 000000000..4035a22cc --- /dev/null +++ b/e2e/flows/displays-graphics/linear-gradient.yaml @@ -0,0 +1,14 @@ +appId: so.legend.spark.examples.kitchensink +name: expo-linear-gradient on macOS +intent: > + The Linear gradient screen finds the native module linked and lays out a three-color diagonal gradient that matches the approved golden (an undrawn gradient leaves the + area empty). +checks: [DISP-GRADIENT-01] +tags: [displays-graphics] +platforms: [macos] +matrix: { appearance: [dark], locale: [en] } +--- +- launchApp: { clearState: true, openUrl: "spark-ks://displays-graphics/linear-gradient" } +- extendedWaitUntil: { visible: { id: displays-graphics-linear-gradient-native }, timeout: 15s } +- assertVisible: { id: displays-graphics-linear-gradient-laid-out } +- assertScreenshot: { golden: displays-graphics-linear-gradient, mask: [{ id: infra-shell-sidebar }] } diff --git a/e2e/flows/displays-graphics/masked-view.yaml b/e2e/flows/displays-graphics/masked-view.yaml new file mode 100644 index 000000000..ce58421e6 --- /dev/null +++ b/e2e/flows/displays-graphics/masked-view.yaml @@ -0,0 +1,14 @@ +appId: so.legend.spark.examples.kitchensink +name: "@react-native-masked-view/masked-view on macOS" +intent: > + The Masked view screen finds the native view manager registered and masks colored stripes with the text + MASK and a half-transparent circle, matching the approved golden (an unmasked view shows the full stripes). +checks: [DISP-MASKED-01] +tags: [displays-graphics] +platforms: [macos] +matrix: { appearance: [dark], locale: [en] } +--- +- launchApp: { clearState: true, openUrl: "spark-ks://displays-graphics/masked-view" } +- extendedWaitUntil: { visible: { id: displays-graphics-masked-view-registered }, timeout: 10s } +- assertVisible: { id: displays-graphics-masked-view-laid-out } +- assertScreenshot: { golden: displays-graphics-masked-view, mask: [{ id: infra-shell-sidebar }] } diff --git a/e2e/flows/displays-graphics/screen-corner-radius.yaml b/e2e/flows/displays-graphics/screen-corner-radius.yaml new file mode 100644 index 000000000..521817fe2 --- /dev/null +++ b/e2e/flows/displays-graphics/screen-corner-radius.yaml @@ -0,0 +1,11 @@ +appId: so.legend.spark.examples.kitchensink +name: react-native-screen-corner-radius on macOS +intent: > + The Screen corner radius screen imports the library, whose native module is iOS only, and shows a 0 radius + with square corners instead of failing at import. +checks: [DISP-CORNER-01] +tags: [displays-graphics] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://displays-graphics/screen-corner-radius" } +- extendedWaitUntil: { visible: { id: displays-graphics-screen-corner-radius-0-false }, timeout: 10s } diff --git a/e2e/flows/displays-graphics/skia.yaml b/e2e/flows/displays-graphics/skia.yaml new file mode 100644 index 000000000..394537e21 --- /dev/null +++ b/e2e/flows/displays-graphics/skia.yaml @@ -0,0 +1,13 @@ +appId: so.legend.spark.examples.kitchensink +name: "@shopify/react-native-skia on macOS" +intent: > + The Skia screen draws a green circle on a yellow Canvas and reads the native view back as an image; its + center pixel is green. An offscreen raster surface with a blue circle on red reads back the same way. The + screen then shows passed. +checks: [DISP-SKIA-01] +tags: [displays-graphics] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://displays-graphics/skia" } +- extendedWaitUntil: { visible: { id: displays-graphics-skia-passed }, timeout: 15s } +- takeScreenshot: skia diff --git a/e2e/flows/displays-graphics/svg.yaml b/e2e/flows/displays-graphics/svg.yaml new file mode 100644 index 000000000..d40c9946f --- /dev/null +++ b/e2e/flows/displays-graphics/svg.yaml @@ -0,0 +1,14 @@ +appId: so.legend.spark.examples.kitchensink +name: react-native-svg on macOS +intent: > + The SVG screen finds the native SVG components registered and draws a gradient rectangle, a circle, a + stroked path and text, matching the approved golden (an undrawn SVG leaves the area empty). +checks: [DISP-SVG-01] +tags: [displays-graphics] +platforms: [macos] +matrix: { appearance: [dark], locale: [en] } +--- +- launchApp: { clearState: true, openUrl: "spark-ks://displays-graphics/svg" } +- extendedWaitUntil: { visible: { id: displays-graphics-svg-native }, timeout: 10s } +- assertVisible: { id: displays-graphics-svg-laid-out } +- assertScreenshot: { golden: displays-graphics-svg, mask: [{ id: infra-shell-sidebar }] } diff --git a/e2e/flows/displays-graphics/symbols.yaml b/e2e/flows/displays-graphics/symbols.yaml new file mode 100644 index 000000000..4621becfb --- /dev/null +++ b/e2e/flows/displays-graphics/symbols.yaml @@ -0,0 +1,15 @@ +appId: so.legend.spark.examples.kitchensink +name: expo-symbols on macOS +intent: > + The SF Symbols screen finds the native module linked and lays out six symbols (monochrome, hierarchical, + palette, multicolor, bold and a pulsing one), each drawn by AppKit rather than the fallback text, matching the approved golden (the pulsing symbol is + masked). +checks: [DISP-SYMBOLS-01] +tags: [displays-graphics] +platforms: [macos] +matrix: { appearance: [dark], locale: [en] } +--- +- launchApp: { clearState: true, openUrl: "spark-ks://displays-graphics/symbols" } +- extendedWaitUntil: { visible: { id: displays-graphics-symbols-native }, timeout: 10s } +- assertVisible: { id: displays-graphics-symbols-laid-out } +- assertScreenshot: { golden: displays-graphics-symbols, mask: [{ id: infra-shell-sidebar }, { id: displays-graphics-symbols-pulse }] } diff --git a/e2e/flows/displays-graphics/vector-icons.yaml b/e2e/flows/displays-graphics/vector-icons.yaml new file mode 100644 index 000000000..f9fd46ccd --- /dev/null +++ b/e2e/flows/displays-graphics/vector-icons.yaml @@ -0,0 +1,13 @@ +appId: so.legend.spark.examples.kitchensink +name: "@expo/vector-icons on macOS" +intent: > + The Vector icons screen loads the Ionicons and Material Community Icons fonts through expo-font, both + register, and three icons draw as glyphs, matching the approved golden. +checks: [DISP-ICONS-01] +tags: [displays-graphics] +platforms: [macos] +matrix: { appearance: [dark], locale: [en] } +--- +- launchApp: { clearState: true, openUrl: "spark-ks://displays-graphics/vector-icons" } +- extendedWaitUntil: { visible: { id: displays-graphics-vector-icons-loaded }, timeout: 10s } +- assertScreenshot: { golden: displays-graphics-vector-icons, mask: [{ id: infra-shell-sidebar }] } diff --git a/e2e/flows/internationalization/expo-localization.yaml b/e2e/flows/internationalization/expo-localization.yaml new file mode 100644 index 000000000..e16b63086 --- /dev/null +++ b/e2e/flows/internationalization/expo-localization.yaml @@ -0,0 +1,11 @@ +appId: so.legend.spark.examples.kitchensink +name: expo-localization on macOS +intent: > + The Expo Localization screen reads the user's preferred locale and calendar from expo-localization's + native module and shows the language tag and time zone. +checks: [I18N-EXPOLOCALIZATION-01] +tags: [internationalization] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://internationalization/expo-localization" } +- extendedWaitUntil: { visible: { id: internationalization-expo-localization-ready }, timeout: 10s } diff --git a/e2e/flows/keyboard/keyboard-controller.yaml b/e2e/flows/keyboard/keyboard-controller.yaml new file mode 100644 index 000000000..f34b4cbdf --- /dev/null +++ b/e2e/flows/keyboard/keyboard-controller.yaml @@ -0,0 +1,12 @@ +appId: so.legend.spark.examples.kitchensink +name: react-native-keyboard-controller on macOS +intent: > + The Keyboard controller screen renders KeyboardProvider, KeyboardAwareScrollView, a text input and + KeyboardToolbar, and both useKeyboardState and KeyboardController report the keyboard hidden, so the screen + shows passed. +checks: [KEY-CONTROLLER-01] +tags: [keyboard] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://keyboard/keyboard-controller" } +- extendedWaitUntil: { visible: { id: keyboard-keyboard-controller-passed }, timeout: 10s } diff --git a/e2e/flows/media/image-manipulator.yaml b/e2e/flows/media/image-manipulator.yaml new file mode 100644 index 000000000..3ba94985f --- /dev/null +++ b/e2e/flows/media/image-manipulator.yaml @@ -0,0 +1,13 @@ +appId: so.legend.spark.examples.kitchensink +name: expo-image-manipulator on macOS +intent: > + The Image manipulator screen rotates, flips, resizes and crops a 2×1 red and blue PNG, saves each result as + PNG and decodes it with Skia; sizes and pixel colors match (rotate 90° clockwise puts red on top), so the + screen shows passed. +checks: [MEDIA-IMAGEMANIP-01] +tags: [media] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://media/image-manipulator" } +- extendedWaitUntil: { visible: { id: media-image-manipulator-passed }, timeout: 15s } +- takeScreenshot: image-manipulator diff --git a/e2e/flows/media/image-picker.yaml b/e2e/flows/media/image-picker.yaml new file mode 100644 index 000000000..a4dfeafbb --- /dev/null +++ b/e2e/flows/media/image-picker.yaml @@ -0,0 +1,12 @@ +appId: so.legend.spark.examples.kitchensink +name: expo-image-picker on macOS +intent: > + The Image picker screen finds the native module, reads the media library permission as granted (the open + panel needs none) and a camera permission status, and gets ERR_UNAVAILABLE from launchCameraAsync, so the + screen shows passed. The open panel itself needs a person to pick files. +checks: [MEDIA-IMAGEPICKER-01] +tags: [media] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://media/image-picker" } +- extendedWaitUntil: { visible: { id: media-image-picker-passed }, timeout: 10s } diff --git a/e2e/flows/media/nitro-zxing.yaml b/e2e/flows/media/nitro-zxing.yaml new file mode 100644 index 000000000..45f2c12c0 --- /dev/null +++ b/e2e/flows/media/nitro-zxing.yaml @@ -0,0 +1,11 @@ +appId: so.legend.spark.examples.kitchensink +name: react-native-nitro-zxing on macOS +intent: > + The Barcode scanning screen writes a QR code PNG to the app's data folder, loads it with Nitro Image and + scans it with ZXing, which returns the value spark-kitchen-sink. +checks: [MEDIA-ZXING-01] +tags: [media] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://media/nitro-zxing" } +- extendedWaitUntil: { visible: { id: media-nitro-zxing-spark-kitchen-sink }, timeout: 10s } diff --git a/e2e/flows/native-controls/datetimepicker.yaml b/e2e/flows/native-controls/datetimepicker.yaml new file mode 100644 index 000000000..6ed4b46d0 --- /dev/null +++ b/e2e/flows/native-controls/datetimepicker.yaml @@ -0,0 +1,15 @@ +appId: so.legend.spark.examples.kitchensink +name: "@react-native-community/datetimepicker on macOS" +intent: > + The Date & time picker screen lays out a date field, a time field and an inline calendar with time for + 15 January 2026 09:30 UTC, matching the approved golden (an unregistered component renders nothing). +checks: [CTRL-DATETIMEPICKER-01] +tags: [native-controls] +platforms: [macos] +matrix: { appearance: [dark], locale: [en] } +--- +- launchApp: { clearState: true, openUrl: "spark-ks://native-controls/datetimepicker" } +- extendedWaitUntil: { visible: { id: native-controls-datetimepicker-date-laid-out }, timeout: 10s } +- assertVisible: { id: native-controls-datetimepicker-time-laid-out } +- assertVisible: { id: native-controls-datetimepicker-inline-laid-out } +- assertScreenshot: { golden: native-controls-datetimepicker, mask: [{ id: infra-shell-sidebar }] } diff --git a/e2e/flows/networking-webview/expo-web-browser.yaml b/e2e/flows/networking-webview/expo-web-browser.yaml new file mode 100644 index 000000000..3d24e0cab --- /dev/null +++ b/e2e/flows/networking-webview/expo-web-browser.yaml @@ -0,0 +1,11 @@ +appId: so.legend.spark.examples.kitchensink +name: expo-web-browser on macOS +intent: > + The Expo Web Browser screen opens example.com in expo-web-browser's macOS web window (needs network + access), closes it with dismissBrowser, and shows the dismiss result. +checks: [WEB-EXPOBROWSER-01] +tags: [networking-webview] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://networking-webview/expo-web-browser" } +- extendedWaitUntil: { visible: { id: networking-webview-expo-web-browser-dismiss }, timeout: 15s } diff --git a/e2e/flows/networking-webview/netinfo.yaml b/e2e/flows/networking-webview/netinfo.yaml new file mode 100644 index 000000000..67c965af7 --- /dev/null +++ b/e2e/flows/networking-webview/netinfo.yaml @@ -0,0 +1,11 @@ +appId: so.legend.spark.examples.kitchensink +name: "@react-native-community/netinfo on macOS" +intent: > + The NetInfo screen fetches the network state from the native monitor; on a connected Mac it shows a + connected wifi or ethernet state. +checks: [WEB-NETINFO-01] +tags: [networking-webview] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://networking-webview/netinfo" } +- extendedWaitUntil: { visible: { id: networking-webview-netinfo-connected }, timeout: 10s } diff --git a/e2e/flows/networking-webview/nitro-fetch.yaml b/e2e/flows/networking-webview/nitro-fetch.yaml new file mode 100644 index 000000000..b809ce259 --- /dev/null +++ b/e2e/flows/networking-webview/nitro-fetch.yaml @@ -0,0 +1,12 @@ +appId: so.legend.spark.examples.kitchensink +name: react-native-nitro-fetch on macOS +intent: > + The Nitro Fetch screen requests the npm registry through the native client (needs network access), checks + the status, a JSON content type and body, a 404, a prefetched response, and that its URL cache sits in + the app's Caches folder rather than the working directory, then shows passed. +checks: [WEB-NITROFETCH-01] +tags: [networking-webview] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://networking-webview/nitro-fetch" } +- extendedWaitUntil: { visible: { id: networking-webview-nitro-fetch-passed }, timeout: 20s } diff --git a/e2e/flows/processes-ipc/worklets-core.yaml b/e2e/flows/processes-ipc/worklets-core.yaml new file mode 100644 index 000000000..c450b2987 --- /dev/null +++ b/e2e/flows/processes-ipc/worklets-core.yaml @@ -0,0 +1,11 @@ +appId: so.legend.spark.examples.kitchensink +name: react-native-worklets-core on macOS +intent: > + The Worklets Core screen runs a summing worklet in its own context, which writes a shared value and calls + back to the main runtime with createRunOnJS, then shows passed. +checks: [PROC-WORKLETS-01] +tags: [processes-ipc] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://processes-ipc/worklets-core" } +- extendedWaitUntil: { visible: { id: processes-ipc-worklets-core-passed }, timeout: 10s } diff --git a/e2e/flows/security/expo-crypto.yaml b/e2e/flows/security/expo-crypto.yaml new file mode 100644 index 000000000..4f1dec5b3 --- /dev/null +++ b/e2e/flows/security/expo-crypto.yaml @@ -0,0 +1,11 @@ +appId: so.legend.spark.examples.kitchensink +name: expo-crypto on macOS +intent: > + The Expo Crypto screen checks SHA-256 and SHA-1 digests of "abc" against published vectors, random + bytes and a v4 UUID, then shows passed. +checks: [SEC-EXPOCRYPTO-01] +tags: [security] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://security/expo-crypto" } +- extendedWaitUntil: { visible: { id: security-expo-crypto-passed }, timeout: 15s } diff --git a/e2e/flows/security/quick-crypto.yaml b/e2e/flows/security/quick-crypto.yaml new file mode 100644 index 000000000..a8a73f058 --- /dev/null +++ b/e2e/flows/security/quick-crypto.yaml @@ -0,0 +1,11 @@ +appId: so.legend.spark.examples.kitchensink +name: react-native-quick-crypto on macOS +intent: > + The Quick Crypto screen runs hashes, HMAC, PBKDF2, AES-256-GCM, random bytes and a WebCrypto digest + through the native OpenSSL build and shows passed when every result matches its known vector. +checks: [SEC-QUICKCRYPTO-01] +tags: [security] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://security/quick-crypto" } +- extendedWaitUntil: { visible: { id: security-quick-crypto-passed }, timeout: 10s } diff --git a/e2e/flows/security/tracking-transparency.yaml b/e2e/flows/security/tracking-transparency.yaml new file mode 100644 index 000000000..0715942c6 --- /dev/null +++ b/e2e/flows/security/tracking-transparency.yaml @@ -0,0 +1,11 @@ +appId: so.legend.spark.examples.kitchensink +name: react-native-tracking-transparency on macOS +intent: > + The Tracking transparency screen imports the library and asks for the status. App Tracking Transparency + does nothing on the Mac, so the library answers unavailable without native code. +checks: [SEC-ATT-01] +tags: [security] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://security/tracking-transparency" } +- extendedWaitUntil: { visible: { id: security-tracking-transparency-status-unavailable }, timeout: 10s } diff --git a/e2e/flows/storage-data/mmkv.yaml b/e2e/flows/storage-data/mmkv.yaml new file mode 100644 index 000000000..dd5ef57c1 --- /dev/null +++ b/e2e/flows/storage-data/mmkv.yaml @@ -0,0 +1,19 @@ +appId: so.legend.spark.examples.kitchensink +name: react-native-mmkv on macOS +intent: > + The MMKV screen round-trips typed values and a buffer, removes a key and sees every change through a + listener, then shows passed. Its launch counter lives in MMKV: a plain relaunch shows launch 2, and a + relaunch with clearState shows launch 1 again because MMKV stores its files in the app's data folder. +checks: [DATA-MMKV-01, DATA-MMKV-02] +tags: [storage-data] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://storage-data/mmkv" } +- extendedWaitUntil: { visible: { id: storage-data-mmkv-passed }, timeout: 10s } +- assertVisible: { id: storage-data-mmkv-launch-1 } +- launchApp: { openUrl: "spark-ks://storage-data/mmkv" } +- extendedWaitUntil: { visible: { id: storage-data-mmkv-passed }, timeout: 10s } +- assertVisible: { id: storage-data-mmkv-launch-2 } +- launchApp: { clearState: true, openUrl: "spark-ks://storage-data/mmkv" } +- extendedWaitUntil: { visible: { id: storage-data-mmkv-passed }, timeout: 10s } +- assertVisible: { id: storage-data-mmkv-launch-1 } diff --git a/e2e/flows/storage-data/nitro-sqlite.yaml b/e2e/flows/storage-data/nitro-sqlite.yaml new file mode 100644 index 000000000..83af9daaa --- /dev/null +++ b/e2e/flows/storage-data/nitro-sqlite.yaml @@ -0,0 +1,11 @@ +appId: so.legend.spark.examples.kitchensink +name: react-native-nitro-sqlite on macOS +intent: > + The Nitro SQLite screen creates a table, inserts a Unicode row with parameters and two more in a batch, + reads them back asynchronously and sums a column, then deletes the database and shows passed. +checks: [DATA-NITROSQLITE-01] +tags: [storage-data] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://storage-data/nitro-sqlite" } +- extendedWaitUntil: { visible: { id: storage-data-nitro-sqlite-passed }, timeout: 10s } diff --git a/e2e/flows/system-integration/expo-application.yaml b/e2e/flows/system-integration/expo-application.yaml new file mode 100644 index 000000000..a327599d7 --- /dev/null +++ b/e2e/flows/system-integration/expo-application.yaml @@ -0,0 +1,11 @@ +appId: so.legend.spark.examples.kitchensink +name: expo-application on macOS +intent: > + The Expo Application screen reads the bundle identifier and versions, the installation time from the app + bundle, and gets null for the vendor ID and unknown for the release type, then shows passed. +checks: [SYS-EXPOAPP-01] +tags: [system-integration] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://system-integration/expo-application" } +- extendedWaitUntil: { visible: { id: system-integration-expo-application-passed }, timeout: 10s } diff --git a/e2e/flows/system-integration/expo-device.yaml b/e2e/flows/system-integration/expo-device.yaml new file mode 100644 index 000000000..1fcb8d3e3 --- /dev/null +++ b/e2e/flows/system-integration/expo-device.yaml @@ -0,0 +1,11 @@ +appId: so.legend.spark.examples.kitchensink +name: expo-device on macOS +intent: > + The Expo Device screen reads the Mac's model identifier, macOS version and build, device name, memory, CPU + architecture, desktop device type and uptime from expo-device, then shows passed. +checks: [SYS-EXPODEVICE-01] +tags: [system-integration] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://system-integration/expo-device" } +- extendedWaitUntil: { visible: { id: system-integration-expo-device-passed }, timeout: 10s } diff --git a/e2e/flows/system-integration/expo-haptics.yaml b/e2e/flows/system-integration/expo-haptics.yaml new file mode 100644 index 000000000..41c3d621b --- /dev/null +++ b/e2e/flows/system-integration/expo-haptics.yaml @@ -0,0 +1,11 @@ +appId: so.legend.spark.examples.kitchensink +name: expo-haptics on macOS +intent: > + The Expo Haptics screen calls notification, impact and selection feedback; each resolves through + NSHapticFeedbackManager (felt only on a Force Touch trackpad with a finger on it), so it shows passed. +checks: [SYS-EXPOHAPTICS-01] +tags: [system-integration] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://system-integration/expo-haptics" } +- extendedWaitUntil: { visible: { id: system-integration-expo-haptics-passed }, timeout: 10s } diff --git a/e2e/flows/system-integration/expo-location.yaml b/e2e/flows/system-integration/expo-location.yaml new file mode 100644 index 000000000..71b2c8e1c --- /dev/null +++ b/e2e/flows/system-integration/expo-location.yaml @@ -0,0 +1,13 @@ +appId: so.legend.spark.examples.kitchensink +name: expo-location on macOS +intent: > + The Expo Location screen reads the foreground permission, whether Location Services are on and the provider + status, which agree with each other. getHeadingAsync must reject with the exact foreground-permission error + when permission is absent, or the heading-unavailable reason when authorized (a Mac has no compass). + Locating needs a person to answer the permission prompt. +checks: [SYS-EXPOLOCATION-01] +tags: [system-integration] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://system-integration/expo-location" } +- extendedWaitUntil: { visible: { id: system-integration-expo-location-passed }, timeout: 10s } diff --git a/e2e/flows/system-integration/keep-awake.yaml b/e2e/flows/system-integration/keep-awake.yaml new file mode 100644 index 000000000..0dcf54a04 --- /dev/null +++ b/e2e/flows/system-integration/keep-awake.yaml @@ -0,0 +1,11 @@ +appId: so.legend.spark.examples.kitchensink +name: expo-keep-awake on macOS +intent: > + The Keep awake screen activates expo-keep-awake and finds the app's "expo-keep-awake" display-sleep + assertion in pmset, then deactivates and finds it gone, so the screen shows passed. +checks: [SYS-KEEPAWAKE-01] +tags: [system-integration] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://system-integration/keep-awake" } +- extendedWaitUntil: { visible: { id: system-integration-keep-awake-passed }, timeout: 15s } diff --git a/e2e/flows/windows/safe-area.yaml b/e2e/flows/windows/safe-area.yaml new file mode 100644 index 000000000..802b7cb75 --- /dev/null +++ b/e2e/flows/windows/safe-area.yaml @@ -0,0 +1,13 @@ +appId: so.legend.spark.examples.kitchensink +name: react-native-safe-area-context on macOS +intent: > + The Safe area screen wraps its content in SafeAreaProvider. The native provider reports a frame and + finite insets through the hooks, and a SafeAreaView lays out its child. +checks: [WIN-SAFEAREA-01] +tags: [windows] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://windows/safe-area" } +- extendedWaitUntil: { visible: { id: windows-safe-area-provider-ready }, timeout: 10s } +- assertVisible: { id: windows-safe-area-view-laid-out } +- takeScreenshot: safe-area diff --git a/e2e/golden/macos/dark/ar/system-integration-dock.png b/e2e/golden/macos/dark/ar/system-integration-dock.png index 65cbb3bc7..ca927506c 100644 Binary files a/e2e/golden/macos/dark/ar/system-integration-dock.png and b/e2e/golden/macos/dark/ar/system-integration-dock.png differ diff --git a/e2e/golden/macos/dark/en/displays-graphics-linear-gradient.png b/e2e/golden/macos/dark/en/displays-graphics-linear-gradient.png new file mode 100644 index 000000000..8460775d5 Binary files /dev/null and b/e2e/golden/macos/dark/en/displays-graphics-linear-gradient.png differ diff --git a/e2e/golden/macos/dark/en/displays-graphics-masked-view.png b/e2e/golden/macos/dark/en/displays-graphics-masked-view.png new file mode 100644 index 000000000..c97b08461 Binary files /dev/null and b/e2e/golden/macos/dark/en/displays-graphics-masked-view.png differ diff --git a/e2e/golden/macos/dark/en/displays-graphics-svg.png b/e2e/golden/macos/dark/en/displays-graphics-svg.png new file mode 100644 index 000000000..9b00fc1bf Binary files /dev/null and b/e2e/golden/macos/dark/en/displays-graphics-svg.png differ diff --git a/e2e/golden/macos/dark/en/displays-graphics-symbols.png b/e2e/golden/macos/dark/en/displays-graphics-symbols.png new file mode 100644 index 000000000..bdeae5d25 Binary files /dev/null and b/e2e/golden/macos/dark/en/displays-graphics-symbols.png differ diff --git a/e2e/golden/macos/dark/en/displays-graphics-vector-icons.png b/e2e/golden/macos/dark/en/displays-graphics-vector-icons.png new file mode 100644 index 000000000..fdd28620a Binary files /dev/null and b/e2e/golden/macos/dark/en/displays-graphics-vector-icons.png differ diff --git a/e2e/golden/macos/dark/en/infra-shell-catalog.png b/e2e/golden/macos/dark/en/infra-shell-catalog.png index a51518780..759d18e11 100644 Binary files a/e2e/golden/macos/dark/en/infra-shell-catalog.png and b/e2e/golden/macos/dark/en/infra-shell-catalog.png differ diff --git a/e2e/golden/macos/dark/en/native-controls-datetimepicker.png b/e2e/golden/macos/dark/en/native-controls-datetimepicker.png new file mode 100644 index 000000000..3db6083df Binary files /dev/null and b/e2e/golden/macos/dark/en/native-controls-datetimepicker.png differ diff --git a/e2e/golden/macos/dark/en/system-integration-dock.png b/e2e/golden/macos/dark/en/system-integration-dock.png index 5e6f68f34..dbf640773 100644 Binary files a/e2e/golden/macos/dark/en/system-integration-dock.png and b/e2e/golden/macos/dark/en/system-integration-dock.png differ diff --git a/e2e/golden/macos/light/ar/system-integration-dock.png b/e2e/golden/macos/light/ar/system-integration-dock.png index ce58e7750..9148ba027 100644 Binary files a/e2e/golden/macos/light/ar/system-integration-dock.png and b/e2e/golden/macos/light/ar/system-integration-dock.png differ diff --git a/e2e/golden/macos/light/en/infra-shell-catalog.png b/e2e/golden/macos/light/en/infra-shell-catalog.png index 935f67de0..a3a780989 100644 Binary files a/e2e/golden/macos/light/en/infra-shell-catalog.png and b/e2e/golden/macos/light/en/infra-shell-catalog.png differ diff --git a/e2e/golden/macos/light/en/system-integration-dock.png b/e2e/golden/macos/light/en/system-integration-dock.png index c8f79c30f..b2767e755 100644 Binary files a/e2e/golden/macos/light/en/system-integration-dock.png and b/e2e/golden/macos/light/en/system-integration-dock.png differ diff --git a/e2e/pointer/flows/component-library/gesture-handler.yaml b/e2e/pointer/flows/component-library/gesture-handler.yaml new file mode 100644 index 000000000..0939491f6 --- /dev/null +++ b/e2e/pointer/flows/component-library/gesture-handler.yaml @@ -0,0 +1,15 @@ +appId: so.legend.spark.examples.kitchensink +name: react-native-gesture-handler on macOS +intent: > + The Gesture handler screen finds the native module and RNGestureHandlerButton registered, and its + GestureDetector handles a native tap inside a GestureHandlerRootView. Clicking the blue area must + fire the detector's successful tap callback. +checks: [COMP-GESTURE-01] +tags: [component-library] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://component-library/gesture-handler" } +- extendedWaitUntil: { visible: { id: component-library-gesture-handler-native }, timeout: 10s } +- tapOn: { id: component-library-gesture-handler-target } +- extendedWaitUntil: { visible: { id: component-library-gesture-handler-tap-fired }, timeout: 10s } +- takeScreenshot: gesture-handler diff --git a/e2e/pointer/flows/component-library/pressable-pointer.yaml b/e2e/pointer/flows/component-library/pressable-pointer.yaml new file mode 100644 index 000000000..8ecb143e4 --- /dev/null +++ b/e2e/pointer/flows/component-library/pressable-pointer.yaml @@ -0,0 +1,15 @@ +appId: so.legend.spark.examples.kitchensink +name: React Native Pressable with OS pointer input +intent: > + A real mouse down and up must reach React Native's Pressable and fire onPress. + Two consecutive taps must each release the press and increment the count. +checks: [COMP-POINTER-01] +tags: [component-library] +platforms: [macos] +--- +- launchApp: { clearState: true, openUrl: "spark-ks://component-library/pressable-opacity" } +- extendedWaitUntil: { visible: { id: component-library-pressable-opacity-rn-presses-0 }, timeout: 10s } +- tapOn: { id: component-library-pressable-opacity-rn } +- extendedWaitUntil: { visible: { id: component-library-pressable-opacity-rn-presses-1 }, timeout: 10s } +- tapOn: { id: component-library-pressable-opacity-rn } +- extendedWaitUntil: { visible: { id: component-library-pressable-opacity-rn-presses-2 }, timeout: 10s } diff --git a/examples/camera/App.tsx b/examples/camera/App.tsx index ad710aea3..a9d789948 100644 --- a/examples/camera/App.tsx +++ b/examples/camera/App.tsx @@ -83,11 +83,11 @@ function CameraPage({ report }: { report?: string }) { }, []); useEffect(() => { if (!complete || nitro === undefined || !report) return; - void writeText(report, JSON.stringify({ passed: nitro && checks.length > 0 && checks.every(c => c.passed), versions: { reactNative: "0.81.6", reactNativeMacOS: "0.81.7", nitro: "0.37.0", nitroImage: "0.15.2", visionCamera: "5.2.3" }, results: checks, nativeViewPassed: nitro, hardwareChecks: "Pending on a Mac with a camera: permission, preview, photo, video, microphone, device reconnect" }, null, 2)).catch(console.error); + void writeText(report, JSON.stringify({ passed: nitro && checks.length > 0 && checks.every(c => c.passed), versions: { reactNative: "0.83.10", reactNativeMacOS: "0.83.0", nitro: "0.37.0", nitroImage: "0.15.2", visionCamera: "5.2.3" }, results: checks, nativeViewPassed: nitro, hardwareChecks: "Pending on a Mac with a camera: permission, preview, photo, video, microphone, device reconnect" }, null, 2)).catch(console.error); }, [complete, nitro, checks, report]); return <ScrollView contentContainerStyle={{ padding: 28, gap: 20 }}> <Text style={{ fontSize: 28, fontWeight: "700" }}>VisionCamera for macOS · prototype</Text> - <Text>RN macOS 0.81.7 · VisionCamera 5.2.3 · Nitro 0.37.0 · macOS 14+ · Apple Silicon</Text> + <Text>RN macOS 0.83.0 · VisionCamera 5.2.3 · Nitro 0.37.0 · macOS 14+ · Apple Silicon</Text> <Text>Camera-free checks run automatically. Use the controls below on a Mac with a camera to verify preview and capture.</Text> {complete && checks.every(c => c.passed) && <CameraDemo />} <NitroCompatibility onComplete={onNitroComplete} /> diff --git a/examples/camera/CameraDemo.tsx b/examples/camera/CameraDemo.tsx index a35d80672..bf79819c4 100644 --- a/examples/camera/CameraDemo.tsx +++ b/examples/camera/CameraDemo.tsx @@ -29,7 +29,7 @@ export function CameraDemo() { {active && device && camera.hasPermission && <CaptureBoundary onError={report}><LiveCamera key={`${device.id}:${audio}`} device={device} audio={audio} report={report} /></CaptureBoundary>} <Text>Preview quality must be checked visually; this macOS prototype cannot report the preview’s first-frame event.</Text> <Text>Try: resize the window; capture and inspect a photo; record and play a video; stop and restart; unplug and reconnect a USB camera. Check audio playback if enabled.</Text> - <Button title="Export hardware test log…" onPress={() => void action(async () => { const result = await saveFileDialog({ defaultName: "camera-test.json", filters: [{ extensions: ["json"] }] }); if (!result.canceled) { await writeText(result.path, JSON.stringify({ versions: { reactNativeMacOS: "0.81.7", visionCamera: "5.2.3", nitro: "0.37.0" }, devices: devices.map(d => ({ id: d.id, name: d.localizedName })), cameraPermission: camera.status, microphonePermission: microphone.status, log, visualChecks: "Please record visual quality, orientation, playback and reconnect results separately." }, null, 2)); report(`Saved ${result.path}`); } })} /> + <Button title="Export hardware test log…" onPress={() => void action(async () => { const result = await saveFileDialog({ defaultName: "camera-test.json", filters: [{ extensions: ["json"] }] }); if (!result.canceled) { await writeText(result.path, JSON.stringify({ versions: { reactNativeMacOS: "0.83.0", visionCamera: "5.2.3", nitro: "0.37.0" }, devices: devices.map(d => ({ id: d.id, name: d.localizedName })), cameraPermission: camera.status, microphonePermission: microphone.status, log, visualChecks: "Please record visual quality, orientation, playback and reconnect results separately." }, null, 2)); report(`Saved ${result.path}`); } })} /> {log.map((line, i) => <Text selectable key={i} style={{ fontFamily: "Menlo", fontSize: 11 }}>{line}</Text>)} </View>; } diff --git a/examples/camera/NitroCompatibility.tsx b/examples/camera/NitroCompatibility.tsx index 71896096d..3763af599 100644 --- a/examples/camera/NitroCompatibility.tsx +++ b/examples/camera/NitroCompatibility.tsx @@ -44,7 +44,7 @@ export function NitroCompatibility({ report, onComplete }: { report?: string; on const last = JSON.parse(native.current!.snapshot()); if (last.label !== "Updated from React") throw new Error("Remounted native view lost its props"); check("Remount and native method call", last); - if (report) await writeText(report, JSON.stringify({ passed: true, react: "19.1.4", reactNative: "0.81.6", reactNativeMacOS: "0.81.7", nitro: "0.37.0", results }, null, 2)); + if (report) await writeText(report, JSON.stringify({ passed: true, react: "19.2.0", reactNative: "0.83.10", reactNativeMacOS: "0.83.0", nitro: "0.37.0", results }, null, 2)); if (!disposed) onComplete?.(true); } catch (cause) { if (disposed) return; @@ -57,7 +57,7 @@ export function NitroCompatibility({ report, onComplete }: { report?: string; on }, [report, onComplete]); return <View style={{ gap: 12 }} testID="nitro-compatibility"> <Text style={{ fontSize: 18, fontWeight: "600" }}>Native view compatibility</Text> - <Text>React Native macOS 0.81.7 · Nitro 0.37.0</Text> + <Text>React Native macOS 0.83.0 · Nitro 0.37.0</Text> {mounted && <CompatibilityViewComponent label={label} style={{ width, height: 120 }} hybridRef={hybridRef} />} {checks.map(check => <Text key={check.name}>PASS · {check.name}</Text>)} {error ? <Text style={{ color: "#b42318" }}>{error}</Text> : null} diff --git a/examples/kitchen-sink/babel.config.js b/examples/kitchen-sink/babel.config.js new file mode 100644 index 000000000..e1bbefcad --- /dev/null +++ b/examples/kitchen-sink/babel.config.js @@ -0,0 +1,12 @@ +module.exports = api => { + api.cache(true); + // Each library's Babel plugin also rewrites code outside its demo (other libraries' worklets, + // React Native imports), so each one only processes its own demo. + return { + presets: ["babel-preset-expo"], + overrides: [ + { test: /screens\/appearance-theming\/unistyles\//, plugins: [["react-native-unistyles/plugin", { root: "screens/appearance-theming/unistyles" }]] }, + { test: /screens\/processes-ipc\/WorkletsCoreDemo\.tsx$/, plugins: ["react-native-worklets-core/plugin"] }, + ], + }; +}; diff --git a/examples/kitchen-sink/desktop.config.json b/examples/kitchen-sink/desktop.config.json index 5095121c6..d98b1bfd0 100644 --- a/examples/kitchen-sink/desktop.config.json +++ b/examples/kitchen-sink/desktop.config.json @@ -61,7 +61,50 @@ "expo-updates-interface", "react-native-macos", "react-native-enriched-html", - "react-native-enriched-markdown" + "react-native-enriched-markdown", + "@react-native-community/blur", + "@react-native-masked-view/masked-view", + "@sentry/react-native", + "@shopify/react-native-skia", + "expo-application", + "expo-blur", + "expo-crypto", + "expo-device", + "expo-font", + "expo-glass-effect", + "expo-haptics", + "expo-image", + "expo-image-manipulator", + "expo-image-picker", + "expo-keep-awake", + "expo-linear-gradient", + "expo-localization", + "expo-location", + "expo-router", + "expo-splash-screen", + "expo-symbols", + "expo-system-ui", + "expo-web-browser", + "react-native-app-logs", + "react-native-blurhash", + "react-native-fast-image", + "react-native-gesture-handler", + "react-native-keyboard-controller", + "react-native-mmkv", + "react-native-nitro-fetch", + "react-native-nitro-image", + "react-native-nitro-sqlite", + "react-native-nitro-zxing", + "react-native-quick-base64", + "react-native-quick-crypto", + "react-native-reanimated", + "react-native-release-profiler", + "react-native-safe-area-context", + "react-native-screen-corner-radius", + "react-native-tracking-transparency", + "react-native-unistyles", + "react-native-worklets", + "react-native-worklets-core" ] } } diff --git a/examples/kitchen-sink/package.json b/examples/kitchen-sink/package.json index c0a236c61..3c0bbbdcd 100644 --- a/examples/kitchen-sink/package.json +++ b/examples/kitchen-sink/package.json @@ -15,25 +15,25 @@ "doctor": "bun ../../scripts/spark.mjs doctor" }, "dependencies": { - "expo": "54.0.37", + "expo": "55.0.32", "expo-desktop": "1.0.6", "expo-desktop-prebuild-config": "1.1.3", "expo-desktop-config-plugins": "1.3.2", - "expo-desktop-metro-config": "54.81.3", - "expo-desktop-template-bare-minimum": "54.81.6", - "react": "19.1.4", - "react-native": "0.81.6", - "react-native-macos": "0.81.7", + "expo-desktop-metro-config": "55.83.3", + "expo-desktop-template-bare-minimum": "55.83.10", + "react": "19.2.0", + "react-native": "0.83.10", + "react-native-macos": "0.83.0", "@react-native-community/cli": "20.1.3", "typescript": "5.9.3", - "@types/react": "19.1.10", - "react-native-windows": "0.81.35", - "expo-desktop-modules-core": "54.0.14", - "expo-desktop-stubs": "54.0.15", - "@expo/ui": "0.2.0-beta.9", - "expo-clipboard": "8.0.8", - "expo-secure-store": "15.0.8", - "expo-linking": "8.0.12", + "@types/react": "19.2.18", + "react-native-windows": "0.83.2", + "expo-desktop-modules-core": "55.0.2", + "expo-desktop-stubs": "55.0.2", + "@expo/ui": "55.0.17", + "expo-clipboard": "55.0.17", + "expo-secure-store": "55.0.18", + "expo-linking": "55.0.17", "uniwind": "1.6.3", "tailwindcss": "4.2.4", "@legendapp/spark": "0.0.1", @@ -43,12 +43,73 @@ "@babel/parser": "7.28.5", "@babel/traverse": "7.28.5", "@react-native-async-storage/async-storage": "2.2.0", - "@react-native/metro-config": "0.81.6", + "@react-native/metro-config": "0.83.10", "@react-native-community/cli-platform-apple": "20.1.3", "react-native-enriched-markdown": "1.1.1", "react-native-enriched-html": "1.1.1", "react-native-webview": "16.0.0", - "@op-engineering/op-sqlite": "18.2.1" + "@op-engineering/op-sqlite": "18.2.1", + "react-native-mmkv": "4.3.2", + "react-native-unistyles": "3.6.0", + "react-native-quick-crypto": "1.1.9", + "react-native-quick-base64": "3.0.1", + "react-native-nitro-fetch": "1.9.0", + "@react-native-community/blur": "4.4.1", + "react-native-worklets-core": "1.6.3", + "@babel/plugin-proposal-optional-chaining": "7.21.0", + "@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6", + "@babel/plugin-transform-template-literals": "7.29.7", + "react-native-fast-image": "10.9.9", + "react-native-release-profiler": "0.4.5", + "react-native-tracking-transparency": "0.1.2", + "@shopify/react-native-skia": "2.14.0", + "react-native-blurhash": "2.1.3", + "react-native-screen-corner-radius": "0.2.3", + "react-native-nitro-sqlite": "10.2.0", + "react-native-app-logs": "0.3.1", + "react-native-nitro-zxing": "0.3.0", + "react-native-safe-area-context": "5.6.2", + "expo-keep-awake": "55.0.8", + "react-native-screens": "4.23.0", + "react-native-reanimated": "4.2.1", + "react-native-worklets": "0.7.4", + "react-native-gesture-handler": "2.30.0", + "@react-navigation/native": "7.5.1", + "@react-navigation/native-stack": "7.21.0", + "@react-navigation/bottom-tabs": "7.21.0", + "expo-crypto": "55.0.19", + "expo-linear-gradient": "55.0.18", + "expo-web-browser": "55.0.20", + "expo-application": "55.0.19", + "expo-device": "55.0.21", + "expo-localization": "55.0.19", + "expo-haptics": "55.0.18", + "react-native-svg": "15.15.3", + "@react-native-community/netinfo": "11.5.2", + "@expo/vector-icons": "15.1.1", + "expo-font": "55.0.8", + "react-native-drawer-layout": "4.2.12", + "@gorhom/bottom-sheet": "5.2.14", + "react-native-pressable-opacity": "1.0.10", + "expo-status-bar": "55.0.6", + "expo-blur": "55.0.18", + "@babel/plugin-transform-shorthand-properties": "7.27.1", + "@babel/plugin-transform-arrow-functions": "7.27.1", + "react-native-nitro-image": "0.15.2", + "expo-image": "55.0.11", + "expo-symbols": "55.0.9", + "@react-native-masked-view/masked-view": "0.3.2", + "expo-glass-effect": "55.0.11", + "expo-splash-screen": "55.0.25", + "expo-system-ui": "55.0.22", + "expo-image-picker": "55.0.24", + "expo-image-manipulator": "55.0.21", + "expo-location": "55.1.15", + "@react-native-community/datetimepicker": "9.2.2", + "react-native-keyboard-controller": "1.22.9", + "@sentry/react-native": "8.32.0", + "expo-router": "55.0.18", + "react-native-graph": "1.4.0" }, "expo": { "install": { diff --git a/examples/kitchen-sink/screens/app-lifecycle/SplashScreenDemo.tsx b/examples/kitchen-sink/screens/app-lifecycle/SplashScreenDemo.tsx new file mode 100644 index 000000000..b60f509b2 --- /dev/null +++ b/examples/kitchen-sink/screens/app-lifecycle/SplashScreenDemo.tsx @@ -0,0 +1,23 @@ +import { useEffect, useState } from "react"; +import { ScrollView, Text } from "react-native"; +import * as SplashScreen from "expo-splash-screen"; + +// Test IDs: app-lifecycle-splash-screen-passed, or -failed with the error. +const ID = "app-lifecycle-splash-screen"; + +/** expo-splash-screen through its own API; macOS has no launch splash, so its calls resolve without effect. */ +export function SplashScreenDemo() { + const [result, setResult] = useState<string>(); + useEffect(() => { + void (async () => { + SplashScreen.setOptions({ duration: 200, fade: true }); + await SplashScreen.preventAutoHideAsync(); + SplashScreen.hide(); + await SplashScreen.hideAsync(); + })().then(() => setResult("passed"), error => setResult(`failed: ${String(error)}`)); + }, []); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text className="text-sm text-muted">setOptions, preventAutoHideAsync, hide and hideAsync resolve without a native splash screen.</Text> + <Text selectable className="font-mono text-xs text-foreground" testID={result ? `${ID}-${result === "passed" ? "passed" : "failed"}` : undefined}>{result ?? "running"}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/app-lifecycle/index.tsx b/examples/kitchen-sink/screens/app-lifecycle/index.tsx index c6a7fa03d..445eb4e99 100644 --- a/examples/kitchen-sink/screens/app-lifecycle/index.tsx +++ b/examples/kitchen-sink/screens/app-lifecycle/index.tsx @@ -1,7 +1,8 @@ -import { defineScreens } from "../../shell/registry"; +import { defineScreens, lazyScreen } from "../../shell/registry"; import { LaunchInstance } from "./LaunchInstance"; import { LIFECYCLE_MESSAGES } from "./lifecycle-messages"; export default defineScreens("app-lifecycle", [ { id: "launch", title: "Launch & single instance", titles: { ar: LIFECYCLE_MESSAGES.ar.title }, summary: "Single-instance status, forwarded second launches (arguments, files, URLs), relaunch with a quit veto, and safe mode.", summaries: { ar: "حالة النسخة الواحدة، والتشغيلات الثانية المُمرَّرة إليها (الوسائط والملفات والروابط)، وإعادة التشغيل مع رفض الإنهاء، والوضع الآمن." }, component: LaunchInstance }, + { id: "splash-screen", title: "Splash screen", titles: { ar: "شاشة البداية" }, summary: "expo-splash-screen on desktop: its API resolves without effect, since a desktop app has no launch splash.", summaries: { ar: "‏expo-splash-screen على سطح المكتب: تكتمل واجهته البرمجية دون أثر، إذ لا توجد شاشة بداية لتطبيق سطح المكتب." }, component: lazyScreen(() => require("./SplashScreenDemo").SplashScreenDemo) }, ]); diff --git a/examples/kitchen-sink/screens/appearance-theming/SystemUIDemo.tsx b/examples/kitchen-sink/screens/appearance-theming/SystemUIDemo.tsx new file mode 100644 index 000000000..70da62a5a --- /dev/null +++ b/examples/kitchen-sink/screens/appearance-theming/SystemUIDemo.tsx @@ -0,0 +1,28 @@ +import { useEffect, useState } from "react"; +import { ScrollView, Text } from "react-native"; +import * as SystemUI from "expo-system-ui"; + +// Test IDs: appearance-theming-system-ui-passed, or -failed with the failing cases. +const ID = "appearance-theming-system-ui"; + +async function check() { + await SystemUI.setBackgroundColorAsync("#336699"); + const set = String(await SystemUI.getBackgroundColorAsync()).toUpperCase(); + // null restores the window's default background. + await SystemUI.setBackgroundColorAsync(null); + const reset = String(await SystemUI.getBackgroundColorAsync()).toUpperCase(); + return [ + set !== "#336699" && `set (${set})`, + (reset === "NULL" || reset === "#336699") && `reset (${reset})`, + ].filter(Boolean); +} + +/** expo-system-ui through its own API; Spark's patch sets the main window's background on macOS. */ +export function SystemUIDemo() { + const [result, setResult] = useState<string>(); + useEffect(() => { check().then(failures => setResult(failures.length ? `failed: ${failures.join(", ")}` : "passed"), error => setResult(`failed: ${String(error)}`)); }, []); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text className="text-sm text-muted">Sets the main window background, reads it back, then restores the default.</Text> + <Text selectable className="font-mono text-xs text-foreground" testID={result ? `${ID}-${result === "passed" ? "passed" : "failed"}` : undefined}>{result ?? "running"}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/appearance-theming/index.tsx b/examples/kitchen-sink/screens/appearance-theming/index.tsx index 93006ba20..bedfd4e96 100644 --- a/examples/kitchen-sink/screens/appearance-theming/index.tsx +++ b/examples/kitchen-sink/screens/appearance-theming/index.tsx @@ -1,7 +1,9 @@ -import { defineScreens } from "../../shell/registry"; +import { defineScreens, lazyScreen } from "../../shell/registry"; import { APPEARANCE_MESSAGES } from "./appearance-messages"; import { AppearancePreferences } from "./AppearancePreferences"; export default defineScreens("appearance-theming", [ { id: "preferences", title: "Appearance preferences", titles: { ar: APPEARANCE_MESSAGES.ar.title }, summary: "System appearance, accent and highlight colors, increase contrast, reduce transparency and motion, differentiate without color, invert colors and keyboard navigation, with their change events, a per-window appearance override and AppKit semantic colors.", summaries: { ar: "مظهر النظام، ولونا التمييز والتحديد، وزيادة التباين، وتقليل الشفافية والحركة، والتمييز دون استخدام اللون، وعكس الألوان، والتنقل بلوحة المفاتيح، مع أحداث تغيّرها، وتجاوز المظهر لكل نافذة، وألوان AppKit الدلالية." }, component: AppearancePreferences }, + { id: "unistyles", title: "Unistyles", titles: { ar: "أنماط Unistyles" }, summary: "react-native-unistyles on desktop: adaptive light and dark themes that follow the app's appearance, breakpoints from the window width, and runtime values.", summaries: { ar: "‏react-native-unistyles على سطح المكتب: سمات فاتحة وداكنة تتبع مظهر التطبيق، ونقاط توقف حسب عرض النافذة، وقيم وقت التشغيل." }, component: lazyScreen(() => require("./unistyles/UnistylesDemo").UnistylesDemo) }, + { id: "system-ui", title: "System UI", titles: { ar: "واجهة النظام" }, summary: "expo-system-ui on desktop: setting and reading the main window's background color, then restoring the default.", summaries: { ar: "‏expo-system-ui على سطح المكتب: تعيين لون خلفية النافذة الرئيسية وقراءته ثم استعادة اللون الافتراضي." }, component: lazyScreen(() => require("./SystemUIDemo").SystemUIDemo) }, ]); diff --git a/examples/kitchen-sink/screens/appearance-theming/unistyles/UnistylesDemo.tsx b/examples/kitchen-sink/screens/appearance-theming/unistyles/UnistylesDemo.tsx new file mode 100644 index 000000000..2d8088961 --- /dev/null +++ b/examples/kitchen-sink/screens/appearance-theming/unistyles/UnistylesDemo.tsx @@ -0,0 +1,36 @@ +import { Text, View } from "react-native"; +import { StyleSheet, useUnistyles } from "react-native-unistyles"; + +// Test IDs: appearance-theming-unistyles-theme-<light|dark> follows the app's appearance through +// Unistyles' adaptive themes; appearance-theming-unistyles-runtime-ready appears once the native +// runtime reports a window size and pixel ratio. +const ID = "appearance-theming-unistyles"; +const themes = { + light: { colors: { background: "#f4f4f5", foreground: "#18181b", accent: "#2563eb" } }, + dark: { colors: { background: "#18181b", foreground: "#f4f4f5", accent: "#60a5fa" } }, +}; +const breakpoints = { xs: 0, md: 700 }; +type Themes = typeof themes; +type Breakpoints = typeof breakpoints; +declare module "react-native-unistyles" { + export interface UnistylesThemes extends Themes {} + export interface UnistylesBreakpoints extends Breakpoints {} +} +StyleSheet.configure({ themes, breakpoints, settings: { adaptiveThemes: true } }); + +const styles = StyleSheet.create((theme, rt) => ({ + card: { gap: 8, padding: 16, borderRadius: 12, borderWidth: 2, borderColor: theme.colors.accent, backgroundColor: theme.colors.background, width: { xs: "100%", md: rt.screen.width / 2 } }, + text: { color: theme.colors.foreground, fontFamily: "Menlo", fontSize: 12 }, +})); + +/** react-native-unistyles themes, breakpoints and runtime values from its own API. */ +export function UnistylesDemo() { + const { rt } = useUnistyles(); + return <View style={{ padding: 24, gap: 12 }}> + <View style={styles.card}> + <Text style={styles.text} testID={`${ID}-theme-${rt.themeName}`}>{`theme: ${rt.themeName} (color scheme ${rt.colorScheme})`}</Text> + <Text style={styles.text} testID={`${ID}-breakpoint-${rt.breakpoint}`}>{`breakpoint: ${rt.breakpoint}`}</Text> + <Text style={styles.text} testID={rt.screen.width > 0 && rt.pixelRatio >= 1 ? `${ID}-runtime-ready` : undefined}>{`window: ${Math.round(rt.screen.width)}×${Math.round(rt.screen.height)} @${rt.pixelRatio}x, insets ${JSON.stringify(rt.insets)}`}</Text> + </View> + </View>; +} diff --git a/examples/kitchen-sink/screens/component-library/BottomSheetDemo.tsx b/examples/kitchen-sink/screens/component-library/BottomSheetDemo.tsx new file mode 100644 index 000000000..4ce4a7dc8 --- /dev/null +++ b/examples/kitchen-sink/screens/component-library/BottomSheetDemo.tsx @@ -0,0 +1,27 @@ +import { useEffect, useRef, useState } from "react"; +import { Text, View } from "react-native"; +import BottomSheet, { BottomSheetView } from "@gorhom/bottom-sheet"; +import { GestureHandlerRootView } from "react-native-gesture-handler"; + +// Test ID: component-library-bottom-sheet-index-1 once the sheet, snapped to its second point, reports it. +const ID = "component-library-bottom-sheet"; + +/** @gorhom/bottom-sheet through its own API, on Reanimated and Gesture Handler. */ +export function BottomSheetDemo() { + const sheet = useRef<BottomSheet>(null); + const [index, setIndex] = useState(0); + // The sheet ignores snaps until it has measured its layout, so keep asking until it reports index 1. + useEffect(() => { + if (index === 1) return; + const timer = setInterval(() => sheet.current?.snapToIndex(1), 300); + return () => clearInterval(timer); + }, [index]); + return <GestureHandlerRootView style={{ flex: 1 }}> + <View style={{ flex: 1, padding: 24 }}> + <Text className="font-mono text-xs text-foreground" testID={`${ID}-index-${index}`}>{`sheet index ${index}`}</Text> + <BottomSheet ref={sheet} snapPoints={[120, 260]} index={0} onChange={setIndex} backgroundStyle={{ backgroundColor: "#1e293b" }}> + <BottomSheetView style={{ padding: 24 }}><Text style={{ color: "white" }}>Sheet content</Text></BottomSheetView> + </BottomSheet> + </View> + </GestureHandlerRootView>; +} diff --git a/examples/kitchen-sink/screens/component-library/DrawerLayoutDemo.tsx b/examples/kitchen-sink/screens/component-library/DrawerLayoutDemo.tsx new file mode 100644 index 000000000..0b6a13575 --- /dev/null +++ b/examples/kitchen-sink/screens/component-library/DrawerLayoutDemo.tsx @@ -0,0 +1,28 @@ +import { useEffect, useState } from "react"; +import { Text, View } from "react-native"; +import { Drawer } from "react-native-drawer-layout"; +import { GestureHandlerRootView } from "react-native-gesture-handler"; + +// Test IDs: component-library-drawer-layout-opened and -closed after the drawer opens, then closes again. +// The drawer also reports onClose when it mounts closed, so events count only from the first open. +const ID = "component-library-drawer-layout"; + +/** react-native-drawer-layout through its own API, on Reanimated and Gesture Handler. */ +export function DrawerLayoutDemo() { + const [open, setOpen] = useState(false); + const [events, setEvents] = useState<string[]>([]); + useEffect(() => { + const opening = setTimeout(() => setOpen(true), 300); + const closing = setTimeout(() => setOpen(false), 1500); + return () => { clearTimeout(opening); clearTimeout(closing); }; + }, []); + return <GestureHandlerRootView style={{ flex: 1 }}> + <Drawer open={open} onOpen={() => { setOpen(true); setEvents(previous => [...previous, "opened"]); }} onClose={() => { setOpen(false); setEvents(previous => previous.includes("opened") ? [...previous, "closed"] : previous); }} + drawerStyle={{ backgroundColor: "#1e293b" }} renderDrawerContent={() => <View style={{ padding: 24 }}><Text style={{ color: "white" }}>Drawer content</Text></View>}> + <View style={{ flex: 1, padding: 24, gap: 8 }}> + <Text className="text-foreground">Opens after 0.3 s and closes after 1.5 s.</Text> + {events.map((event, index) => <Text key={index} className="font-mono text-xs text-foreground" testID={`${ID}-${event}`}>{event}</Text>)} + </View> + </Drawer> + </GestureHandlerRootView>; +} diff --git a/examples/kitchen-sink/screens/component-library/ExpoRouterDemo.tsx b/examples/kitchen-sink/screens/component-library/ExpoRouterDemo.tsx new file mode 100644 index 000000000..9c35bd248 --- /dev/null +++ b/examples/kitchen-sink/screens/component-library/ExpoRouterDemo.tsx @@ -0,0 +1,11 @@ +import { View } from "react-native"; +import { ExpoRoot } from "expo-router"; + +// The routes live in ./expo-router; `location` starts the router at "/" instead of the Kitchen Sink's own +// launch URL. Test IDs come from the routes. +const routes = require.context("./expo-router", true, /\.tsx$/); + +/** expo-router through its own API: file-based routes with a stack, a dynamic segment and search params. */ +export function ExpoRouterDemo() { + return <View className="flex-1 bg-background"><ExpoRoot context={routes} location="/" /></View>; +} diff --git a/examples/kitchen-sink/screens/component-library/GestureHandlerDemo.tsx b/examples/kitchen-sink/screens/component-library/GestureHandlerDemo.tsx new file mode 100644 index 000000000..184b5be82 --- /dev/null +++ b/examples/kitchen-sink/screens/component-library/GestureHandlerDemo.tsx @@ -0,0 +1,32 @@ +import { useState } from "react"; +import { Text, TurboModuleRegistry, UIManager, View } from "react-native"; +import { Gesture, GestureDetector, GestureHandlerRootView, RectButton } from "react-native-gesture-handler"; + +// Test IDs: component-library-gesture-handler-native when the native module and button component are +// registered; -tap-fired only after the detector's native tap handler succeeds. +const ID = "component-library-gesture-handler"; + +/** react-native-gesture-handler through its own API; it supports macOS upstream. */ +export function GestureHandlerDemo() { + const [events, setEvents] = useState<string[]>([]); + const log = (event: string) => setEvents(previous => [event, ...previous].slice(0, 5)); + const native = TurboModuleRegistry.get("RNGestureHandlerModule") != null && UIManager.hasViewManagerConfig("RNGestureHandlerButton"); + const gesture = Gesture.Race( + Gesture.Tap().runOnJS(true).onEnd((_event, success) => { if (success) log("tap"); }), + Gesture.Pan().runOnJS(true).onEnd(event => log(`pan ${Math.round(event.translationX)},${Math.round(event.translationY)}`)), + ); + return <GestureHandlerRootView style={{ flex: 1 }}> + <View style={{ padding: 24, gap: 12 }}> + <Text className="text-sm text-muted" testID={native ? `${ID}-native` : undefined}>{`Native module and button registered: ${native ? "yes" : "no"}`}</Text> + <GestureDetector gesture={gesture}> + <View testID={`${ID}-target`} collapsable={false} style={{ height: 120, borderRadius: 12, backgroundColor: "#3b82f6", justifyContent: "center", alignItems: "center" }}> + <Text style={{ color: "white" }}>Click or drag here</Text> + </View> + </GestureDetector> + <RectButton onPress={() => log("button")} style={{ padding: 12, borderRadius: 8, backgroundColor: "#10b981" }}> + <Text style={{ color: "white" }}>RectButton</Text> + </RectButton> + <Text testID={events.includes("tap") ? `${ID}-tap-fired` : undefined} className="font-mono text-xs text-foreground">{events.join(" ") || "no events yet"}</Text> + </View> + </GestureHandlerRootView>; +} diff --git a/examples/kitchen-sink/screens/component-library/NavigationDemo.tsx b/examples/kitchen-sink/screens/component-library/NavigationDemo.tsx new file mode 100644 index 000000000..9a98eca45 --- /dev/null +++ b/examples/kitchen-sink/screens/component-library/NavigationDemo.tsx @@ -0,0 +1,60 @@ +import { useEffect, useState } from "react"; +import { Text, View } from "react-native"; +import { DarkTheme, NavigationContainer, NavigationIndependentTree, useNavigation, type NavigationProp } from "@react-navigation/native"; +import { createNativeStackNavigator } from "@react-navigation/native-stack"; +import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; + +// Test IDs (component-library-navigation-…): stack-header-<title> is the stack's header title; stack-<screen> +// is a stack screen's content; stack-done after Home → Details → back; tab-<name> is the visible tab. +const ID = "component-library-navigation"; +type StackParams = { Home: undefined; Details: undefined }; +const Stack = createNativeStackNavigator<StackParams>(); +const Tabs = createBottomTabNavigator(); + +function StackPage({ name, onDone }: { name: keyof StackParams; onDone: () => void }) { + const navigation = useNavigation<NavigationProp<StackParams>>(); + const [visits, setVisits] = useState(0); + useEffect(() => navigation.addListener("focus", () => setVisits(count => count + 1)), [navigation]); + useEffect(() => { + if (name === "Home" && visits === 1) setTimeout(() => navigation.navigate("Details"), 300); + if (name === "Details" && visits === 1) setTimeout(() => navigation.goBack(), 300); + if (name === "Home" && visits === 2) onDone(); + }, [name, visits, navigation, onDone]); + return <View style={{ padding: 16 }}><Text className="text-foreground" testID={`${ID}-stack-${name}`}>{`${name} (visit ${visits})`}</Text></View>; +} + +// The test ID appears only once the title has a size, so a header that never lays out fails the flow. +function HeaderTitle({ title }: { title: string }) { + const [laidOut, setLaidOut] = useState(false); + return <Text className="text-foreground" onLayout={event => setLaidOut(event.nativeEvent.layout.height > 0)} testID={laidOut ? `${ID}-stack-header-${title}` : undefined}>{title}</Text>; +} + +function TabPage({ name }: { name: string }) { + const navigation = useNavigation<NavigationProp<Record<string, undefined>>>(); + useEffect(() => { if (name === "First") setTimeout(() => navigation.navigate("Second"), 300); }, [name, navigation]); + return <View style={{ padding: 16 }}><Text className="text-foreground" testID={`${ID}-tab-${name}`}>{`${name} tab`}</Text></View>; +} + +/** React Navigation through its own API: native stack and bottom tabs. */ +export function NavigationDemo() { + const [done, setDone] = useState(false); + return <View style={{ flex: 1, padding: 24, gap: 12 }}> + <View style={{ height: 220, borderWidth: 1, borderColor: "#888", borderRadius: 8, overflow: "hidden" }}> + <NavigationIndependentTree><NavigationContainer theme={DarkTheme}> + <Stack.Navigator screenOptions={{ headerTitle: ({ children }) => <HeaderTitle title={children} /> }}> + <Stack.Screen name="Home">{() => <StackPage name="Home" onDone={() => setDone(true)} />}</Stack.Screen> + <Stack.Screen name="Details">{() => <StackPage name="Details" onDone={() => setDone(true)} />}</Stack.Screen> + </Stack.Navigator> + </NavigationContainer></NavigationIndependentTree> + </View> + <View style={{ height: 180, borderWidth: 1, borderColor: "#888", borderRadius: 8, overflow: "hidden" }}> + <NavigationIndependentTree><NavigationContainer theme={DarkTheme}> + <Tabs.Navigator> + <Tabs.Screen name="First">{() => <TabPage name="First" />}</Tabs.Screen> + <Tabs.Screen name="Second">{() => <TabPage name="Second" />}</Tabs.Screen> + </Tabs.Navigator> + </NavigationContainer></NavigationIndependentTree> + </View> + <Text className="font-mono text-xs text-foreground" testID={done ? `${ID}-stack-done` : undefined}>{done ? "stack: Home → Details → back" : "stack: navigating…"}</Text> + </View>; +} diff --git a/examples/kitchen-sink/screens/component-library/PressableOpacityDemo.tsx b/examples/kitchen-sink/screens/component-library/PressableOpacityDemo.tsx new file mode 100644 index 000000000..d027d1360 --- /dev/null +++ b/examples/kitchen-sink/screens/component-library/PressableOpacityDemo.tsx @@ -0,0 +1,29 @@ +import { useState } from "react"; +import { Pressable, ScrollView, Text } from "react-native"; +import { GestureHandlerRootView } from "react-native-gesture-handler"; +import { NativePressableOpacity, PressableOpacity } from "react-native-pressable-opacity"; + +// Test IDs: component-library-pressable-opacity-js and -native once each button lays out. +const ID = "component-library-pressable-opacity"; + +/** react-native-pressable-opacity alongside React Native's own Pressable. */ +export function PressableOpacityDemo() { + const [shown, setShown] = useState<string[]>([]); + const [rnPresses, setRNPresses] = useState(0); + const [presses, setPresses] = useState(0); + const show = (name: string) => setShown(previous => previous.includes(name) ? previous : [...previous, name]); + // NativePressableOpacity uses a GestureDetector, which needs a root view. + return <GestureHandlerRootView style={{ flex: 1 }}><ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Pressable testID={`${ID}-rn`} onPress={() => setRNPresses(count => count + 1)} style={{ padding: 12, borderRadius: 8, backgroundColor: "#3b82f6" }}> + <Text style={{ color: "white" }}>React Native Pressable</Text> + </Pressable> + <Text testID={`${ID}-rn-presses-${rnPresses}`} className="font-mono text-xs text-foreground">{`React Native presses ${rnPresses}`}</Text> + <PressableOpacity onPress={() => setPresses(count => count + 1)} onLayout={() => show("js")} style={{ padding: 12, borderRadius: 8, backgroundColor: "#3b82f6" }}> + <Text style={{ color: "white" }} testID={shown.includes("js") ? `${ID}-js` : undefined}>PressableOpacity</Text> + </PressableOpacity> + <NativePressableOpacity onPress={() => setPresses(count => count + 1)} onLayout={() => show("native")} style={{ padding: 12, borderRadius: 8, backgroundColor: "#10b981" }}> + <Text style={{ color: "white" }} testID={shown.includes("native") ? `${ID}-native` : undefined}>NativePressableOpacity</Text> + </NativePressableOpacity> + <Text className="font-mono text-xs text-foreground">{`presses ${presses}`}</Text> + </ScrollView></GestureHandlerRootView>; +} diff --git a/examples/kitchen-sink/screens/component-library/ReanimatedDemo.tsx b/examples/kitchen-sink/screens/component-library/ReanimatedDemo.tsx new file mode 100644 index 000000000..12691ca62 --- /dev/null +++ b/examples/kitchen-sink/screens/component-library/ReanimatedDemo.tsx @@ -0,0 +1,28 @@ +import { useEffect, useState } from "react"; +import { Text, View } from "react-native"; +import Animated, { runOnJS, useAnimatedStyle, useFrameCallback, useSharedValue, withTiming } from "react-native-reanimated"; + +// Test IDs: component-library-reanimated-finished once a 400 ms timing animation reports it finished at 1; +// component-library-reanimated-frames once frame callbacks have run on the UI thread. +const ID = "component-library-reanimated"; + +/** react-native-reanimated through its own API; it supports macOS upstream. */ +export function ReanimatedDemo() { + const progress = useSharedValue(0); + const frames = useSharedValue(0); + const [finished, setFinished] = useState<string>(); + const [frameCount, setFrameCount] = useState(0); + useFrameCallback(() => { + frames.value += 1; + if (frames.value % 10 === 0) runOnJS(setFrameCount)(frames.value); + }); + useEffect(() => { + progress.value = withTiming(1, { duration: 400 }, done => { if (done) runOnJS(setFinished)(String(progress.value)); }); + }, [progress]); + const box = useAnimatedStyle(() => ({ opacity: 0.2 + progress.value * 0.8, transform: [{ translateX: progress.value * 200 }] })); + return <View style={{ padding: 24, gap: 12 }}> + <Animated.View style={[{ width: 80, height: 80, borderRadius: 12, backgroundColor: "#3b82f6" }, box]} /> + <Text className="font-mono text-xs text-foreground" testID={finished === "1" ? `${ID}-finished` : undefined}>{`timing finished at ${finished ?? "…"}`}</Text> + <Text className="font-mono text-xs text-foreground" testID={frameCount >= 10 ? `${ID}-frames` : undefined}>{`frame callbacks ${frameCount}`}</Text> + </View>; +} diff --git a/examples/kitchen-sink/screens/component-library/ScreensDemo.tsx b/examples/kitchen-sink/screens/component-library/ScreensDemo.tsx new file mode 100644 index 000000000..cdaa790d9 --- /dev/null +++ b/examples/kitchen-sink/screens/component-library/ScreensDemo.tsx @@ -0,0 +1,34 @@ +import { useState } from "react"; +import { Text, View } from "react-native"; +import { Screen, ScreenContainer, ScreenStack, screensEnabled } from "react-native-screens"; +import { Button } from "../../Controls"; + +// Test IDs: component-library-screens-<name>-visible for the screens React lays out (the hidden one has +// display: none, so it never reports a layout). The button activates the second screen by hand. +const ID = "component-library-screens"; + +function Page({ name, onShown }: { name: string; onShown: (name: string) => void }) { + return <View style={{ padding: 12 }} onLayout={event => event.nativeEvent.layout.height > 0 && onShown(name)}> + <Text className="text-foreground">{`Screen ${name}`}</Text> + </View>; +} + +/** react-native-screens components; on macOS Spark's patch uses their web (plain view) implementations. */ +export function ScreensDemo() { + const [active, setActive] = useState<"first" | "second">("first"); + const [shown, setShown] = useState<string[]>([]); + const show = (name: string) => setShown(previous => previous.includes(name) ? previous : [...previous, name]); + return <View style={{ padding: 24, gap: 12 }}> + <Text className="text-sm text-muted">{`Native screens enabled: ${screensEnabled()}`}</Text> + <ScreenContainer style={{ height: 80, borderWidth: 1, borderColor: "#888" }}> + {(["first", "second"] as const).map(name => <Screen key={name} enabled activityState={active === name ? 2 : 0} style={{ position: "absolute", inset: 0 }}> + <Page name={name} onShown={show} /> + </Screen>)} + </ScreenContainer> + <ScreenStack style={{ height: 60, borderWidth: 1, borderColor: "#888" }}> + <Screen enabled activityState={2} style={{ position: "absolute", inset: 0 }}><Page name="stack" onShown={show} /></Screen> + </ScreenStack> + <Button onPress={() => setActive("second")}>Activate the second screen</Button> + {shown.map(name => <Text key={name} className="font-mono text-xs text-foreground" testID={`${ID}-${name}-visible`}>{`${name} visible`}</Text>)} + </View>; +} diff --git a/examples/kitchen-sink/screens/component-library/StatusBarDemo.tsx b/examples/kitchen-sink/screens/component-library/StatusBarDemo.tsx new file mode 100644 index 000000000..592a1c551 --- /dev/null +++ b/examples/kitchen-sink/screens/component-library/StatusBarDemo.tsx @@ -0,0 +1,19 @@ +import { useEffect, useState } from "react"; +import { ScrollView, Text } from "react-native"; +import { StatusBar, setStatusBarHidden, setStatusBarStyle } from "expo-status-bar"; + +// Test ID: component-library-expo-status-bar-ran once the component mounted and the setters returned. +const ID = "component-library-expo-status-bar"; + +/** expo-status-bar on desktop: nothing to style, but apps calling it must not crash. */ +export function StatusBarDemo() { + const [ran, setRan] = useState<string>(); + useEffect(() => { + try { setStatusBarStyle("light"); setStatusBarHidden(false, "none"); setRan("ran"); } + catch (error) { setRan(`failed: ${String(error)}`); } + }, []); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <StatusBar style="auto" /> + <Text className="font-mono text-xs text-foreground" testID={ran === "ran" ? `${ID}-ran` : undefined}>{ran ?? "…"}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/component-library/expo-router/_layout.tsx b/examples/kitchen-sink/screens/component-library/expo-router/_layout.tsx new file mode 100644 index 000000000..99051d62b --- /dev/null +++ b/examples/kitchen-sink/screens/component-library/expo-router/_layout.tsx @@ -0,0 +1,7 @@ +import { useColorScheme } from "react-native"; +import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native"; +import { Stack } from "expo-router"; + +export default function Layout() { + return <ThemeProvider value={useColorScheme() === "dark" ? DarkTheme : DefaultTheme}><Stack /></ThemeProvider>; +} diff --git a/examples/kitchen-sink/screens/component-library/expo-router/details/[id].tsx b/examples/kitchen-sink/screens/component-library/expo-router/details/[id].tsx new file mode 100644 index 000000000..2ea3aaa4b --- /dev/null +++ b/examples/kitchen-sink/screens/component-library/expo-router/details/[id].tsx @@ -0,0 +1,12 @@ +import { Text, View } from "react-native"; +import { Stack, useLocalSearchParams, usePathname } from "expo-router"; + +// Test ID: component-library-expo-router-details-<id>-<from>. +export default function Details() { + const { id, from } = useLocalSearchParams<{ id: string; from?: string }>(); + const pathname = usePathname(); + return <View className="flex-1 gap-3 p-6"> + <Stack.Screen options={{ title: `Details ${id}` }} /> + <Text className="text-foreground" testID={`component-library-expo-router-details-${id}-${from ?? "none"}`}>{`${pathname} from ${from ?? "none"}`}</Text> + </View>; +} diff --git a/examples/kitchen-sink/screens/component-library/expo-router/index.tsx b/examples/kitchen-sink/screens/component-library/expo-router/index.tsx new file mode 100644 index 000000000..fb825b036 --- /dev/null +++ b/examples/kitchen-sink/screens/component-library/expo-router/index.tsx @@ -0,0 +1,14 @@ +import { useEffect } from "react"; +import { Text, View } from "react-native"; +import { Link, Stack, useRouter } from "expo-router"; + +// Test ID: component-library-expo-router-home; opening it pushes /details/42. +export default function Home() { + const router = useRouter(); + useEffect(() => { router.push("/details/42?from=home"); }, [router]); + return <View className="flex-1 gap-3 p-6"> + <Stack.Screen options={{ title: "Home" }} /> + <Text className="text-foreground" testID="component-library-expo-router-home">Home route</Text> + <Link href="/details/7" className="text-accent">Open details 7</Link> + </View>; +} diff --git a/examples/kitchen-sink/screens/component-library/index.tsx b/examples/kitchen-sink/screens/component-library/index.tsx new file mode 100644 index 000000000..07f92cbad --- /dev/null +++ b/examples/kitchen-sink/screens/component-library/index.tsx @@ -0,0 +1,13 @@ +import { defineScreens, lazyScreen } from "../../shell/registry"; + +export default defineScreens("component-library", [ + { id: "screens", title: "Screens", titles: { ar: "شاشات التنقل" }, summary: "react-native-screens on desktop: Screen, ScreenContainer and ScreenStack render as plain views, showing the active screen and hiding inactive ones.", summaries: { ar: "‏react-native-screens على سطح المكتب: تُعرض Screen وScreenContainer وScreenStack كعروض عادية، فتظهر الشاشة النشطة وتُخفى غير النشطة." }, component: lazyScreen(() => require("./ScreensDemo").ScreensDemo) }, + { id: "reanimated", title: "Reanimated", titles: { ar: "الحركة مع Reanimated" }, summary: "react-native-reanimated and react-native-worklets on desktop: a timing animation driven on the UI thread, frame callbacks and an animated style.", summaries: { ar: "‏react-native-reanimated وreact-native-worklets على سطح المكتب: حركة توقيت تُدار على خيط الواجهة، واستدعاءات الإطارات، ونمط متحرك." }, component: lazyScreen(() => require("./ReanimatedDemo").ReanimatedDemo) }, + { id: "gesture-handler", title: "Gesture handler", titles: { ar: "معالج الإيماءات" }, summary: "react-native-gesture-handler on desktop: the native module and button component, a root view, and tap and pan gestures driven by the mouse or trackpad.", summaries: { ar: "‏react-native-gesture-handler على سطح المكتب: الوحدة الأصلية ومكوّن الزر وعرض الجذر، وإيماءات النقر والسحب بالفأرة أو لوحة التتبع." }, component: lazyScreen(() => require("./GestureHandlerDemo").GestureHandlerDemo) }, + { id: "navigation", title: "React Navigation", titles: { ar: "التنقل مع React Navigation" }, summary: "React Navigation on desktop: a native stack with headers and back navigation, and bottom tabs, on top of react-native-screens and safe-area-context.", summaries: { ar: "‏React Navigation على سطح المكتب: مكدس أصلي مع ترويسات ورجوع، وعلامات تبويب سفلية، فوق react-native-screens وsafe-area-context." }, component: lazyScreen(() => require("./NavigationDemo").NavigationDemo) }, + { id: "drawer-layout", title: "Drawer layout", titles: { ar: "تخطيط الدرج" }, summary: "react-native-drawer-layout on desktop: a drawer that opens and closes with Reanimated and Gesture Handler.", summaries: { ar: "‏react-native-drawer-layout على سطح المكتب: درج يُفتح ويُغلق باستخدام Reanimated وGesture Handler." }, component: lazyScreen(() => require("./DrawerLayoutDemo").DrawerLayoutDemo) }, + { id: "bottom-sheet", title: "Bottom sheet", titles: { ar: "اللوحة السفلية" }, summary: "@gorhom/bottom-sheet on desktop: a sheet with snap points that snaps and reports its index.", summaries: { ar: "‏@gorhom/bottom-sheet على سطح المكتب: لوحة بنقاط التقاط تنتقل بينها وتُبلغ عن موضعها." }, component: lazyScreen(() => require("./BottomSheetDemo").BottomSheetDemo) }, + { id: "pressable-opacity", title: "Pressable opacity", titles: { ar: "شفافية الضغط" }, summary: "react-native-pressable-opacity on desktop: PressableOpacity and NativePressableOpacity buttons.", summaries: { ar: "‏react-native-pressable-opacity على سطح المكتب: أزرار PressableOpacity وNativePressableOpacity." }, component: lazyScreen(() => require("./PressableOpacityDemo").PressableOpacityDemo) }, + { id: "expo-status-bar", title: "Expo Status Bar", titles: { ar: "شريط الحالة في Expo" }, summary: "expo-status-bar on desktop: the component renders and its setters run without a status bar to change.", summaries: { ar: "‏expo-status-bar على سطح المكتب: يُعرض المكوّن وتعمل دوال الضبط دون شريط حالة لتغييره." }, component: lazyScreen(() => require("./StatusBarDemo").StatusBarDemo) }, + { id: "expo-router", title: "Expo Router", titles: { ar: "موجّه Expo" }, summary: "expo-router on desktop: file-based routes in a native stack, pushing a dynamic route with search params.", summaries: { ar: "‏expo-router على سطح المكتب: مسارات قائمة على الملفات داخل مكدس أصلي، مع الانتقال إلى مسار ديناميكي بمعاملات بحث." }, component: lazyScreen(() => require("./ExpoRouterDemo").ExpoRouterDemo) }, +]); diff --git a/examples/kitchen-sink/screens/devtools/AppLogsDemo.tsx b/examples/kitchen-sink/screens/devtools/AppLogsDemo.tsx new file mode 100644 index 000000000..6ad12d498 --- /dev/null +++ b/examples/kitchen-sink/screens/devtools/AppLogsDemo.tsx @@ -0,0 +1,23 @@ +import { useEffect, useState } from "react"; +import { ScrollView, Text } from "react-native"; +import AppLogs from "react-native-app-logs"; + +// Test ID: devtools-app-logs-received once a log line this screen wrote comes back from OSLogStore. +const ID = "devtools-app-logs"; +const MARKER = "kitchen-sink-app-logs-marker"; + +/** react-native-app-logs through its own API; Spark adds the macOS build (OSLogStore exists from macOS 12). */ +export function AppLogsDemo() { + const [received, setReceived] = useState<string>(); + useEffect(() => { + AppLogs.configure({ interval: 0.5 }); + const subscription = AppLogs.registerHandler({ filter: MARKER, handler: ({ logs }) => { if (logs.length) setReceived(logs[0]!.message); } }); + // React Native writes console output to the unified log. + const timer = setInterval(() => console.warn(MARKER), 500); + return () => { clearInterval(timer); subscription.remove(); }; + }, []); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text className="text-sm text-muted">Writes a marker to the console every 0.5 s and waits for it to come back from the app's unified log.</Text> + <Text selectable className="font-mono text-xs text-foreground" testID={received ? `${ID}-received` : undefined}>{received ?? "waiting"}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/devtools/ReleaseProfilerDemo.tsx b/examples/kitchen-sink/screens/devtools/ReleaseProfilerDemo.tsx new file mode 100644 index 000000000..407ccc83c --- /dev/null +++ b/examples/kitchen-sink/screens/devtools/ReleaseProfilerDemo.tsx @@ -0,0 +1,42 @@ +import { useEffect, useState } from "react"; +import { ScrollView, Text } from "react-native"; +import { startProfiling, stopProfiling } from "react-native-release-profiler"; +import { getDirectory, readText, remove } from "@legendapp/spark/files"; + +// Test IDs: devtools-release-profiler-passed, or -failed with the failing cases. +const ID = "devtools-release-profiler"; + +function busyWork() { + let total = 0; + for (let i = 0; i < 2_000_000; i++) total += Math.sqrt(i); + return total; +} + +async function check() { + startProfiling(); + const deadline = Date.now() + 300; + while (Date.now() < deadline) busyWork(); + const file = await stopProfiling(); + try { + const trace = JSON.parse(await readText(file)) as { traceEvents?: unknown[] }; + return { + file, + failures: [ + !file.startsWith(`${await getDirectory("cache")}/`) && "cache folder", + !(trace.traceEvents?.length) && "trace events", + ].filter(Boolean), + }; + } finally { + await remove(file); + } +} + +/** react-native-release-profiler through its own API; Spark only adds the macOS build and cache folder. */ +export function ReleaseProfilerDemo() { + const [result, setResult] = useState<string>(); + useEffect(() => { check().then(({ file, failures }) => setResult(failures.length ? `failed: ${failures.join(", ")} (${file})` : "passed"), error => setResult(`failed: ${String(error)}`)); }, []); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text className="text-sm text-muted">Profiles 300 ms of busy JavaScript with the Hermes sampling profiler and reads back the trace.</Text> + <Text selectable className="font-mono text-xs text-foreground" testID={result ? `${ID}-${result === "passed" ? "passed" : "failed"}` : undefined}>{result ?? "running"}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/devtools/SentryDemo.tsx b/examples/kitchen-sink/screens/devtools/SentryDemo.tsx new file mode 100644 index 000000000..2f216b635 --- /dev/null +++ b/examples/kitchen-sink/screens/devtools/SentryDemo.tsx @@ -0,0 +1,38 @@ +import { useEffect, useState } from "react"; +import { ScrollView, Text, TurboModuleRegistry } from "react-native"; +import * as Sentry from "@sentry/react-native"; +import type { ErrorEvent } from "@sentry/react-native"; + +// Test IDs: devtools-sentry-passed, or -failed with the failing cases. Nothing is sent: beforeSend keeps +// the event and drops it, and the client closes afterwards. +const ID = "devtools-sentry"; + +async function check() { + let captured: ErrorEvent | undefined; + Sentry.init({ dsn: "https://public@sentry.invalid/1", beforeSend: event => { captured = event; return null; } }); + Sentry.captureException(new Error("Kitchen Sink test error")); + for (let waited = 0; !captured && waited < 3000; waited += 50) await new Promise(resolve => setTimeout(resolve, 50)); + await Sentry.close(); + const native = TurboModuleRegistry.get("RNSentry") != null; + return { + info: `native module ${native ? "linked" : "missing"}; os ${captured?.contexts?.os?.name ?? "?"} ${captured?.contexts?.os?.version ?? ""}; device ${captured?.contexts?.device?.model ?? "?"}`, + failures: [ + captured?.exception?.values?.[0]?.value !== "Kitchen Sink test error" && "captured exception", + !captured?.exception?.values?.[0]?.stacktrace?.frames?.length && "stack trace", + !native && "native module", + captured?.contexts?.os?.name !== "macOS" && `native os context (${captured?.contexts?.os?.name})`, + ].filter(Boolean), + }; +} + +/** @sentry/react-native through its own API; sentry-cocoa supports macOS. */ +export function SentryDemo() { + const [result, setResult] = useState<string>(); + const [info, setInfo] = useState(""); + useEffect(() => { check().then(({ info, failures }) => { setInfo(info); setResult(failures.length ? `failed: ${failures.join(", ")}` : "passed"); }, error => setResult(`failed: ${String(error)}`)); }, []); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text className="text-sm text-muted">Initializes Sentry, captures an error and inspects the event, including the native macOS context.</Text> + <Text selectable className="font-mono text-xs text-muted">{info}</Text> + <Text selectable className="font-mono text-xs text-foreground" testID={result ? `${ID}-${result === "passed" ? "passed" : "failed"}` : undefined}>{result ?? "running"}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/devtools/index.tsx b/examples/kitchen-sink/screens/devtools/index.tsx new file mode 100644 index 000000000..b789b886a --- /dev/null +++ b/examples/kitchen-sink/screens/devtools/index.tsx @@ -0,0 +1,7 @@ +import { defineScreens, lazyScreen } from "../../shell/registry"; + +export default defineScreens("devtools", [ + { id: "release-profiler", title: "Release profiler", titles: { ar: "محلل الإصدار" }, summary: "react-native-release-profiler on desktop: Hermes sampling profiler start and stop, writing a .cpuprofile trace to the app's own Caches folder.", summaries: { ar: "‏react-native-release-profiler على سطح المكتب: بدء محلل العينات في Hermes وإيقافه، مع كتابة ملف ‎.cpuprofile في مجلد Caches الخاص بالتطبيق." }, component: lazyScreen(() => require("./ReleaseProfilerDemo").ReleaseProfilerDemo) }, + { id: "app-logs", title: "App logs", titles: { ar: "سجلات التطبيق" }, summary: "react-native-app-logs on desktop: the app's own unified-log entries from OSLogStore, filtered and delivered to JavaScript.", summaries: { ar: "‏react-native-app-logs على سطح المكتب: إدخالات السجل الموحد الخاصة بالتطبيق من OSLogStore، مُرشَّحة ومُسلَّمة إلى JavaScript." }, component: lazyScreen(() => require("./AppLogsDemo").AppLogsDemo) }, + { id: "sentry", title: "Sentry", titles: { ar: "رصد الأخطاء عبر Sentry" }, summary: "@sentry/react-native on desktop: capturing an error with its stack trace and the native macOS context, without sending it.", summaries: { ar: "‏@sentry/react-native على سطح المكتب: التقاط خطأ مع تتبع المكدس وسياق macOS الأصلي دون إرساله." }, component: lazyScreen(() => require("./SentryDemo").SentryDemo) }, +]); diff --git a/examples/kitchen-sink/screens/displays-graphics/BlurDemo.tsx b/examples/kitchen-sink/screens/displays-graphics/BlurDemo.tsx new file mode 100644 index 000000000..16d0f7cbf --- /dev/null +++ b/examples/kitchen-sink/screens/displays-graphics/BlurDemo.tsx @@ -0,0 +1,35 @@ +import { useState } from "react"; +import { ScrollView, Text, UIManager, View } from "react-native"; +import { BlurView, VibrancyView } from "@react-native-community/blur"; + +// Test IDs: displays-graphics-blur-<BlurView|VibrancyView>-registered per registered native component; +// displays-graphics-blur-<type>-laid-out once each BlurView has a native frame; displays-graphics-blur-vibrancy-child +// is a VibrancyView child, mounted inside the material. +const ID = "displays-graphics-blur"; +const types = ["light", "dark", "material", "thinMaterialDark"] as const; +const stripes = ["#ef4444", "#f59e0b", "#10b981", "#3b82f6", "#a855f7"]; + +/** @react-native-community/blur through its own components; Spark adds the AppKit views. */ +export function BlurDemo() { + const [laidOut, setLaidOut] = useState<Record<string, boolean>>({}); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-4 p-6"> + {(["BlurView", "VibrancyView"] as const).map(name => { + const registered = UIManager.hasViewManagerConfig(name); + return <Text key={name} className="text-sm text-muted" testID={registered ? `${ID}-${name}-registered` : undefined}>{`Native ${name} registered: ${registered ? "yes" : "no"}`}</Text>; + })} + <View style={{ flexDirection: "row", flexWrap: "wrap", gap: 16 }}> + {types.map(type => <View key={type} style={{ width: 200, height: 120, borderRadius: 12, overflow: "hidden" }}> + <View style={{ position: "absolute", inset: 0, flexDirection: "row" }}>{stripes.map(color => <View key={color} style={{ flex: 1, backgroundColor: color }} />)}</View> + <BlurView blurType={type} blurAmount={20} reducedTransparencyFallbackColor="#808080" style={{ position: "absolute", inset: 24 }} + onLayout={event => event.nativeEvent.layout.width > 0 && setLaidOut(previous => ({ ...previous, [type]: true }))} /> + <Text testID={laidOut[type] ? `${ID}-${type}-laid-out` : undefined} style={{ margin: 32, fontWeight: "600" }}>{type}</Text> + </View>)} + <View style={{ width: 200, height: 120, borderRadius: 12, overflow: "hidden" }}> + <View style={{ position: "absolute", inset: 0, flexDirection: "row" }}>{stripes.map(color => <View key={color} style={{ flex: 1, backgroundColor: color }} />)}</View> + <VibrancyView blurType="material" blurAmount={20} style={{ position: "absolute", inset: 24, justifyContent: "center", alignItems: "center" }}> + <Text testID={`${ID}-vibrancy-child`} style={{ fontWeight: "600", color: "white" }}>vibrancy</Text> + </VibrancyView> + </View> + </View> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/displays-graphics/BlurhashDemo.tsx b/examples/kitchen-sink/screens/displays-graphics/BlurhashDemo.tsx new file mode 100644 index 000000000..f6a5c7ad6 --- /dev/null +++ b/examples/kitchen-sink/screens/displays-graphics/BlurhashDemo.tsx @@ -0,0 +1,34 @@ +import { useEffect, useState } from "react"; +import { ScrollView, Text, UIManager } from "react-native"; +import { Blurhash } from "react-native-blurhash"; +import { toByteArray } from "base64-js"; +import { getDirectory, writeBytes } from "@legendapp/spark/files"; + +// Test IDs: displays-graphics-blurhash-decoded once the native view reports onLoadEnd; +// displays-graphics-blurhash-encoded when encoding a local red PNG gives a valid, red Blurhash. +const ID = "displays-graphics-blurhash"; +const HASH = "LGFFaXYk^6#M@-5c,1J5@[or[Q6."; +const PNG = "iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAYAAAB/qH1jAAAAEklEQVR4nGP4z8DwHxkzoAsAAA8hD/EEN8afAAAAAElFTkSuQmCC"; + +/** react-native-blurhash through its own API; Spark adds the AppKit view and NSImage codec. */ +export function BlurhashDemo() { + const [decoded, setDecoded] = useState<string>(); + const [encoded, setEncoded] = useState<string>(); + useEffect(() => { + void (async () => { + const file = `${await getDirectory("data")}/blurhash-demo.png`; + await writeBytes(file, toByteArray(PNG)); + const hash = await Blurhash.encode(`file://${file}`, 4, 3); + const color = Blurhash.getAverageColor(hash); + setEncoded(Blurhash.isBlurhashValid(hash).isValid && color?.r === 255 && color.g === 0 && color.b === 0 ? `ok ${hash}` : `failed ${hash} ${JSON.stringify(color)}`); + })().catch(error => setEncoded(`failed ${String(error)}`)); + }, []); + const registered = UIManager.hasViewManagerConfig("BlurhashView"); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text className="text-sm text-muted">{`Native BlurhashView registered: ${registered ? "yes" : "no"}`}</Text> + <Blurhash blurhash={HASH} resizeMode="cover" style={{ width: 240, height: 160, borderRadius: 8 }} + onLoadEnd={() => setDecoded("ok")} onLoadError={message => setDecoded(`failed ${message}`)} /> + <Text selectable className="font-mono text-xs text-foreground" testID={decoded === "ok" ? `${ID}-decoded` : undefined}>{`decode: ${decoded ?? "…"}`}</Text> + <Text selectable className="font-mono text-xs text-foreground" testID={encoded?.startsWith("ok") ? `${ID}-encoded` : undefined}>{`encode: ${encoded ?? "…"}`}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/displays-graphics/ExpoBlurDemo.tsx b/examples/kitchen-sink/screens/displays-graphics/ExpoBlurDemo.tsx new file mode 100644 index 000000000..934b65c82 --- /dev/null +++ b/examples/kitchen-sink/screens/displays-graphics/ExpoBlurDemo.tsx @@ -0,0 +1,27 @@ +import { useState } from "react"; +import { ScrollView, Text, View } from "react-native"; +import { requireOptionalNativeModule } from "expo-modules-core"; +import { BlurView, type BlurTint } from "expo-blur"; + +// Test IDs: displays-graphics-expo-blur-native when the native module is linked; +// displays-graphics-expo-blur-<tint>-laid-out once each BlurView has a native frame. +const ID = "displays-graphics-expo-blur"; +const tints: { tint: BlurTint; intensity: number }[] = [{ tint: "light", intensity: 60 }, { tint: "dark", intensity: 80 }, { tint: "systemMaterial", intensity: 100 }]; +const stripes = ["#ef4444", "#f59e0b", "#10b981", "#3b82f6", "#a855f7"]; + +/** expo-blur through its own API; Spark's patch adds AppKit materials. */ +export function ExpoBlurDemo() { + const [laidOut, setLaidOut] = useState<string[]>([]); + const native = requireOptionalNativeModule("ExpoBlur") != null; + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-4 p-6"> + <Text className="text-sm text-muted" testID={native ? `${ID}-native` : undefined}>{`Native module linked: ${native ? "yes" : "no"}`}</Text> + <View style={{ flexDirection: "row", gap: 16 }}> + {tints.map(({ tint, intensity }) => <View key={tint} style={{ width: 200, height: 120, borderRadius: 12, overflow: "hidden" }}> + <View style={{ position: "absolute", inset: 0, flexDirection: "row" }}>{stripes.map(color => <View key={color} style={{ flex: 1, backgroundColor: color }} />)}</View> + <BlurView tint={tint} intensity={intensity} style={{ position: "absolute", inset: 20 }} + onLayout={event => event.nativeEvent.layout.width > 0 && setLaidOut(previous => previous.includes(tint) ? previous : [...previous, tint])} /> + <Text testID={laidOut.includes(tint) ? `${ID}-${tint}-laid-out` : undefined} style={{ margin: 28, color: "white", fontWeight: "600" }}>{`${tint} ${intensity}`}</Text> + </View>)} + </View> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/displays-graphics/ExpoGlassDemo.tsx b/examples/kitchen-sink/screens/displays-graphics/ExpoGlassDemo.tsx new file mode 100644 index 000000000..ccdf4eb24 --- /dev/null +++ b/examples/kitchen-sink/screens/displays-graphics/ExpoGlassDemo.tsx @@ -0,0 +1,37 @@ +import { useState } from "react"; +import { ScrollView, Text, View } from "react-native"; +import { requireOptionalNativeModule } from "expo-modules-core"; +import { GlassContainer, GlassView, isGlassEffectAPIAvailable, isLiquidGlassAvailable } from "expo-glass-effect"; + +// Test IDs: displays-graphics-expo-glass-native when the native module is linked; +// displays-graphics-expo-glass-available-<yes|no> for isGlassEffectAPIAvailable (yes on macOS 26); +// displays-graphics-expo-glass-<name>-child once a glass view's child has laid out. +const ID = "displays-graphics-expo-glass"; +const stripes = ["#ef4444", "#f59e0b", "#10b981", "#3b82f6", "#a855f7"]; +const backdrop = <View style={{ position: "absolute", inset: 0, flexDirection: "row" }}>{stripes.map(color => <View key={color} style={{ flex: 1, backgroundColor: color }} />)}</View>; + +/** expo-glass-effect through its own API; Spark's patch hosts it in AppKit's NSGlassEffectView. */ +export function ExpoGlassDemo() { + const [laidOut, setLaidOut] = useState<string[]>([]); + const native = requireOptionalNativeModule("ExpoGlassEffect") != null; + const available = native && isGlassEffectAPIAvailable(); + const child = (name: string) => <Text testID={laidOut.includes(name) ? `${ID}-${name}-child` : undefined} style={{ fontWeight: "600" }} + onLayout={() => setLaidOut(previous => previous.includes(name) ? previous : [...previous, name])}>{name}</Text>; + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-4 p-6"> + <Text className="text-sm text-muted" testID={native ? `${ID}-native` : undefined}>{`Native module linked: ${native ? "yes" : "no"}`}</Text> + <Text className="text-sm text-muted" testID={`${ID}-available-${available ? "yes" : "no"}`}>{`Glass API available: ${available ? "yes" : "no"} · Liquid Glass: ${native && isLiquidGlassAvailable() ? "yes" : "no"}`}</Text> + <View style={{ width: 640, height: 160, borderRadius: 12, overflow: "hidden", flexDirection: "row", alignItems: "center", justifyContent: "space-around" }}> + {backdrop} + <GlassView style={{ width: 160, height: 100, borderRadius: 20, alignItems: "center", justifyContent: "center" }}>{child("regular")}</GlassView> + <GlassView glassEffectStyle="clear" style={{ width: 160, height: 100, borderRadius: 20, alignItems: "center", justifyContent: "center" }}>{child("clear")}</GlassView> + <GlassView tintColor="#3b82f680" style={{ width: 160, height: 100, borderRadius: 50, alignItems: "center", justifyContent: "center" }}>{child("tinted")}</GlassView> + </View> + <View style={{ width: 640, height: 120, borderRadius: 12, overflow: "hidden", justifyContent: "center" }}> + {backdrop} + <GlassContainer spacing={24} style={{ flexDirection: "row", gap: 12, padding: 12 }}> + <GlassView style={{ width: 100, height: 80, borderRadius: 40, alignItems: "center", justifyContent: "center" }}>{child("merged-a")}</GlassView> + <GlassView style={{ width: 100, height: 80, borderRadius: 40, alignItems: "center", justifyContent: "center" }}>{child("merged-b")}</GlassView> + </GlassContainer> + </View> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/displays-graphics/ExpoImageDemo.tsx b/examples/kitchen-sink/screens/displays-graphics/ExpoImageDemo.tsx new file mode 100644 index 000000000..15915dc51 --- /dev/null +++ b/examples/kitchen-sink/screens/displays-graphics/ExpoImageDemo.tsx @@ -0,0 +1,60 @@ +import { useEffect, useRef, useState } from "react"; +import { ScrollView, Text, View } from "react-native"; +import { Image } from "expo-image"; +import { toByteArray } from "base64-js"; +import { getDirectory, writeBytes } from "@legendapp/spark/files"; + +// Test IDs: displays-graphics-expo-image-passed, or -failed with the failing cases. Offline: the image +// is a 4×2 PNG written to the app's data folder. +const ID = "displays-graphics-expo-image"; +const PNG = "iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAYAAAB/qH1jAAAAEklEQVR4nGP4z8DwHxkzoAsAAA8hD/EEN8afAAAAAElFTkSuQmCC"; + +/** expo-image through its own API; on macOS Spark's stand-in renders it with React Native's Image. */ +export function ExpoImageDemo() { + const [uri, setUri] = useState<string>(); + const [result, setResult] = useState<string>(); + const [unsupported, setUnsupported] = useState(""); + const [assetLoaded, setAssetLoaded] = useState(false); + const unsupportedEvents = useRef<string[]>([]); + const image = useRef<Image>(null); + const loads = useRef<string[]>([]); + useEffect(() => { + void (async () => { + const file = `${await getDirectory("data")}/expo-image-demo.png`; + await writeBytes(file, toByteArray(PNG)); + setUri(`file://${file}`); + })().catch(error => setResult(`failed: ${String(error)}`)); + }, []); + async function onLoad(width: number, height: number) { + loads.current.push(`${width}x${height}`); + if (loads.current.length === 1) { await image.current?.reloadAsync(); return; } + const prefetched = await Image.prefetch(uri!); + const failures = [ + loads.current.join() !== "4x2,4x2" && `onLoad (${loads.current.join()})`, + !prefetched && "prefetch", + ].filter(Boolean); + setResult(failures.length ? `failed: ${failures.join(", ")}` : "passed"); + } + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text className="text-sm text-muted">Loads a local PNG, reloads it through the ref, then prefetches it; the tinted copy uses contentFit contain.</Text> + <View style={{ flexDirection: "row", gap: 16 }}> + {uri && <Image ref={image} source={{ uri, width: 800, height: 600 }} contentFit="cover" style={{ width: 160, height: 80, borderRadius: 8 }} + onLoad={event => void onLoad(event.source.width, event.source.height)} + onError={event => setResult(`failed: onError ${event.error}`)} />} + {uri && <Image source={uri} contentFit="contain" tintColor="#3b82f6" style={{ width: 160, height: 160, borderWidth: 1, borderColor: "#888" }} />} + </View> + <Text className="text-sm text-muted">Bundled image in a 120×60 box; none crops its natural 160×80 size in an 80×40 box. The full image has green/red/blue/yellow bands; the crop shows only red/blue.</Text> + <View style={{ flexDirection: "row", gap: 16 }}> + <Image source={require("./image-scale.png")} contentFit="contain" style={{ width: 120, height: 60 }} + onLoad={event => setAssetLoaded(event.source.width === 160 && event.source.height === 80)} /> + <Image source={require("./image-scale.png")} contentFit="none" style={{ width: 80, height: 40, borderWidth: 1, borderColor: "#888" }} /> + <Image source={{ blurhash: "LEHV6nWB2yk8pyo0adR*.7kCMdnj" }} style={{ width: 1, height: 1 }} + onLoadStart={() => unsupportedEvents.current.push("start")} + onError={event => unsupportedEvents.current.push(event.error === "expo-image cannot draw blurhash, thumbhash, SF Symbol or ImageRef sources on macOS" ? "error" : event.error)} + onLoadEnd={() => { unsupportedEvents.current.push("end"); setUnsupported(unsupportedEvents.current.join(",")); }} /> + </View> + <Text className="text-xs text-foreground" testID={assetLoaded ? `${ID}-asset-loaded` : undefined}>Bundled image: {assetLoaded ? "loaded at 160×80" : "loading"}</Text> + <Text className="text-xs text-foreground" testID={unsupported === "start,error,end" ? `${ID}-unsupported-passed` : `${ID}-unsupported-failed`}>Unsupported source: {unsupported}</Text> + <Text selectable className="font-mono text-xs text-foreground" testID={result ? `${ID}-${result === "passed" ? "passed" : "failed"}` : undefined}>{result ?? "running"}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/displays-graphics/FastImageDemo.tsx b/examples/kitchen-sink/screens/displays-graphics/FastImageDemo.tsx new file mode 100644 index 000000000..59800d534 --- /dev/null +++ b/examples/kitchen-sink/screens/displays-graphics/FastImageDemo.tsx @@ -0,0 +1,63 @@ +import { useEffect, useRef, useState } from "react"; +import { ScrollView, Text, View } from "react-native"; +import FastImage, { FastImageBackground, type FastImageRef } from "react-native-fast-image"; +import { toByteArray } from "base64-js"; +import { getDirectory, writeBytes } from "@legendapp/spark/files"; + +// Test IDs: displays-graphics-fast-image-passed, or -failed with the failing cases. Offline: the image +// is a 4×2 PNG written to the app's data folder. +const ID = "displays-graphics-fast-image"; +const PNG = "iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAYAAAB/qH1jAAAAEklEQVR4nGP4z8DwHxkzoAsAAA8hD/EEN8afAAAAAElFTkSuQmCC"; + +/** react-native-fast-image through its own API; on macOS Spark's stand-in renders it with Image. */ +export function FastImageDemo() { + const [uri, setUri] = useState<string>(); + const [result, setResult] = useState<string>(); + const [backgroundLayout, setBackgroundLayout] = useState(false); + const [assetLoaded, setAssetLoaded] = useState(false); + const [placeholderReady, setPlaceholderReady] = useState(false); + const image = useRef<FastImageRef>(null); + const loads = useRef<string[]>([]); + useEffect(() => { + void (async () => { + const file = `${await getDirectory("data")}/fast-image-demo.png`; + await writeBytes(file, toByteArray(PNG)); + setUri(`file://${file}`); + })().catch(error => setResult(`failed: ${String(error)}`)); + }, []); + async function onLoad(width: number, height: number) { + loads.current.push(`${width}x${height}`); + if (loads.current.length === 1) { image.current?.reload(); return; } + const [preloaded] = await FastImage.preload([{ uri: uri! }]); + const configured = await FastImage.configureCache() as { ok?: boolean; error?: string }; + const failures = [ + !(configured.ok === false && configured.error === "FastImage.configureCache is not supported on macOS") && "configureCache", + loads.current.join() !== "4x2,4x2" && `onLoad (${loads.current.join()})`, + !(preloaded?.ok && preloaded.width === 4 && preloaded.height === 2) && `preload (${JSON.stringify(preloaded)})`, + FastImage.priority.high !== "high" && "constants", + ].filter(Boolean); + setResult(failures.length ? `failed: ${failures.join(", ")}` : "passed"); + } + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text className="text-sm text-muted">Loads a local PNG, reloads it through the ref, then preloads it; the tinted copy uses objectFit contain.</Text> + <View style={{ flexDirection: "row", gap: 16 }}> + {uri && <FastImage ref={image} source={{ uri, width: 800, height: 600 }} objectFit="cover" style={{ width: 160, height: 80, borderRadius: 8 }} + onLoad={event => void onLoad(event.nativeEvent.width, event.nativeEvent.height)} + onError={event => setResult(`failed: onError ${event.nativeEvent.error}`)} />} + {uri && <FastImage source={{ uri }} objectFit="contain" tintColor="#3b82f6" style={{ width: 160, height: 160, borderWidth: 1, borderColor: "#888" }} />} + </View> + <Text className="text-sm text-muted">Bundled background fills 120×60; the object placeholder stays visible when its source fails.</Text> + <View style={{ flexDirection: "row", gap: 16 }}> + <FastImageBackground source={require("./image-scale.png")} objectFit="contain" style={{ width: 120, height: 60 }} testID={`${ID}-background`} + onLayout={event => setBackgroundLayout(event.nativeEvent.layout.width === 120 && event.nativeEvent.layout.height === 60)} + onLoad={event => setAssetLoaded(event.nativeEvent.width === 160 && event.nativeEvent.height === 80)}> + <Text className="text-xs text-white">Background</Text> + </FastImageBackground> + {uri && <FastImage source={{ uri: `${uri}.missing` }} placeholder={{ uri }} style={{ width: 160, height: 80 }} + onError={() => setPlaceholderReady(true)} />} + </View> + <Text className="text-xs text-foreground" testID={backgroundLayout && assetLoaded ? `${ID}-background-passed` : undefined}>Background: {backgroundLayout && assetLoaded ? "laid out and loaded" : "loading"}</Text> + <Text className="text-xs text-foreground" testID={placeholderReady ? `${ID}-placeholder-ready` : undefined}>Object placeholder: {placeholderReady ? "source failed; placeholder remains" : "loading"}</Text> + <Text selectable className="font-mono text-xs text-foreground" testID={result ? `${ID}-${result === "passed" ? "passed" : "failed"}` : undefined}>{result ?? "running"}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/displays-graphics/GraphDemo.tsx b/examples/kitchen-sink/screens/displays-graphics/GraphDemo.tsx new file mode 100644 index 000000000..930f1410e --- /dev/null +++ b/examples/kitchen-sink/screens/displays-graphics/GraphDemo.tsx @@ -0,0 +1,23 @@ +import { useState } from "react"; +import { ScrollView, Text, View } from "react-native"; +import { GestureHandlerRootView } from "react-native-gesture-handler"; +import { LineGraph, type GraphPoint } from "react-native-graph"; + +// Test IDs: displays-graphics-graph-<static|animated>-laid-out once each graph has rendered and laid out. +// Upstream builds paths with Skia 1's mutable SkPath and threw on Skia 2; the patch uses PathBuilder. +const ID = "displays-graphics-graph"; +const points: GraphPoint[] = Array.from({ length: 40 }, (_, i) => ({ date: new Date(Date.UTC(2026, 0, i + 1)), value: 50 + 30 * Math.sin(i / 4) + i })); + +/** react-native-graph through its own API, drawn with Skia. */ +export function GraphDemo() { + const [laidOut, setLaidOut] = useState<string[]>([]); + const graph = (name: string, element: React.ReactNode) => <View key={name} style={{ width: 480, height: 160 }} + testID={laidOut.includes(name) ? `${ID}-${name}-laid-out` : undefined} + onLayout={() => setLaidOut(previous => previous.includes(name) ? previous : [...previous, name])}>{element}</View>; + // The animated graph wraps itself in a GestureDetector, which needs a root view. + return <GestureHandlerRootView style={{ flex: 1 }}><ScrollView className="flex-1 bg-background" contentContainerClassName="gap-4 p-6"> + <Text className="text-sm text-muted">A static line graph and an animated one with a gradient fill, over 40 points.</Text> + {graph("static", <LineGraph points={points} animated={false} color="#3b82f6" style={{ flex: 1 }} />)} + {graph("animated", <LineGraph points={points} animated color="#10b981" gradientFillColors={["#10b98166", "#10b98100"]} enablePanGesture style={{ flex: 1 }} />)} + </ScrollView></GestureHandlerRootView>; +} diff --git a/examples/kitchen-sink/screens/displays-graphics/LinearGradientDemo.tsx b/examples/kitchen-sink/screens/displays-graphics/LinearGradientDemo.tsx new file mode 100644 index 000000000..5b4a71557 --- /dev/null +++ b/examples/kitchen-sink/screens/displays-graphics/LinearGradientDemo.tsx @@ -0,0 +1,20 @@ +import { useState } from "react"; +import { ScrollView, Text } from "react-native"; +import { requireOptionalNativeModule } from "expo-modules-core"; +import { LinearGradient } from "expo-linear-gradient"; + +// Test IDs: displays-graphics-linear-gradient-native when the native module is linked; +// displays-graphics-linear-gradient-laid-out once the gradient view has a size. +const ID = "displays-graphics-linear-gradient"; + +/** expo-linear-gradient through its own API; it supports macOS upstream (CAGradientLayer). */ +export function LinearGradientDemo() { + const [laidOut, setLaidOut] = useState(false); + const native = requireOptionalNativeModule("ExpoLinearGradient") != null; + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text className="text-sm text-muted" testID={native ? `${ID}-native` : undefined}>{`Native module linked: ${native ? "yes" : "no"}`}</Text> + <LinearGradient colors={["#ef4444", "#f59e0b", "#3b82f6"]} locations={[0, 0.4, 1]} start={{ x: 0, y: 0 }} end={{ x: 1, y: 1 }} + style={{ width: 320, height: 160, borderRadius: 12 }} onLayout={event => setLaidOut(event.nativeEvent.layout.width > 0)} /> + <Text className="font-mono text-xs text-foreground" testID={laidOut ? `${ID}-laid-out` : undefined}>{laidOut ? "laid out" : "…"}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/displays-graphics/MaskedViewDemo.tsx b/examples/kitchen-sink/screens/displays-graphics/MaskedViewDemo.tsx new file mode 100644 index 000000000..635526bc9 --- /dev/null +++ b/examples/kitchen-sink/screens/displays-graphics/MaskedViewDemo.tsx @@ -0,0 +1,26 @@ +import { useState } from "react"; +import { ScrollView, Text, UIManager, View } from "react-native"; +import MaskedView from "@react-native-masked-view/masked-view"; + +// Test IDs: displays-graphics-masked-view-registered when the native view manager is registered; +// displays-graphics-masked-view-laid-out once the masked view has a native frame. +const ID = "displays-graphics-masked-view"; +const stripes = ["#ef4444", "#f59e0b", "#10b981", "#3b82f6", "#a855f7"]; + +/** @react-native-masked-view/masked-view through its own API; Spark's patch adds the AppKit view. */ +export function MaskedViewDemo() { + const [laidOut, setLaidOut] = useState(false); + const registered = UIManager.hasViewManagerConfig("RNCMaskedView"); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-4 p-6"> + <Text className="text-sm text-muted" testID={registered ? `${ID}-registered` : undefined}>{`Native RNCMaskedView registered: ${registered ? "yes" : "no"}`}</Text> + <Text className="text-sm text-muted">Colored stripes show only through the text and the half-transparent circle.</Text> + <MaskedView testID={laidOut ? `${ID}-laid-out` : undefined} style={{ width: 480, height: 160 }} + onLayout={event => setLaidOut(event.nativeEvent.layout.width > 0)} + maskElement={<View style={{ flex: 1, flexDirection: "row", alignItems: "center", gap: 24 }}> + <Text style={{ fontSize: 72, fontWeight: "900", color: "black" }}>MASK</Text> + <View style={{ width: 120, height: 120, borderRadius: 60, backgroundColor: "rgba(0, 0, 0, 0.5)" }} /> + </View>}> + <View style={{ flex: 1, flexDirection: "row" }}>{stripes.map(color => <View key={color} style={{ flex: 1, backgroundColor: color }} />)}</View> + </MaskedView> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/displays-graphics/ScreenCornerRadiusDemo.tsx b/examples/kitchen-sink/screens/displays-graphics/ScreenCornerRadiusDemo.tsx new file mode 100644 index 000000000..be79b623c --- /dev/null +++ b/examples/kitchen-sink/screens/displays-graphics/ScreenCornerRadiusDemo.tsx @@ -0,0 +1,13 @@ +import { ScrollView, Text } from "react-native"; +import { IsScreenRounded, ScreenCornerRadius } from "react-native-screen-corner-radius"; + +// Test ID: displays-graphics-screen-corner-radius-<radius>-<rounded>, e.g. -0-false on a Mac. +const ID = "displays-graphics-screen-corner-radius"; + +/** react-native-screen-corner-radius's constants; its native module is iOS only. */ +export function ScreenCornerRadiusDemo() { + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text className="text-sm text-muted">The display's corner radius, for matching rounded screen corners on iPhone. Desktop displays report 0.</Text> + <Text selectable className="font-mono text-xs text-foreground" testID={`${ID}-${ScreenCornerRadius}-${IsScreenRounded}`}>{`radius ${ScreenCornerRadius}, rounded ${IsScreenRounded}`}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/displays-graphics/SkiaDemo.tsx b/examples/kitchen-sink/screens/displays-graphics/SkiaDemo.tsx new file mode 100644 index 000000000..155140a07 --- /dev/null +++ b/examples/kitchen-sink/screens/displays-graphics/SkiaDemo.tsx @@ -0,0 +1,54 @@ +import { useEffect, useState } from "react"; +import { ScrollView, Text } from "react-native"; +import { Canvas, Circle, Rect, Skia, useCanvasRef } from "@shopify/react-native-skia"; + +// Test IDs: displays-graphics-skia-passed, or -failed with the failing cases. +const ID = "displays-graphics-skia"; + +function pixel(image: ReturnType<typeof Skia.Image.MakeImageFromEncoded>, x: number, y: number) { + const pixels = image?.readPixels(x, y, { width: 1, height: 1, colorType: 4, alphaType: 1 }); + return pixels ? Array.from(pixels).slice(0, 3).join() : "none"; +} + +/** @shopify/react-native-skia through its own API; it builds for macOS upstream. */ +export function SkiaDemo() { + const canvas = useCanvasRef(); + const [result, setResult] = useState<string>(); + // Snapshotting a Canvas before AppKit has inserted its view dereferences a null native view, so + // the check waits for the Canvas to lay out. + const [laidOut, setLaidOut] = useState(false); + useEffect(() => { + if (!laidOut) return; + const timer = setTimeout(async () => { + try { + // Offscreen raster: a red square with a blue circle. + const surface = Skia.Surface.Make(64, 64)!; + const paint = Skia.Paint(); + paint.setColor(Skia.Color("red")); + surface.getCanvas().drawRect(Skia.XYWHRect(0, 0, 64, 64), paint); + paint.setColor(Skia.Color("blue")); + surface.getCanvas().drawCircle(32, 32, 16, paint); + surface.flush(); + const raster = surface.makeImageSnapshot(); + // On-screen Canvas, read back from the native view. + const shown = await canvas.current?.makeImageSnapshotAsync(); + const failures = [ + pixel(raster, 2, 2) !== "255,0,0" && `raster corner (${pixel(raster, 2, 2)})`, + pixel(raster, 32, 32) !== "0,0,255" && `raster circle (${pixel(raster, 32, 32)})`, + !shown && "canvas snapshot", + shown && pixel(shown, Math.floor(shown.width() / 2), Math.floor(shown.height() / 2)) !== "0,128,0" && `canvas center (${pixel(shown, Math.floor(shown.width() / 2), Math.floor(shown.height() / 2))})`, + ].filter(Boolean); + setResult(failures.length ? `failed: ${failures.join(", ")}` : "passed"); + } catch (error) { setResult(`failed: ${String(error)}`); } + }, 100); + return () => clearTimeout(timer); + }, [canvas, laidOut]); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text className="text-sm text-muted">A Skia Canvas (green circle on yellow) and an offscreen raster surface, both read back by pixel.</Text> + <Canvas ref={canvas} style={{ width: 160, height: 160 }} onLayout={() => setLaidOut(true)}> + <Rect x={0} y={0} width={160} height={160} color="yellow" /> + <Circle cx={80} cy={80} r={50} color="green" /> + </Canvas> + <Text selectable className="font-mono text-xs text-foreground" testID={result ? `${ID}-${result === "passed" ? "passed" : "failed"}` : undefined}>{result ?? "running"}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/displays-graphics/SvgDemo.tsx b/examples/kitchen-sink/screens/displays-graphics/SvgDemo.tsx new file mode 100644 index 000000000..90a23a567 --- /dev/null +++ b/examples/kitchen-sink/screens/displays-graphics/SvgDemo.tsx @@ -0,0 +1,26 @@ +import { useState } from "react"; +import { ScrollView, Text, UIManager, View } from "react-native"; +import Svg, { Circle, Defs, LinearGradient, Path, Rect, Stop, Text as SvgText } from "react-native-svg"; + +// Test IDs: displays-graphics-svg-native when the native SVG components are registered; +// displays-graphics-svg-laid-out once the canvas has a size. +const ID = "displays-graphics-svg"; + +/** react-native-svg through its own API; it supports macOS upstream. */ +export function SvgDemo() { + const [laidOut, setLaidOut] = useState(false); + const native = ["RNSVGSvgView", "RNSVGPath", "RNSVGText"].every(name => UIManager.hasViewManagerConfig(name)); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text className="text-sm text-muted" testID={native ? `${ID}-native` : undefined}>{`Native SVG components registered: ${native ? "yes" : "no"}`}</Text> + <View onLayout={event => setLaidOut(event.nativeEvent.layout.width > 0)}><Svg width={320} height={160} viewBox="0 0 320 160"> + <Defs> + <LinearGradient id="sky" x1="0" y1="0" x2="1" y2="1"><Stop offset="0" stopColor="#3b82f6" /><Stop offset="1" stopColor="#a855f7" /></LinearGradient> + </Defs> + <Rect x={0} y={0} width={320} height={160} rx={16} fill="url(#sky)" /> + <Circle cx={60} cy={80} r={36} fill="#f59e0b" /> + <Path d="M120 120 L170 40 L220 120 Z" fill="#10b981" stroke="white" strokeWidth={3} /> + <SvgText x={240} y={88} fill="white" fontSize={18} fontWeight="bold">SVG</SvgText> + </Svg></View> + <Text className="font-mono text-xs text-foreground" testID={laidOut ? `${ID}-laid-out` : undefined}>{laidOut ? "laid out" : "…"}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/displays-graphics/SymbolsDemo.tsx b/examples/kitchen-sink/screens/displays-graphics/SymbolsDemo.tsx new file mode 100644 index 000000000..5bc31e24a --- /dev/null +++ b/examples/kitchen-sink/screens/displays-graphics/SymbolsDemo.tsx @@ -0,0 +1,33 @@ +import { useState } from "react"; +import { ScrollView, Text, View } from "react-native"; +import { requireOptionalNativeModule } from "expo-modules-core"; +import { SymbolView, type SymbolViewProps } from "expo-symbols"; + +// Test IDs: displays-graphics-symbols-native when the native module is linked; +// displays-graphics-symbols-laid-out once every symbol view has a native frame; displays-graphics-symbols-pulse +// wraps the animated symbol, which the golden masks. +const ID = "displays-graphics-symbols"; +const symbols: (Omit<SymbolViewProps, "name"> & { name: SymbolViewProps["name"]; label: string })[] = [ + { label: "monochrome", name: "star.fill", tintColor: "#f59e0b" }, + { label: "hierarchical", name: "folder.fill.badge.plus", type: "hierarchical", tintColor: "#3b82f6" }, + { label: "palette", name: "person.crop.circle.badge.checkmark", type: "palette", colors: ["#10b981", "#ef4444"] }, + { label: "multicolor", name: "cloud.sun.rain.fill", type: "multicolor" }, + { label: "bold", name: "heart", weight: "bold", tintColor: "#ec4899" }, + { label: "pulse", name: "wifi", tintColor: "#a855f7", animationSpec: { effect: { type: "pulse" }, repeating: true } }, +]; + +/** expo-symbols through its own API; Spark's patch renders SF Symbols with AppKit. */ +export function SymbolsDemo() { + const [laidOut, setLaidOut] = useState(0); + const native = requireOptionalNativeModule("SymbolModule") != null; + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-4 p-6"> + <Text className="text-sm text-muted" testID={native ? `${ID}-native` : undefined}>{`Native module linked: ${native ? "yes" : "no"}`}</Text> + <View style={{ flexDirection: "row", flexWrap: "wrap", gap: 24 }} testID={laidOut >= symbols.length ? `${ID}-laid-out` : undefined}> + {symbols.map(({ label, ...props }) => <View key={label} testID={props.animationSpec ? `${ID}-pulse` : undefined} style={{ alignItems: "center", gap: 8, width: 96 }}> + <SymbolView {...props} size={48} fallback={<Text className="text-foreground">missing</Text>} + onLayout={event => event.nativeEvent.layout.width > 0 && setLaidOut(count => count + 1)} /> + <Text className="text-xs text-muted">{label}</Text> + </View>)} + </View> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/displays-graphics/VectorIconsDemo.tsx b/examples/kitchen-sink/screens/displays-graphics/VectorIconsDemo.tsx new file mode 100644 index 000000000..f13509108 --- /dev/null +++ b/examples/kitchen-sink/screens/displays-graphics/VectorIconsDemo.tsx @@ -0,0 +1,24 @@ +import { useEffect, useState } from "react"; +import { ScrollView, Text, View } from "react-native"; +import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; +import * as Font from "expo-font"; + +// Test ID: displays-graphics-vector-icons-loaded once both icon fonts are registered with expo-font. +const ID = "displays-graphics-vector-icons"; + +/** @expo/vector-icons through its own API; fonts load with expo-font, which supports macOS. */ +export function VectorIconsDemo() { + const [loaded, setLoaded] = useState<string>(); + useEffect(() => { + Font.loadAsync({ ...Ionicons.font, ...MaterialCommunityIcons.font }) + .then(() => setLoaded(Font.isLoaded("ionicons") && Font.isLoaded("material-community") ? "loaded" : "not registered"), error => setLoaded(`failed: ${String(error)}`)); + }, []); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <View style={{ flexDirection: "row", gap: 16 }}> + <Ionicons name="heart" size={48} color="#ef4444" /> + <Ionicons name="logo-apple" size={48} color="#e5e5e5" /> + <MaterialCommunityIcons name="rocket-launch" size={48} color="#38bdf8" /> + </View> + <Text className="font-mono text-xs text-foreground" testID={loaded === "loaded" ? `${ID}-loaded` : undefined}>{loaded ?? "loading"}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/displays-graphics/image-scale.png b/examples/kitchen-sink/screens/displays-graphics/image-scale.png new file mode 100644 index 000000000..dde66f8fa Binary files /dev/null and b/examples/kitchen-sink/screens/displays-graphics/image-scale.png differ diff --git a/examples/kitchen-sink/screens/displays-graphics/index.tsx b/examples/kitchen-sink/screens/displays-graphics/index.tsx new file mode 100644 index 000000000..2478b9542 --- /dev/null +++ b/examples/kitchen-sink/screens/displays-graphics/index.tsx @@ -0,0 +1,18 @@ +import { defineScreens, lazyScreen } from "../../shell/registry"; + +export default defineScreens("displays-graphics", [ + { id: "blur", title: "Blur", titles: { ar: "التمويه" }, summary: "@react-native-community/blur on desktop: BlurView and VibrancyView as native NSVisualEffectView materials over a colorful backdrop, in light, dark and adaptive styles.", summaries: { ar: "‏@react-native-community/blur على سطح المكتب: ‏BlurView وVibrancyView كمواد NSVisualEffectView أصلية فوق خلفية ملونة، بأنماط فاتحة وداكنة ومتكيفة." }, component: lazyScreen(() => require("./BlurDemo").BlurDemo) }, + { id: "fast-image", title: "FastImage", titles: { ar: "صور FastImage" }, summary: "react-native-fast-image on desktop: React Native's Image stands in for its UIKit view, with load events, objectFit, tint, preload and reload.", summaries: { ar: "‏react-native-fast-image على سطح المكتب: يحل Image من React Native محل عرض UIKit الخاص بها، مع أحداث التحميل وobjectFit والتلوين والتحميل المسبق وإعادة التحميل." }, component: lazyScreen(() => require("./FastImageDemo").FastImageDemo) }, + { id: "skia", title: "Skia", titles: { ar: "رسومات Skia" }, summary: "@shopify/react-native-skia on desktop: a declarative Canvas drawn by Skia's Metal backend, read back pixel by pixel, and an offscreen raster surface.", summaries: { ar: "‏@shopify/react-native-skia على سطح المكتب: ‏Canvas تصريحية ترسمها واجهة Metal في Skia وتُقرأ بكسلًا بكسلًا، وسطح نقطي خارج الشاشة." }, component: lazyScreen(() => require("./SkiaDemo").SkiaDemo) }, + { id: "blurhash", title: "Blurhash", titles: { ar: "صور Blurhash" }, summary: "react-native-blurhash on desktop: a native AppKit view decoding a Blurhash placeholder, and encoding a local image to a Blurhash string.", summaries: { ar: "‏react-native-blurhash على سطح المكتب: عرض AppKit أصلي يفك ترميز صورة Blurhash مؤقتة، وترميز صورة محلية إلى نص Blurhash." }, component: lazyScreen(() => require("./BlurhashDemo").BlurhashDemo) }, + { id: "screen-corner-radius", title: "Screen corner radius", titles: { ar: "نصف قطر زوايا الشاشة" }, summary: "react-native-screen-corner-radius on desktop: it imports without its iOS-only native module and reports square corners.", summaries: { ar: "‏react-native-screen-corner-radius على سطح المكتب: يُستورد دون وحدته الأصلية الخاصة بـ iOS ويُبلغ عن زوايا مربعة." }, component: lazyScreen(() => require("./ScreenCornerRadiusDemo").ScreenCornerRadiusDemo) }, + { id: "linear-gradient", title: "Linear gradient", titles: { ar: "تدرج خطي" }, summary: "expo-linear-gradient on desktop: a native gradient view with colors, locations and start and end points.", summaries: { ar: "‏expo-linear-gradient على سطح المكتب: عرض تدرج أصلي بألوان ومواضع ونقطتي بداية ونهاية." }, component: lazyScreen(() => require("./LinearGradientDemo").LinearGradientDemo) }, + { id: "svg", title: "SVG", titles: { ar: "رسومات SVG" }, summary: "react-native-svg on desktop: shapes, paths, gradients and text drawn by its native AppKit views.", summaries: { ar: "‏react-native-svg على سطح المكتب: أشكال ومسارات وتدرجات ونصوص ترسمها عروض AppKit الأصلية." }, component: lazyScreen(() => require("./SvgDemo").SvgDemo) }, + { id: "vector-icons", title: "Vector icons", titles: { ar: "أيقونات متجهة" }, summary: "@expo/vector-icons on desktop: icon fonts loaded through expo-font and drawn as text glyphs.", summaries: { ar: "‏@expo/vector-icons على سطح المكتب: خطوط أيقونات تُحمَّل عبر expo-font وتُرسم كحروف نصية." }, component: lazyScreen(() => require("./VectorIconsDemo").VectorIconsDemo) }, + { id: "expo-blur", title: "Expo Blur", titles: { ar: "تمويه Expo" }, summary: "expo-blur on desktop: BlurView tints and intensities as AppKit visual-effect materials over a colorful backdrop.", summaries: { ar: "‏expo-blur على سطح المكتب: ألوان BlurView وشدّاته كمواد تأثيرات مرئية في AppKit فوق خلفية ملونة." }, component: lazyScreen(() => require("./ExpoBlurDemo").ExpoBlurDemo) }, + { id: "expo-image", title: "Expo Image", titles: { ar: "صور Expo" }, summary: "expo-image on desktop: React Native's Image stands in for its UIKit view, with sources, contentFit, tint, load events, reload and prefetch.", summaries: { ar: "‏expo-image على سطح المكتب: يحل Image من React Native محل عرض UIKit الخاص به، مع المصادر وcontentFit والتلوين وأحداث التحميل وإعادة التحميل والجلب المسبق." }, component: lazyScreen(() => require("./ExpoImageDemo").ExpoImageDemo) }, + { id: "symbols", title: "SF Symbols", titles: { ar: "رموز SF" }, summary: "expo-symbols on desktop: SF Symbols drawn by AppKit in monochrome, hierarchical, palette and multicolor styles, with weights and effects.", summaries: { ar: "‏expo-symbols على سطح المكتب: رموز SF يرسمها AppKit بأنماط أحادية اللون وهرمية ولوحة ألوان ومتعددة الألوان، مع الأوزان والتأثيرات." }, component: lazyScreen(() => require("./SymbolsDemo").SymbolsDemo) }, + { id: "masked-view", title: "Masked view", titles: { ar: "عرض مقنّع" }, summary: "@react-native-masked-view/masked-view on desktop: content shows only through the opaque parts of a live mask element.", summaries: { ar: "‏@react-native-masked-view/masked-view على سطح المكتب: يظهر المحتوى فقط عبر الأجزاء المعتمة من عنصر قناع حي." }, component: lazyScreen(() => require("./MaskedViewDemo").MaskedViewDemo) }, + { id: "expo-glass-effect", title: "Expo Glass", titles: { ar: "زجاج Expo" }, summary: "expo-glass-effect on desktop: GlassView and GlassContainer hosted in AppKit's Liquid Glass views, with regular, clear and tinted styles.", summaries: { ar: "‏expo-glass-effect على سطح المكتب: ‏GlassView وGlassContainer داخل عروض الزجاج السائل في AppKit، بأنماط عادية وشفافة وملونة." }, component: lazyScreen(() => require("./ExpoGlassDemo").ExpoGlassDemo) }, + { id: "graph", title: "Graph", titles: { ar: "رسم بياني" }, summary: "react-native-graph on desktop: static and animated line graphs drawn with Skia.", summaries: { ar: "‏react-native-graph على سطح المكتب: رسوم بيانية خطية ثابتة ومتحركة يرسمها Skia." }, component: lazyScreen(() => require("./GraphDemo").GraphDemo) }, +]); diff --git a/examples/kitchen-sink/screens/internationalization/ExpoLocalizationDemo.tsx b/examples/kitchen-sink/screens/internationalization/ExpoLocalizationDemo.tsx new file mode 100644 index 000000000..c8f1fb90d --- /dev/null +++ b/examples/kitchen-sink/screens/internationalization/ExpoLocalizationDemo.tsx @@ -0,0 +1,17 @@ +import { ScrollView, Text } from "react-native"; +import { getCalendars, getLocales } from "expo-localization"; + +// Test ID: internationalization-expo-localization-ready when the native module returns a locale with a +// language tag and a calendar with a time zone. +const ID = "internationalization-expo-localization"; + +/** expo-localization through its own API; Spark's patch adds the macOS build. */ +export function ExpoLocalizationDemo() { + const [locale] = getLocales(); + const [calendar] = getCalendars(); + const ready = !!locale?.languageTag && !!calendar?.timeZone; + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text selectable className="font-mono text-xs text-muted">{JSON.stringify({ locale, calendar }, null, 2)}</Text> + <Text className="font-mono text-xs text-foreground" testID={ready ? `${ID}-ready` : undefined}>{ready ? `${locale!.languageTag}, ${calendar!.timeZone}` : "missing locale or calendar"}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/internationalization/index.tsx b/examples/kitchen-sink/screens/internationalization/index.tsx index 82143bfa2..efe9049d0 100644 --- a/examples/kitchen-sink/screens/internationalization/index.tsx +++ b/examples/kitchen-sink/screens/internationalization/index.tsx @@ -1,8 +1,9 @@ -import { defineScreens } from "../../shell/registry"; +import { defineScreens, lazyScreen } from "../../shell/registry"; import { ScrollDirection } from "./ScrollDirection"; import { TextAlignment } from "./TextAlignment"; export default defineScreens("internationalization", [ { id: "scroll-direction", title: "Scroll direction", titles: { ar: "اتجاه التمرير" }, summary: "A horizontal ScrollView: it starts at the leading edge, and its content reads correctly in right-to-left locales.", summaries: { ar: "عرض ScrollView أفقي يبدأ من الحافة الأمامية، ويُقرأ محتواه صحيحًا في اللغات التي تُكتب من اليمين إلى اليسار." }, component: ScrollDirection }, { id: "text-alignment", title: "Text alignment", titles: { ar: "محاذاة النص" }, summary: "Text in full-width boxes with each textAlign: the default and auto start at the leading edge in right-to-left locales.", summaries: { ar: "نص في مربعات بعرض كامل بكل قيمة لـ textAlign: تبدأ القيمة الافتراضية وauto من الحافة الأمامية في اللغات التي تُكتب من اليمين إلى اليسار." }, component: TextAlignment }, + { id: "expo-localization", title: "Expo Localization", titles: { ar: "توطين Expo" }, summary: "expo-localization on desktop: the user's preferred locales and calendar from macOS settings.", summaries: { ar: "‏expo-localization على سطح المكتب: اللغات والتقويم المفضلة للمستخدم من إعدادات macOS." }, component: lazyScreen(() => require("./ExpoLocalizationDemo").ExpoLocalizationDemo) }, ]); diff --git a/examples/kitchen-sink/screens/keyboard/KeyboardControllerDemo.tsx b/examples/kitchen-sink/screens/keyboard/KeyboardControllerDemo.tsx new file mode 100644 index 000000000..09aba08b6 --- /dev/null +++ b/examples/kitchen-sink/screens/keyboard/KeyboardControllerDemo.tsx @@ -0,0 +1,26 @@ +import { useState } from "react"; +import { Text, TextInput } from "react-native"; +import { KeyboardAwareScrollView, KeyboardController, KeyboardProvider, KeyboardToolbar, useKeyboardState } from "react-native-keyboard-controller"; + +// Test IDs: keyboard-keyboard-controller-passed once the provider, scroll view and toolbar render and the +// keyboard reports hidden. A Mac has no software keyboard, so the library runs its no-op web bindings. +const ID = "keyboard-keyboard-controller"; + +function Content() { + const visible = useKeyboardState(state => state.isVisible); + const [text, setText] = useState(""); + const hidden = !visible && !KeyboardController.isVisible(); + return <> + <KeyboardAwareScrollView bottomOffset={24} contentContainerStyle={{ gap: 12, padding: 24 }}> + <Text className="text-sm text-muted">KeyboardProvider, KeyboardAwareScrollView and KeyboardToolbar render; with no software keyboard on a Mac they leave layout unchanged.</Text> + <TextInput className="rounded-md border border-border px-3 py-2 text-foreground" placeholder="Type here" value={text} onChangeText={setText} /> + <Text selectable className="font-mono text-xs text-foreground" testID={hidden ? `${ID}-passed` : `${ID}-failed`}>{hidden ? "passed" : "failed: keyboard reported visible"}</Text> + </KeyboardAwareScrollView> + <KeyboardToolbar /> + </>; +} + +/** react-native-keyboard-controller through its own API; Spark's patch selects its web bindings on macOS. */ +export function KeyboardControllerDemo() { + return <KeyboardProvider><Content /></KeyboardProvider>; +} diff --git a/examples/kitchen-sink/screens/keyboard/index.tsx b/examples/kitchen-sink/screens/keyboard/index.tsx index 1b466584b..aac7ceefd 100644 --- a/examples/kitchen-sink/screens/keyboard/index.tsx +++ b/examples/kitchen-sink/screens/keyboard/index.tsx @@ -1,7 +1,8 @@ -import { defineScreens } from "../../shell/registry"; +import { defineScreens, lazyScreen } from "../../shell/registry"; import { Keyboard } from "./Keyboard"; // Ported from legend-apps' apps/test-kitchen-sink. export default defineScreens("keyboard", [ { id: "keyboard", title: "Keyboard monitor", titles: { ar: "مراقب لوحة المفاتيح" }, summary: "Key down/up events for the focused window, including natively consumed keys.", summaries: { ar: "أحداث ضغط المفاتيح وإفلاتها في النافذة النشطة، بما فيها المفاتيح التي يستهلكها النظام الأصلي." }, component: Keyboard }, + { id: "keyboard-controller", title: "Keyboard controller", titles: { ar: "متحكم لوحة المفاتيح" }, summary: "react-native-keyboard-controller on desktop: its provider, keyboard-aware scroll view and toolbar render without a software keyboard to avoid.", summaries: { ar: "‏react-native-keyboard-controller على سطح المكتب: يعرض المزوّد وعرض التمرير المدرك للوحة المفاتيح وشريط الأدوات دون لوحة مفاتيح برمجية يجب تجنبها." }, component: lazyScreen(() => require("./KeyboardControllerDemo").KeyboardControllerDemo) }, ]); diff --git a/examples/kitchen-sink/screens/media/ImageManipulatorDemo.tsx b/examples/kitchen-sink/screens/media/ImageManipulatorDemo.tsx new file mode 100644 index 000000000..844ca914a --- /dev/null +++ b/examples/kitchen-sink/screens/media/ImageManipulatorDemo.tsx @@ -0,0 +1,59 @@ +import { useEffect, useState } from "react"; +import { Image, ScrollView, Text, View } from "react-native"; +import { FlipType, ImageManipulator, SaveFormat } from "expo-image-manipulator"; +import { Skia } from "@shopify/react-native-skia"; +import { toByteArray } from "base64-js"; +import { getDirectory, writeBytes } from "@legendapp/spark/files"; + +// Test IDs: media-image-manipulator-passed, or -failed with the failing cases. Offline: the source is a +// 2×1 PNG (red, blue) written to the app's data folder; results are decoded with Skia and read by pixel. +const ID = "media-image-manipulator"; +const PNG = "iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAADklEQVR4nGP4z8AAQv8BD/kD/YURmXYAAAAASUVORK5CYII="; + +type Context = ReturnType<typeof ImageManipulator.manipulate>; + +async function pixels(context: Context) { + const result = await (await context.renderAsync()).saveAsync({ format: SaveFormat.PNG, base64: true }); + const image = Skia.Image.MakeImageFromEncoded(Skia.Data.fromBase64(result.base64!))!; + const colors = []; + for (let y = 0; y < image.height(); y++) for (let x = 0; x < image.width(); x++) { + const [r, , b] = image.readPixels(x, y, { width: 1, height: 1, colorType: 4, alphaType: 1 })!; + colors.push(r > 200 && b < 50 ? "R" : b > 200 && r < 50 ? "B" : "?"); + } + return { size: `${result.width}x${result.height}`, colors: colors.join(""), uri: result.uri }; +} + +async function check(uri: string) { + const cases = { + rotate: await pixels(ImageManipulator.manipulate(uri).rotate(90)), + flip: await pixels(ImageManipulator.manipulate(uri).flip(FlipType.Horizontal)), + resize: await pixels(ImageManipulator.manipulate(uri).resize({ width: 4 })), + crop: await pixels(ImageManipulator.manipulate(uri).crop({ originX: 1, originY: 0, width: 1, height: 1 })), + }; + // Resizing interpolates, so only its outer columns are exact. + const expected = { rotate: ["1x2", /^RB$/], flip: ["2x1", /^BR$/], resize: ["4x2", /^R..BR..B$/], crop: ["1x1", /^B$/] } as const; + const failures = (Object.keys(expected) as (keyof typeof cases)[]) + .filter(name => cases[name].size !== expected[name][0] || !expected[name][1].test(cases[name].colors)) + .map(name => `${name} (${cases[name].size} ${cases[name].colors})`); + return { failures, uris: Object.values(cases).map(result => result.uri) }; +} + +/** expo-image-manipulator through its own API; Spark's patch draws with CoreGraphics on macOS. */ +export function ImageManipulatorDemo() { + const [result, setResult] = useState<string>(); + const [uris, setUris] = useState<string[]>([]); + useEffect(() => { + void (async () => { + const file = `${await getDirectory("data")}/image-manipulator-demo.png`; + await writeBytes(file, toByteArray(PNG)); + const { failures, uris } = await check(`file://${file}`); + setUris(uris); + return failures; + })().then(failures => setResult(failures.length ? `failed: ${failures.join(", ")}` : "passed"), error => setResult(`failed: ${String(error)}`)); + }, []); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text className="text-sm text-muted">Rotates, flips, resizes and crops a 2×1 red and blue PNG, then reads each saved result by pixel.</Text> + <View style={{ flexDirection: "row", gap: 16 }}>{uris.map(uri => <Image key={uri} source={{ uri }} style={{ width: 64, height: 64 }} resizeMode="contain" />)}</View> + <Text selectable className="font-mono text-xs text-foreground" testID={result ? `${ID}-${result === "passed" ? "passed" : "failed"}` : undefined}>{result ?? "running"}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/media/ImagePickerDemo.tsx b/examples/kitchen-sink/screens/media/ImagePickerDemo.tsx new file mode 100644 index 000000000..b5ff011b7 --- /dev/null +++ b/examples/kitchen-sink/screens/media/ImagePickerDemo.tsx @@ -0,0 +1,35 @@ +import { useEffect, useState } from "react"; +import { Image, Pressable, ScrollView, Text } from "react-native"; +import { requireOptionalNativeModule } from "expo-modules-core"; +import * as ImagePicker from "expo-image-picker"; + +// Test IDs: media-image-picker-passed, or -failed with the failing cases. Picking opens an open panel, +// which the driver can't operate, so the check covers the module, permissions and the camera's absence. +const ID = "media-image-picker"; + +async function check() { + const library = await ImagePicker.getMediaLibraryPermissionsAsync(); + const camera = await ImagePicker.getCameraPermissionsAsync(); + const cameraError = await ImagePicker.launchCameraAsync().then(() => "resolved", (error: { code?: string }) => error.code); + return [ + requireOptionalNativeModule("ExponentImagePicker") == null && "native module", + !library.granted && `library permission (${library.status})`, + !["granted", "denied", "undetermined"].includes(camera.status) && `camera permission (${camera.status})`, + cameraError !== "ERR_UNAVAILABLE" && `launchCameraAsync (${cameraError})`, + ].filter(Boolean); +} + +/** expo-image-picker through its own API; Spark's patch picks image and movie files with an open panel on macOS. */ +export function ImagePickerDemo() { + const [result, setResult] = useState<string>(); + const [picked, setPicked] = useState<ImagePicker.ImagePickerAsset[]>([]); + useEffect(() => { check().then(failures => setResult(failures.length ? `failed: ${failures.join(", ")}` : "passed"), error => setResult(`failed: ${String(error)}`)); }, []); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text selectable className="font-mono text-xs text-foreground" testID={result ? `${ID}-${result === "passed" ? "passed" : "failed"}` : undefined}>{result ?? "running"}</Text> + <Pressable className="self-start rounded-md bg-highlight px-3 py-2" onPress={() => void ImagePicker.launchImageLibraryAsync({ mediaTypes: ["images", "videos"], allowsMultipleSelection: true }).then(response => setPicked(response.assets ?? []))}> + <Text className="text-foreground">Pick images or videos…</Text> + </Pressable> + {picked.map(asset => <Text key={asset.uri} selectable className="font-mono text-xs text-muted">{`${asset.type} ${asset.width}×${asset.height} ${asset.mimeType} ${asset.fileName}`}</Text>)} + {picked.filter(asset => asset.type === "image").map(asset => <Image key={asset.uri} source={{ uri: asset.uri }} style={{ width: 160, height: 120 }} resizeMode="contain" />)} + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/media/NitroZxingDemo.tsx b/examples/kitchen-sink/screens/media/NitroZxingDemo.tsx new file mode 100644 index 000000000..0096312f7 --- /dev/null +++ b/examples/kitchen-sink/screens/media/NitroZxingDemo.tsx @@ -0,0 +1,39 @@ +import { useEffect, useState } from "react"; +import { ScrollView, Text } from "react-native"; +import { createBarcodeScanner } from "react-native-nitro-zxing"; +import { loadImage } from "react-native-nitro-image"; +import { toByteArray } from "base64-js"; +import { getDirectory, writeBytes } from "@legendapp/spark/files"; + +// Test IDs: media-nitro-zxing-<value> shows the decoded QR value (the expected one is spark-kitchen-sink), +// or media-nitro-zxing-failed. +const ID = "media-nitro-zxing"; +// A QR code for "spark-kitchen-sink" generated with Core Image's CIQRCodeGenerator. +const QR = "iVBORw0KGgoAAAANSUhEUgAAAGwAAABsCAYAAACPZlfNAAAAAXNSR0IArs4c6QAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAbKADAAQAAAABAAAAbAAAAADaE/gkAAAEYklEQVR4Ae3cy25kRwwD0DjI///yjJHlacCEULe6ZYDeKdSDl0xJ8GCSrz/fP//059co8O+vYVqi/ytQw37Zvwg17JcZ9p98v76+/EdX49MTOuWb5tnPfPGr4nw3d35f2G3FH+5fwx4W9Ha7GnZb4Yf7v9ww+7tDxafx9AaYLx9j+Vif8NTP+mm+9caJb1+Yii2Pa9hyg6RXw1RkeRxvmPzTjjV/uuNTf3H738b9PmPnixvLX9y4L0xFlsc1bLlB0qthKrI8Ht+wd3/PdMcnfqlfwlP/23hf2G2FH+5fwx4W9Ha7GnZb4Yf7r79h/l7jjRFXH3HrT/Otvx33hd1W+OH+NexhQW+3q2G3FX64//iGpRvwML+Xdt4kE+RnvrH1p7HzT/tZ3xemIsvjGrbcIOnVMBVZHscbdnvnJ328CfK5jSd+8kn5p3hf2KmCb66vYW8W/HRcDTtV8M31LzfMm/BmPuNx6YaI+30Jl5D14rfjvrDbCj/cv4Y9LOjtdjXstsIP93+5YWmni5/y8SYYT+dZLz/7pfxULz6Np/P7wqYKfzi/hn3YgOn4GjZV7MP5X9879Mf/T4c7/5Sv41J/851/Wp/6TeebLz9x56e4LywptAyvYcsMSXRqWFJoGR5v2Clfd/hpv1Q/vRGJn/3MF0/8xO0nbv++MBVaHtew5QZJr4apyPI4/lmi/N2p4tOdbP1p7Hz5Jtz5KT/h9jNO/MzvC1OR5XENW26Q9GqYiiyPX34PSzs54X6v+eLGaaeLWz+Np/ym/eXrvCneFzZ14MP5NezDBkzH17CpYh/Of/k9TD7uXHFj893RKd9681M8rZef9eLOT/mnuPP6wlRkeVzDlhskvRqmIsvjeMPc4e7k6fdZP+2f6k/7Tb9nmi9/68X9nr4wFVse17DlBkmvhqnI8jjeMPm7U9255hun+oTbzzjxebp/6iee+Kb8vjAVXB7XsOUGSa+GqcjyeHzDvBHuXHG/X9x6809j+6f54sbyOcWn/PrCdGB5XMOWGyS9GqYiy+OXv9Pxab7pJrjzT/k+PS/1S3zT9/WFJQWX4TVsmSGJTg1LCi3DX34PO93B0+9zZxsnPgm3n/zEUz9x62/37wtT4eVxDVtukPRqmIosj19umHzTjjY/xd4A88Wn81O+/Z2f4tTfeudN6+3XF6Yiy+Mattwg6dUwFVkexxsmf3eyuPHpzp7Oc76xfFL/hNs/xfZLfMT7wpLCy/AatsyQRKeGJYWW4eMb9m7+7vA03xuR8k9x+Tk/4eYnPn1hSaFleA1bZkiiU8OSQsvw9TfMHe9NUE9x66f59rPe/ik/4fZzXl+YiiyPa9hyg6RXw1RkeTy+YWkHn37vaf90A+yf8hPu95rvPPOncV/YVLEP59ewDxswHV/Dpop9OD/eMHfybb7O8wYkXH6n9c6b9k/19ktxX1hSaBlew5YZkujUsKTQMnzdfx+2TJ91dPrC1lnyM6Ea9rM+69Aats6Snwn9Bflwihg/dJqNAAAAAElFTkSuQmCC"; + +async function scan() { + const file = `${await getDirectory("data")}/zxing-demo.png`; + await writeBytes(file, toByteArray(QR)); + const image = await loadImage({ filePath: file }); + try { + const scanner = createBarcodeScanner({ barcodeFormats: ["qr-code"] }); + try { + const [barcode] = await scanner.scanCodesInImageAsync(image); + return barcode?.rawValue; + } finally { + scanner.dispose(); + } + } finally { + image.dispose(); + } +} + +/** react-native-nitro-zxing through its own API; Spark adds the macOS build. */ +export function NitroZxingDemo() { + const [result, setResult] = useState<{ value?: string; error?: string }>(); + useEffect(() => { scan().then(value => setResult({ value }), error => setResult({ error: String(error) })); }, []); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text className="text-sm text-muted">Scans a QR code PNG from the app's data folder with ZXing (no camera).</Text> + <Text selectable className="font-mono text-xs text-foreground" testID={result && (result.value ? `${ID}-${result.value}` : `${ID}-failed`)}>{result ? result.value ?? `failed: ${result.error ?? "no QR code found"}` : "scanning"}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/media/index.tsx b/examples/kitchen-sink/screens/media/index.tsx new file mode 100644 index 000000000..3364ddb7e --- /dev/null +++ b/examples/kitchen-sink/screens/media/index.tsx @@ -0,0 +1,7 @@ +import { defineScreens, lazyScreen } from "../../shell/registry"; + +export default defineScreens("media", [ + { id: "nitro-zxing", title: "Barcode scanning", titles: { ar: "مسح الرموز الشريطية" }, summary: "react-native-nitro-zxing on desktop: ZXing reads a QR code from a still image loaded with Nitro Image.", summaries: { ar: "‏react-native-nitro-zxing على سطح المكتب: تقرأ مكتبة ZXing رمز QR من صورة ثابتة محمّلة عبر Nitro Image." }, component: lazyScreen(() => require("./NitroZxingDemo").NitroZxingDemo) }, + { id: "image-picker", title: "Image picker", titles: { ar: "منتقي الصور" }, summary: "expo-image-picker on desktop: picking image and movie files with an open panel; the camera picker reports unavailable.", summaries: { ar: "‏expo-image-picker على سطح المكتب: اختيار ملفات الصور والأفلام عبر لوحة فتح؛ ومنتقي الكاميرا يُبلغ بعدم توفره." }, component: lazyScreen(() => require("./ImagePickerDemo").ImagePickerDemo) }, + { id: "image-manipulator", title: "Image manipulator", titles: { ar: "معالج الصور" }, summary: "expo-image-manipulator on desktop: rotating, flipping, resizing and cropping an image, checked pixel by pixel.", summaries: { ar: "‏expo-image-manipulator على سطح المكتب: تدوير صورة وقلبها وتغيير حجمها واقتصاصها، مع التحقق بكسلًا بكسلًا." }, component: lazyScreen(() => require("./ImageManipulatorDemo").ImageManipulatorDemo) }, +]); diff --git a/examples/kitchen-sink/screens/native-controls/DateTimePickerDemo.tsx b/examples/kitchen-sink/screens/native-controls/DateTimePickerDemo.tsx new file mode 100644 index 000000000..9bb26714f --- /dev/null +++ b/examples/kitchen-sink/screens/native-controls/DateTimePickerDemo.tsx @@ -0,0 +1,27 @@ +import { useState } from "react"; +import { ScrollView, Text, View } from "react-native"; +import DateTimePicker from "@react-native-community/datetimepicker"; + +// Test IDs: native-controls-datetimepicker-<date|time|inline>-laid-out once each picker has a native frame. +// Fixed date, time zone and locale keep the golden stable. +const ID = "native-controls-datetimepicker"; +const pickers = [ + { name: "date", mode: "date", display: "default" }, + { name: "time", mode: "time", display: "default" }, + { name: "inline", mode: "datetime", display: "inline" }, +] as const; + +/** @react-native-community/datetimepicker through its own API; Spark's patch hosts it in NSDatePicker. */ +export function DateTimePickerDemo() { + const [value, setValue] = useState(new Date(Date.UTC(2026, 0, 15, 9, 30))); + const [laidOut, setLaidOut] = useState<string[]>([]); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-4 p-6"> + <Text selectable className="font-mono text-xs text-muted">{value.toISOString()}</Text> + {pickers.map(({ name, mode, display }) => <View key={name} className="flex-row items-center gap-3"> + <Text className="w-16 text-sm text-muted">{name}</Text> + <DateTimePicker value={value} mode={mode} display={display} timeZoneName="UTC" locale="en_US" onValueChange={(_, date) => setValue(date)} + testID={laidOut.includes(name) ? `${ID}-${name}-laid-out` : undefined} + onLayout={({ nativeEvent: { layout } }) => layout.width > 0 && layout.height > 0 && setLaidOut(previous => previous.includes(name) ? previous : [...previous, name])} /> + </View>)} + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/native-controls/index.tsx b/examples/kitchen-sink/screens/native-controls/index.tsx index 4e4bef048..5abbe6606 100644 --- a/examples/kitchen-sink/screens/native-controls/index.tsx +++ b/examples/kitchen-sink/screens/native-controls/index.tsx @@ -1,4 +1,4 @@ -import { defineScreens } from "../../shell/registry"; +import { defineScreens, lazyScreen } from "../../shell/registry"; import { Glass } from "./Glass"; import { Search } from "./Search"; import { SFSymbols } from "./SFSymbols"; @@ -14,4 +14,5 @@ export default defineScreens("native-controls", [ { id: "glass", title: "Glass", titles: { ar: "الزجاج" }, summary: "A native glass effect view.", summaries: { ar: "عرض أصلي بتأثير الزجاج." }, component: Glass }, { id: "symbol", title: "SF Symbols", titles: { ar: "رموز SF" }, summary: "SF Symbols at several sizes, scales and colors.", summaries: { ar: "رموز SF بعدة أحجام ومقاييس وألوان." }, component: SFSymbols }, { id: "search", title: "Search field", titles: { ar: "حقل البحث" }, summary: "Native search fields with change events and programmatic focus.", summaries: { ar: "حقول بحث أصلية مع أحداث التغيير والتركيز البرمجي." }, component: Search }, + { id: "datetimepicker", title: "Date & time picker", titles: { ar: "منتقي التاريخ والوقت" }, summary: "@react-native-community/datetimepicker on desktop: date, time and inline calendar pickers hosted in AppKit's NSDatePicker.", summaries: { ar: "‏@react-native-community/datetimepicker على سطح المكتب: منتقيات التاريخ والوقت والتقويم المضمّن داخل NSDatePicker في AppKit." }, component: lazyScreen(() => require("./DateTimePickerDemo").DateTimePickerDemo) }, ]); diff --git a/examples/kitchen-sink/screens/networking-webview/NetInfoDemo.tsx b/examples/kitchen-sink/screens/networking-webview/NetInfoDemo.tsx new file mode 100644 index 000000000..a85a72176 --- /dev/null +++ b/examples/kitchen-sink/screens/networking-webview/NetInfoDemo.tsx @@ -0,0 +1,17 @@ +import { ScrollView, Text } from "react-native"; +import { useNetInfoInstance } from "@react-native-community/netinfo"; + +// Test ID: networking-webview-netinfo-connected once the native module reports a connected wifi or +// ethernet state, the ways a Mac connects. +const ID = "networking-webview-netinfo"; + +/** @react-native-community/netinfo through its own API; it supports macOS upstream. */ +export function NetInfoDemo() { + // An isolated instance stops its internet-reachability polling when the screen unmounts. + const { netInfo: state } = useNetInfoInstance(); + const connected = !!state.isConnected && (state.type === "wifi" || state.type === "ethernet"); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text selectable className="font-mono text-xs text-muted">{JSON.stringify(state, null, 2)}</Text> + <Text className="font-mono text-xs text-foreground" testID={connected ? `${ID}-connected` : undefined}>{`${state.type}, connected ${state.isConnected}`}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/networking-webview/NitroFetchDemo.tsx b/examples/kitchen-sink/screens/networking-webview/NitroFetchDemo.tsx new file mode 100644 index 000000000..818b49362 --- /dev/null +++ b/examples/kitchen-sink/screens/networking-webview/NitroFetchDemo.tsx @@ -0,0 +1,36 @@ +import { useEffect, useState } from "react"; +import { ScrollView, Text } from "react-native"; +import { fetch, NitroFetch, prefetch } from "react-native-nitro-fetch"; +import { exists, getDirectory } from "@legendapp/spark/files"; + +// Test IDs: networking-webview-nitro-fetch-passed, or -failed with the failing cases. Needs network access. +const ID = "networking-webview-nitro-fetch"; +const URL = "https://registry.npmjs.org/react-native-nitro-fetch/1.9.0"; + +async function check() { + const response = await fetch(URL, { headers: { Accept: "application/json" } }); + const body = await response.json() as { name?: string; version?: string }; + const missing = await fetch("https://registry.npmjs.org/react-native-nitro-fetch/0.0.0-missing"); + await prefetch(URL, { headers: { prefetchKey: "kitchen-sink-nitro-fetch" } }); + const prefetched = await fetch(URL, { headers: { prefetchKey: "kitchen-sink-nitro-fetch" } }); + return [ + // Without the native module the library silently falls back to the global fetch. + typeof NitroFetch.createClient !== "function" && "native module", + (!response.ok || response.status !== 200) && `status ${response.status}`, + !response.headers.get("content-type")?.includes("json") && "headers", + (body.name !== "react-native-nitro-fetch" || body.version !== "1.9.0") && "json", + missing.status !== 404 && `missing status ${missing.status}`, + (await prefetched.json() as { version?: string }).version !== "1.9.0" && "prefetch", + !await exists(`${await getDirectory("cache")}/nitrofetch_urlcache`) && "cache folder", + ].filter(Boolean); +} + +/** react-native-nitro-fetch through its own fetch API; Spark only adds the macOS build. */ +export function NitroFetchDemo() { + const [result, setResult] = useState<string>(); + useEffect(() => { check().then(failures => setResult(failures.length ? `failed: ${failures.join(", ")}` : "passed"), error => setResult(`failed: ${String(error)}`)); }, []); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text className="text-sm text-muted">{`Fetches ${URL} natively, checks status, headers and JSON, a 404, and a prefetched response.`}</Text> + <Text selectable className="font-mono text-xs text-foreground" testID={result ? `${ID}-${result === "passed" ? "passed" : "failed"}` : undefined}>{result ?? "running"}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/networking-webview/WebBrowserDemo.tsx b/examples/kitchen-sink/screens/networking-webview/WebBrowserDemo.tsx new file mode 100644 index 000000000..930b607db --- /dev/null +++ b/examples/kitchen-sink/screens/networking-webview/WebBrowserDemo.tsx @@ -0,0 +1,24 @@ +import { useEffect, useState } from "react"; +import { ScrollView, Text } from "react-native"; +import * as WebBrowser from "expo-web-browser"; + +// Test ID: networking-webview-expo-web-browser-<result> once openBrowserAsync (an in-app web window on +// macOS) resolves after dismissBrowser closes it; the expected result is dismiss. +const ID = "networking-webview-expo-web-browser"; + +async function check() { + const opened = WebBrowser.openBrowserAsync("https://example.com"); + await new Promise(resolve => setTimeout(resolve, 1500)); + WebBrowser.dismissBrowser(); + return (await opened).type; +} + +/** expo-web-browser through its own API; it supports macOS upstream with a WKWebView window. */ +export function WebBrowserDemo() { + const [result, setResult] = useState<string>(); + useEffect(() => { check().then(setResult, error => setResult(`failed ${String(error)}`)); }, []); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text className="text-sm text-muted">Opens example.com in the browser window, then closes it with dismissBrowser.</Text> + <Text selectable className="font-mono text-xs text-foreground" testID={result ? `${ID}-${result}` : undefined}>{result ?? "running"}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/networking-webview/index.tsx b/examples/kitchen-sink/screens/networking-webview/index.tsx new file mode 100644 index 000000000..ceff1a8c3 --- /dev/null +++ b/examples/kitchen-sink/screens/networking-webview/index.tsx @@ -0,0 +1,7 @@ +import { defineScreens, lazyScreen } from "../../shell/registry"; + +export default defineScreens("networking-webview", [ + { id: "nitro-fetch", title: "Nitro Fetch", titles: { ar: "جلب Nitro" }, summary: "react-native-nitro-fetch on desktop: a native URLSession request with JSON, headers and status, plus a prefetch served from its cache.", summaries: { ar: "‏react-native-nitro-fetch على سطح المكتب: طلب URLSession أصلي مع JSON والترويسات والحالة، وجلب مسبق يُقدَّم من ذاكرته المؤقتة." }, component: lazyScreen(() => require("./NitroFetchDemo").NitroFetchDemo) }, + { id: "expo-web-browser", title: "Expo Web Browser", titles: { ar: "متصفح Expo" }, summary: "expo-web-browser on desktop: openBrowserAsync opens an in-app web window, and dismissBrowser closes it.", summaries: { ar: "‏expo-web-browser على سطح المكتب: يفتح openBrowserAsync نافذة ويب داخل التطبيق، ويغلقها dismissBrowser." }, component: lazyScreen(() => require("./WebBrowserDemo").WebBrowserDemo) }, + { id: "netinfo", title: "NetInfo", titles: { ar: "معلومات الشبكة" }, summary: "@react-native-community/netinfo on desktop: connection type and reachability from the system network monitor.", summaries: { ar: "‏@react-native-community/netinfo على سطح المكتب: نوع الاتصال وإمكانية الوصول من مراقب شبكة النظام." }, component: lazyScreen(() => require("./NetInfoDemo").NetInfoDemo) }, +]); diff --git a/examples/kitchen-sink/screens/processes-ipc/WorkletsCoreDemo.tsx b/examples/kitchen-sink/screens/processes-ipc/WorkletsCoreDemo.tsx new file mode 100644 index 000000000..44c782bd9 --- /dev/null +++ b/examples/kitchen-sink/screens/processes-ipc/WorkletsCoreDemo.tsx @@ -0,0 +1,39 @@ +import { useEffect, useState } from "react"; +import { ScrollView, Text } from "react-native"; +import { Worklets } from "react-native-worklets-core"; + +// Test IDs: processes-ipc-worklets-core-passed, or -failed with the failing cases. +const ID = "processes-ipc-worklets-core"; +// A worklet global, so the check reads the worklet runtime's own value rather than a captured copy. +declare const HermesInternal: unknown; + +async function check() { + const context = Worklets.createContext("kitchen-sink"); + const counter = Worklets.createSharedValue(0); + let echoed = ""; + const echo = Worklets.createRunOnJS((value: string) => { echoed = value; }); + const sum = await context.runAsync(() => { + "worklet"; + let total = 0; + for (let i = 1; i <= 1000; i++) total += i; + counter.value = 42; + echo(`from ${typeof HermesInternal === "object" ? "hermes" : "js"} worklet`); + return total; + }); + for (let waited = 0; !echoed && waited < 2000; waited += 50) await new Promise(resolve => setTimeout(resolve, 50)); + return [ + sum !== 500500 && `runAsync (${sum})`, + counter.value !== 42 && `shared value (${counter.value})`, + echoed !== "from hermes worklet" && `runOnJS (${echoed || "none"})`, + ].filter(Boolean); +} + +/** react-native-worklets-core through its own API; Spark only adds the macOS build. */ +export function WorkletsCoreDemo() { + const [result, setResult] = useState<string>(); + useEffect(() => { check().then(failures => setResult(failures.length ? `failed: ${failures.join(", ")}` : "passed"), error => setResult(`failed: ${String(error)}`)); }, []); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text className="text-sm text-muted">A worklet sums 1…1000 in its own context, writes a shared value and calls back to the main runtime.</Text> + <Text selectable className="font-mono text-xs text-foreground" testID={result ? `${ID}-${result === "passed" ? "passed" : "failed"}` : undefined}>{result ?? "running"}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/processes-ipc/index.tsx b/examples/kitchen-sink/screens/processes-ipc/index.tsx new file mode 100644 index 000000000..e105f3fe0 --- /dev/null +++ b/examples/kitchen-sink/screens/processes-ipc/index.tsx @@ -0,0 +1,5 @@ +import { defineScreens, lazyScreen } from "../../shell/registry"; + +export default defineScreens("processes-ipc", [ + { id: "worklets-core", title: "Worklets Core", titles: { ar: "نوى Worklets" }, summary: "react-native-worklets-core on desktop: a worklet context running JavaScript on its own thread, shared values and calls back to the main runtime.", summaries: { ar: "‏react-native-worklets-core على سطح المكتب: سياق worklet يشغّل JavaScript على خيط خاص به، وقيم مشتركة، واستدعاءات تعود إلى بيئة التشغيل الرئيسية." }, component: lazyScreen(() => require("./WorkletsCoreDemo").WorkletsCoreDemo) }, +]); diff --git a/examples/kitchen-sink/screens/security/ExpoCryptoDemo.tsx b/examples/kitchen-sink/screens/security/ExpoCryptoDemo.tsx new file mode 100644 index 000000000..610fd84b5 --- /dev/null +++ b/examples/kitchen-sink/screens/security/ExpoCryptoDemo.tsx @@ -0,0 +1,29 @@ +import { useEffect, useState } from "react"; +import { ScrollView, Text } from "react-native"; +import * as Crypto from "expo-crypto"; + +// Test IDs: security-expo-crypto-passed, or -failed with the failing cases. +const ID = "security-expo-crypto"; + +async function check() { + const sha256 = await Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, "abc"); + const sha1 = await Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA1, "abc", { encoding: Crypto.CryptoEncoding.BASE64 }); + // The sync getRandomBytes falls back to Math.random in development builds; the async one always asks the native module. + const bytes = await Crypto.getRandomBytesAsync(16); + return [ + sha256 !== "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" && "sha256", + sha1 !== "qZk+NkcGgWq6PiVxeFDCbJzQ2J0=" && `sha1 (${sha1})`, + (bytes.length !== 16 || bytes.every(byte => byte === 0)) && "random bytes", + !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(Crypto.randomUUID()) && "uuid", + ].filter(Boolean); +} + +/** expo-crypto through its own API; it supports macOS upstream. */ +export function ExpoCryptoDemo() { + const [result, setResult] = useState<string>(); + useEffect(() => { check().then(failures => setResult(failures.length ? `failed: ${failures.join(", ")}` : "passed"), error => setResult(`failed: ${String(error)}`)); }, []); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text className="text-sm text-muted">SHA-256 and SHA-1 digests of "abc" match published vectors; random bytes and a v4 UUID come from the native module.</Text> + <Text selectable className="font-mono text-xs text-foreground" testID={result ? `${ID}-${result === "passed" ? "passed" : "failed"}` : undefined}>{result ?? "running"}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/security/QuickCryptoDemo.tsx b/examples/kitchen-sink/screens/security/QuickCryptoDemo.tsx new file mode 100644 index 000000000..fdfff4564 --- /dev/null +++ b/examples/kitchen-sink/screens/security/QuickCryptoDemo.tsx @@ -0,0 +1,36 @@ +import { useEffect, useState } from "react"; +import { ScrollView, Text } from "react-native"; +import QuickCrypto from "react-native-quick-crypto"; + +// Test IDs: security-quick-crypto-passed, or security-quick-crypto-failed with the failing cases. +const ID = "security-quick-crypto"; +const hex = (bytes: ArrayBuffer | Uint8Array) => Array.from(new Uint8Array(bytes), byte => byte.toString(16).padStart(2, "0")).join(""); + +async function check() { + const key = QuickCrypto.randomBytes(32), iv = QuickCrypto.randomBytes(12); + const cipher = QuickCrypto.createCipheriv("aes-256-gcm", key, iv); + const sealed = QuickCrypto.Buffer.concat([cipher.update("desktop 🔐", "utf8"), cipher.final()]); + const decipher = QuickCrypto.createDecipheriv("aes-256-gcm", key, iv); + decipher.setAuthTag(cipher.getAuthTag()); + const opened = QuickCrypto.Buffer.concat([decipher.update(sealed), decipher.final()]).toString("utf8"); + const digest = await QuickCrypto.subtle.digest("SHA-256", new TextEncoder().encode("abc")); + return [ + QuickCrypto.createHash("sha256").update("abc").digest("hex") !== "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" && "sha256", + QuickCrypto.createHmac("sha256", "key").update("The quick brown fox jumps over the lazy dog").digest("hex") !== "f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8" && "hmac", + QuickCrypto.pbkdf2Sync("password", "salt", 4096, 20, "sha1").toString("hex") !== "4b007901b765489abead49d926f721d065a429c1" && "pbkdf2", + opened !== "desktop 🔐" && "aes-256-gcm", + hex(digest) !== "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" && "subtle.digest", + (key.length !== 32 || hex(key) === hex(QuickCrypto.randomBytes(32))) && "randomBytes", + !/^[0-9a-f-]{36}$/.test(QuickCrypto.randomUUID()) && "randomUUID", + ].filter(Boolean); +} + +/** react-native-quick-crypto through its own Node-style API; nothing Spark-specific. */ +export function QuickCryptoDemo() { + const [result, setResult] = useState<string>(); + useEffect(() => { check().then(failures => setResult(failures.length ? `failed: ${failures.join(", ")}` : "passed"), error => setResult(`failed: ${String(error)}`)); }, []); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text className="text-sm text-muted">SHA-256, HMAC, PBKDF2 and WebCrypto digests match published test vectors; AES-256-GCM round-trips.</Text> + <Text selectable className="font-mono text-xs text-foreground" testID={result ? `${ID}-${result === "passed" ? "passed" : "failed"}` : undefined}>{result ?? "running"}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/security/TrackingTransparencyDemo.tsx b/examples/kitchen-sink/screens/security/TrackingTransparencyDemo.tsx new file mode 100644 index 000000000..9d27ecae6 --- /dev/null +++ b/examples/kitchen-sink/screens/security/TrackingTransparencyDemo.tsx @@ -0,0 +1,19 @@ +import { useEffect, useState } from "react"; +import { ScrollView, Text } from "react-native"; +import { getTrackingStatus, requestTrackingPermission, type TrackingStatus } from "react-native-tracking-transparency"; +import { Button } from "../../Controls"; + +// Test ID: security-tracking-transparency-status-<status> shows getTrackingStatus(). On macOS the library +// reports unavailable without native code; App Tracking Transparency does nothing on the Mac. +const ID = "security-tracking-transparency"; + +/** react-native-tracking-transparency through its own API; it imports and answers on macOS unpatched. */ +export function TrackingTransparencyDemo() { + const [status, setStatus] = useState<TrackingStatus | string>(); + useEffect(() => { getTrackingStatus().then(setStatus, error => setStatus(`error ${String(error)}`)); }, []); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text className="text-sm text-muted">Tracking status. On the Mac, App Tracking Transparency never prompts, so the library reports unavailable.</Text> + <Text selectable className="font-mono text-xs text-foreground" testID={status ? `${ID}-status-${status}` : undefined}>{`status: ${status ?? "…"}`}</Text> + <Button onPress={() => void requestTrackingPermission().then(setStatus, error => setStatus(`error ${String(error)}`))}>Request tracking permission</Button> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/security/index.tsx b/examples/kitchen-sink/screens/security/index.tsx new file mode 100644 index 000000000..e52624550 --- /dev/null +++ b/examples/kitchen-sink/screens/security/index.tsx @@ -0,0 +1,7 @@ +import { defineScreens, lazyScreen } from "../../shell/registry"; + +export default defineScreens("security", [ + { id: "quick-crypto", title: "Quick Crypto", titles: { ar: "التشفير السريع" }, summary: "react-native-quick-crypto on desktop: hashes, HMAC, PBKDF2, AES-GCM, random bytes and WebCrypto digest against known vectors.", summaries: { ar: "‏react-native-quick-crypto على سطح المكتب: التجزئة وHMAC وPBKDF2 وAES-GCM والبايتات العشوائية وملخص WebCrypto مقارنةً بقيم معروفة." }, component: lazyScreen(() => require("./QuickCryptoDemo").QuickCryptoDemo) }, + { id: "tracking-transparency", title: "Tracking transparency", titles: { ar: "شفافية التتبع" }, summary: "react-native-tracking-transparency on desktop: App Tracking Transparency status from AppKit's ATTrackingManager, and a button to request permission.", summaries: { ar: "‏react-native-tracking-transparency على سطح المكتب: حالة شفافية تتبع التطبيق من ATTrackingManager، وزر لطلب الإذن." }, component: lazyScreen(() => require("./TrackingTransparencyDemo").TrackingTransparencyDemo) }, + { id: "expo-crypto", title: "Expo Crypto", titles: { ar: "تشفير Expo" }, summary: "expo-crypto on desktop: SHA digests, random bytes and UUIDs from the native module.", summaries: { ar: "‏expo-crypto على سطح المكتب: ملخصات SHA وبايتات وأرقام UUID عشوائية من الوحدة الأصلية." }, component: lazyScreen(() => require("./ExpoCryptoDemo").ExpoCryptoDemo) }, +]); diff --git a/examples/kitchen-sink/screens/storage-data/MMKVDemo.tsx b/examples/kitchen-sink/screens/storage-data/MMKVDemo.tsx new file mode 100644 index 000000000..0cdb993c8 --- /dev/null +++ b/examples/kitchen-sink/screens/storage-data/MMKVDemo.tsx @@ -0,0 +1,54 @@ +import { useEffect, useState } from "react"; +import { ScrollView, Text } from "react-native"; +import { createMMKV } from "react-native-mmkv"; +import { exists, getDirectory } from "@legendapp/spark/files"; + +// Test IDs: storage-data-mmkv-passed or -failed shows the round-trip result; storage-data-mmkv-launch-<n> +// counts launches persisted in MMKV, so clearState resets it only if MMKV lives in the app's data folder. +const ID = "storage-data-mmkv"; + +async function check() { + const storage = createMMKV({ id: "kitchen-sink-mmkv" }); + const launches = (storage.getNumber("launches") ?? 0) + 1; + storage.set("launches", launches); + const scratch = createMMKV({ id: "kitchen-sink-mmkv-scratch" }); + scratch.clearAll(); + const changed: string[] = []; + const listener = scratch.addOnValueChangedListener(key => changed.push(key)); + try { + scratch.set("text", "héllo 👋"); + scratch.set("number", 42.5); + scratch.set("flag", true); + const flag = scratch.getBoolean("flag"); + scratch.set("bytes", new Uint8Array([0, 127, 255]).buffer); + const removed = scratch.remove("flag") && !scratch.contains("flag"); + // Nitro delivers native callbacks to JS asynchronously. + for (let waited = 0; changed.length < 5 && waited < 2000; waited += 50) await new Promise(resolve => setTimeout(resolve, 50)); + const bytes = new Uint8Array(scratch.getBuffer("bytes") ?? new ArrayBuffer(0)); + const failures = [ + scratch.getString("text") !== "héllo 👋" && "string", + scratch.getNumber("number") !== 42.5 && "number", + flag !== true && "boolean", + (bytes.length !== 3 || bytes[2] !== 255) && "buffer", + scratch.getAllKeys().sort().join() !== "bytes,number,text" && "keys", + !removed && "remove", + changed.join() !== "text,number,flag,bytes,flag" && `listener (${changed.join()})`, + !await exists(`${await getDirectory("data")}/mmkv/kitchen-sink-mmkv`) && "data file", + ].filter(Boolean); + return { launches, result: failures.length ? `failed: ${failures.join(", ")}` : "passed" }; + } finally { + listener.remove(); + scratch.clearAll(); + } +} + +/** react-native-mmkv through its own API; Spark only patches where the macOS build stores files. */ +export function MMKVDemo() { + const [state, setState] = useState<{ launches?: number; result: string }>({ result: "running" }); + useEffect(() => { check().then(setState, error => setState({ result: `failed: ${String(error)}` })); }, []); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text className="text-sm text-muted">Values, buffers and listeners round-trip through MMKV. The launch count persists between launches.</Text> + <Text selectable className="font-mono text-xs text-foreground" testID={state.result === "running" ? undefined : `${ID}-${state.result === "passed" ? "passed" : "failed"}`}>{state.result}</Text> + <Text selectable className="font-mono text-xs text-foreground" testID={state.launches ? `${ID}-launch-${state.launches}` : undefined}>{`launches: ${state.launches ?? "…"}`}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/storage-data/NitroSQLiteDemo.tsx b/examples/kitchen-sink/screens/storage-data/NitroSQLiteDemo.tsx new file mode 100644 index 000000000..9a0faf719 --- /dev/null +++ b/examples/kitchen-sink/screens/storage-data/NitroSQLiteDemo.tsx @@ -0,0 +1,36 @@ +import { useEffect, useState } from "react"; +import { ScrollView, Text } from "react-native"; +import { open } from "react-native-nitro-sqlite"; + +// Test IDs: storage-data-nitro-sqlite-passed, or -failed with the failing cases. +const ID = "storage-data-nitro-sqlite"; + +async function check() { + const db = open({ name: "kitchen-sink-nitro.sqlite" }); + try { + db.execute("DROP TABLE IF EXISTS notes"); + db.execute("CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT, score REAL)"); + db.execute("INSERT INTO notes (body, score) VALUES (?, ?)", ["héllo 👋", 1.5]); + const affected = db.executeBatch([{ query: "INSERT INTO notes (body, score) VALUES (?, ?)", params: [["second", 2], ["third", 3]] }]); + const rows = (await db.executeAsync<{ body: string; score: number }>("SELECT body, score FROM notes ORDER BY id")).rows?._array ?? []; + const total = db.execute<{ total: number }>("SELECT sum(score) AS total FROM notes").rows?._array[0]?.total; + return [ + rows.length !== 3 && `rows (${rows.length})`, + rows[0]?.body !== "héllo 👋" && "unicode text", + total !== 6.5 && `sum (${total})`, + affected.rowsAffected !== 2 && `batch (${JSON.stringify(affected)})`, + ].filter(Boolean); + } finally { + db.delete(); + } +} + +/** react-native-nitro-sqlite through its own API; it targets macOS upstream. */ +export function NitroSQLiteDemo() { + const [result, setResult] = useState<string>(); + useEffect(() => { check().then(failures => setResult(failures.length ? `failed: ${failures.join(", ")}` : "passed"), error => setResult(`failed: ${String(error)}`)); }, []); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text className="text-sm text-muted">Creates a table, inserts with parameters and a batch, reads rows asynchronously, then deletes the database.</Text> + <Text selectable className="font-mono text-xs text-foreground" testID={result ? `${ID}-${result === "passed" ? "passed" : "failed"}` : undefined}>{result ?? "running"}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/storage-data/index.tsx b/examples/kitchen-sink/screens/storage-data/index.tsx new file mode 100644 index 000000000..77b671c9f --- /dev/null +++ b/examples/kitchen-sink/screens/storage-data/index.tsx @@ -0,0 +1,6 @@ +import { defineScreens, lazyScreen } from "../../shell/registry"; + +export default defineScreens("storage-data", [ + { id: "mmkv", title: "MMKV", titles: { ar: "تخزين MMKV" }, summary: "react-native-mmkv on desktop: typed values, buffers, change listeners, and storage that persists across launches in the app's own data folder.", summaries: { ar: "‏react-native-mmkv على سطح المكتب: قيم بأنواعها، ومخازن بايتات، ومستمعو التغيير، وتخزين يبقى بين مرات التشغيل في مجلد بيانات التطبيق نفسه." }, component: lazyScreen(() => require("./MMKVDemo").MMKVDemo) }, + { id: "nitro-sqlite", title: "Nitro SQLite", titles: { ar: "قاعدة Nitro SQLite" }, summary: "react-native-nitro-sqlite on desktop: parameterized queries, a transaction and async execution against a database in the app's Application Support folder.", summaries: { ar: "‏react-native-nitro-sqlite على سطح المكتب: استعلامات بمعاملات ومعاملة وتنفيذ غير متزامن على قاعدة بيانات في مجلد Application Support الخاص بالتطبيق." }, component: lazyScreen(() => require("./NitroSQLiteDemo").NitroSQLiteDemo) }, +]); diff --git a/examples/kitchen-sink/screens/system-integration/ExpoApplicationDemo.tsx b/examples/kitchen-sink/screens/system-integration/ExpoApplicationDemo.tsx new file mode 100644 index 000000000..5f432b847 --- /dev/null +++ b/examples/kitchen-sink/screens/system-integration/ExpoApplicationDemo.tsx @@ -0,0 +1,31 @@ +import { useEffect, useState } from "react"; +import { ScrollView, Text } from "react-native"; +import * as Application from "expo-application"; + +// Test IDs: system-integration-expo-application-passed, or -failed with the failing cases. +const ID = "system-integration-expo-application"; + +async function check() { + const installed = await Application.getInstallationTimeAsync(); + const releaseType = await Application.getIosApplicationReleaseTypeAsync(); + return { + summary: `${Application.applicationName} ${Application.applicationId} ${Application.nativeApplicationVersion} (${Application.nativeBuildVersion}), installed ${installed.toISOString()}`, + failures: [ + Application.applicationId !== "so.legend.spark.examples.kitchensink" && `id (${Application.applicationId})`, + !Application.nativeApplicationVersion && "version", + !(installed.getTime() > Date.UTC(2020, 0) && installed.getTime() <= Date.now()) && "installation time", + await Application.getIosIdForVendorAsync() !== null && "vendor id should be null", + releaseType !== Application.ApplicationReleaseType.UNKNOWN && `release type (${releaseType})`, + ].filter(Boolean), + }; +} + +/** expo-application through its own API; Spark's patch adds the macOS build. */ +export function ExpoApplicationDemo() { + const [result, setResult] = useState<{ summary?: string; failures: (string | false)[] }>(); + useEffect(() => { check().then(setResult, error => setResult({ failures: [String(error)] })); }, []); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text className="text-sm text-muted">{result?.summary ?? "…"}</Text> + <Text selectable className="font-mono text-xs text-foreground" testID={result ? `${ID}-${result.failures.length ? "failed" : "passed"}` : undefined}>{result ? (result.failures.length ? `failed: ${result.failures.join(", ")}` : "passed") : "running"}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/system-integration/ExpoDeviceDemo.tsx b/examples/kitchen-sink/screens/system-integration/ExpoDeviceDemo.tsx new file mode 100644 index 000000000..7f02808a7 --- /dev/null +++ b/examples/kitchen-sink/screens/system-integration/ExpoDeviceDemo.tsx @@ -0,0 +1,35 @@ +import { useEffect, useState } from "react"; +import { ScrollView, Text } from "react-native"; +import * as Device from "expo-device"; + +// Test IDs: system-integration-expo-device-passed, or -failed with the failing values. +const ID = "system-integration-expo-device"; + +async function check() { + const uptime = await Device.getUptimeAsync(); + const values = { modelId: Device.modelId, osName: Device.osName, osVersion: Device.osVersion, osBuildId: Device.osBuildId, deviceName: Device.deviceName, deviceType: Device.deviceType, totalMemory: Device.totalMemory, cpu: Device.supportedCpuArchitectures, uptime }; + return { + values, + failures: [ + !/^Mac/.test(Device.modelId ?? "") && "modelId", + Device.osName !== "macOS" && "osName", + !/^\d+\.\d+\.\d+$/.test(Device.osVersion ?? "") && "osVersion", + !Device.osBuildId && "osBuildId", + !Device.deviceName && "deviceName", + Device.deviceType !== Device.DeviceType.DESKTOP && "deviceType", + !((Device.totalMemory ?? 0) > 1e9) && "totalMemory", + !Device.supportedCpuArchitectures?.length && "cpu", + !(uptime > 0) && "uptime", + ].filter(Boolean), + }; +} + +/** expo-device through its own API; Spark's patch adds the macOS answers. */ +export function ExpoDeviceDemo() { + const [result, setResult] = useState<{ values?: object; failures: (string | false)[] }>(); + useEffect(() => { check().then(setResult, error => setResult({ failures: [String(error)] })); }, []); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text selectable className="font-mono text-xs text-muted">{result?.values ? JSON.stringify(result.values, null, 2) : "…"}</Text> + <Text selectable className="font-mono text-xs text-foreground" testID={result ? `${ID}-${result.failures.length ? "failed" : "passed"}` : undefined}>{result ? (result.failures.length ? `failed: ${result.failures.join(", ")}` : "passed") : "running"}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/system-integration/ExpoHapticsDemo.tsx b/examples/kitchen-sink/screens/system-integration/ExpoHapticsDemo.tsx new file mode 100644 index 000000000..d2eba5af7 --- /dev/null +++ b/examples/kitchen-sink/screens/system-integration/ExpoHapticsDemo.tsx @@ -0,0 +1,27 @@ +import { useEffect, useState } from "react"; +import { ScrollView, Text } from "react-native"; +import * as Haptics from "expo-haptics"; +import { Button } from "../../Controls"; + +// Test IDs: system-integration-expo-haptics-passed once every haptics call resolves (they reject with +// UnavailabilityError without a native module), or -failed with the error. +const ID = "system-integration-expo-haptics"; + +async function check() { + await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + await Haptics.selectionAsync(); +} + +/** expo-haptics through its own API; Spark's patch maps it to NSHapticFeedbackManager. */ +export function ExpoHapticsDemo() { + const [result, setResult] = useState<string>(); + useEffect(() => { check().then(() => setResult("passed"), error => setResult(`failed: ${String(error)}`)); }, []); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text className="text-sm text-muted">Rest a finger on a Force Touch trackpad and press the buttons to feel each kind of feedback.</Text> + <Button onPress={() => void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success)}>Notification</Button> + <Button onPress={() => void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy)}>Impact</Button> + <Button onPress={() => void Haptics.selectionAsync()}>Selection</Button> + <Text selectable className="font-mono text-xs text-foreground" testID={result ? `${ID}-${result === "passed" ? "passed" : "failed"}` : undefined}>{result ?? "running"}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/system-integration/ExpoLocationDemo.tsx b/examples/kitchen-sink/screens/system-integration/ExpoLocationDemo.tsx new file mode 100644 index 000000000..8b03ac840 --- /dev/null +++ b/examples/kitchen-sink/screens/system-integration/ExpoLocationDemo.tsx @@ -0,0 +1,50 @@ +import { useEffect, useState } from "react"; +import { Pressable, ScrollView, Text } from "react-native"; +import * as Location from "expo-location"; + +// Test IDs: system-integration-expo-location-passed, or -failed with the failing cases. The check reads +// state only: a permission prompt needs a person, and Macs have no compass. +const ID = "system-integration-expo-location"; + +async function check() { + const permission = await Location.getForegroundPermissionsAsync(); + const services = await Location.hasServicesEnabledAsync(); + const provider = await Location.getProviderStatusAsync(); + const headingError: unknown = await Location.getHeadingAsync().then(() => undefined, (error: unknown) => error); + // Permission is checked before heading support; the stream reports its error reason as a string. + const expectedHeadingError = permission.status === "granted" + ? headingError === "Heading updates not available" + : (headingError as { code?: string } | undefined)?.code === "ERR_DENIED_FOREGROUND_LOCATION_PERMISSION"; + return { + info: `permission ${permission.status}, services ${services}, provider ${JSON.stringify(provider)}`, + failures: [ + !["granted", "denied", "undetermined"].includes(permission.status) && `permission (${permission.status})`, + typeof services !== "boolean" && "services", + provider.locationServicesEnabled !== services && "provider status", + !expectedHeadingError && `heading (${String(headingError)})`, + ].filter(Boolean), + }; +} + +/** expo-location through its own API; Spark's patch builds it for macOS's CoreLocation. */ +export function ExpoLocationDemo() { + const [result, setResult] = useState<string>(); + const [info, setInfo] = useState(""); + const [position, setPosition] = useState<string>(); + useEffect(() => { check().then(({ info, failures }) => { setInfo(info); setResult(failures.length ? `failed: ${failures.join(", ")}` : "passed"); }, error => setResult(`failed: ${String(error)}`)); }, []); + async function locate() { + const permission = await Location.requestForegroundPermissionsAsync(); + if (!permission.granted) return setPosition(`permission ${permission.status}`); + const { coords } = await Location.getCurrentPositionAsync(); + const [place] = await Location.reverseGeocodeAsync(coords); + setPosition(`${coords.latitude.toFixed(3)}, ${coords.longitude.toFixed(3)} ±${Math.round(coords.accuracy ?? 0)} m${place ? ` · ${place.city}, ${place.country}` : ""}`); + } + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text selectable className="font-mono text-xs text-muted">{info}</Text> + <Text selectable className="font-mono text-xs text-foreground" testID={result ? `${ID}-${result === "passed" ? "passed" : "failed"}` : undefined}>{result ?? "running"}</Text> + <Pressable className="self-start rounded-md bg-highlight px-3 py-2" onPress={() => void locate().catch(error => setPosition(String(error)))}> + <Text className="text-foreground">Locate this Mac…</Text> + </Pressable> + {position && <Text selectable className="font-mono text-xs text-muted">{position}</Text>} + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/system-integration/KeepAwakeDemo.tsx b/examples/kitchen-sink/screens/system-integration/KeepAwakeDemo.tsx new file mode 100644 index 000000000..dc18c2e8a --- /dev/null +++ b/examples/kitchen-sink/screens/system-integration/KeepAwakeDemo.tsx @@ -0,0 +1,49 @@ +import { useEffect, useState } from "react"; +import { ScrollView, Text } from "react-native"; +import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake"; +import * as processes from "@legendapp/spark/processes"; + +// Test IDs: system-integration-keep-awake-passed, or -failed with the failing cases. +const ID = "system-integration-keep-awake"; +const TAG = "kitchen-sink-keep-awake"; + +/** Whether pmset lists this app's "expo-keep-awake" display-sleep assertion. */ +async function asserted() { + const result = await processes.runCommand({ target: { type: "executable", path: "/usr/bin/pmset" }, args: ["-g", "assertions"] }); + return new TextDecoder().decode(result.stdout).split("\n").some(line => line.includes("PreventUserIdleDisplaySleep") && line.includes('"expo-keep-awake"')); +} + +/** Polls pmset until the assertion matches `expected`, for up to two seconds. */ +async function settles(expected: boolean) { + for (let waited = 0; waited < 2000; waited += 100) { + if (await asserted() === expected) return true; + await new Promise(resolve => setTimeout(resolve, 100)); + } + return false; +} + +async function check() { + // Development builds hold their own keep-awake tag (Expo's dev tools), so the assertion can already be + // present; then only the active state is checked. + const shared = await asserted(); + try { + await activateKeepAwakeAsync(TAG); + if (!await settles(true)) return ["no assertion while active"]; + } finally { + await deactivateKeepAwake(TAG); + } + return shared || await settles(false) ? [] : ["assertion kept after deactivate"]; +} + +/** expo-keep-awake through its own API; Spark's patch gives macOS a power assertion. */ +export function KeepAwakeDemo() { + const [result, setResult] = useState<string>(); + useEffect(() => { + check().then(failures => setResult(failures.length ? `failed: ${failures.join(", ")}` : "passed"), error => setResult(`failed: ${String(error)}`)); + return () => void deactivateKeepAwake(TAG); + }, []); + return <ScrollView className="flex-1 bg-background" contentContainerClassName="gap-3 p-6"> + <Text className="text-sm text-muted">Activates keep-awake, checks pmset lists the display-sleep assertion, then deactivates and checks it is gone.</Text> + <Text selectable className="font-mono text-xs text-foreground" testID={result ? `${ID}-${result === "passed" ? "passed" : "failed"}` : undefined}>{result ?? "running"}</Text> + </ScrollView>; +} diff --git a/examples/kitchen-sink/screens/system-integration/index.tsx b/examples/kitchen-sink/screens/system-integration/index.tsx index 3b53714c2..0fc5fb108 100644 --- a/examples/kitchen-sink/screens/system-integration/index.tsx +++ b/examples/kitchen-sink/screens/system-integration/index.tsx @@ -1,6 +1,11 @@ -import { defineScreens } from "../../shell/registry"; +import { defineScreens, lazyScreen } from "../../shell/registry"; import { DockTaskbar } from "./DockTaskbar"; export default defineScreens("system-integration", [ { id: "dock", title: "Dock & taskbar", titles: { ar: "Dock وشريط المهام" }, summary: "App badge text and counts, Dock tile progress (normal, paused, error, indeterminate), attention requests, and the Dock menu or Jump List.", summaries: { ar: "نص شارة التطبيق وأعدادها، والتقدم على أيقونة Dock (عادي ومتوقف مؤقتًا وخطأ وغير محدد)، وطلبات التنبيه، وقائمة Dock أو قائمة الانتقال." }, component: DockTaskbar }, + { id: "keep-awake", title: "Keep awake", titles: { ar: "إبقاء الشاشة نشطة" }, summary: "expo-keep-awake on desktop: a power assertion that keeps the display and system awake while active, released on deactivate.", summaries: { ar: "‏expo-keep-awake على سطح المكتب: تأكيد طاقة يُبقي الشاشة والنظام نشطين أثناء التفعيل، ويُحرَّر عند الإلغاء." }, component: lazyScreen(() => require("./KeepAwakeDemo").KeepAwakeDemo) }, + { id: "expo-application", title: "Expo Application", titles: { ar: "تطبيق Expo" }, summary: "expo-application on desktop: bundle name, identifier and versions, installation time from the app bundle, and Mac answers for the iOS-only queries.", summaries: { ar: "‏expo-application على سطح المكتب: اسم الحزمة ومعرّفها وإصداراتها، ووقت التثبيت من حزمة التطبيق، وإجابات Mac عن الاستعلامات الخاصة بـ iOS." }, component: lazyScreen(() => require("./ExpoApplicationDemo").ExpoApplicationDemo) }, + { id: "expo-device", title: "Expo Device", titles: { ar: "جهاز Expo" }, summary: "expo-device on desktop: the Mac's model identifier, macOS version and build, memory, CPU architecture, device type and name.", summaries: { ar: "‏expo-device على سطح المكتب: معرّف طراز جهاز Mac وإصدار macOS وبنائه والذاكرة وبنية المعالج ونوع الجهاز واسمه." }, component: lazyScreen(() => require("./ExpoDeviceDemo").ExpoDeviceDemo) }, + { id: "expo-haptics", title: "Expo Haptics", titles: { ar: "اللمس في Expo" }, summary: "expo-haptics on desktop: notification, impact and selection feedback through the Force Touch trackpad.", summaries: { ar: "‏expo-haptics على سطح المكتب: ردود فعل الإشعار والصدمة والاختيار عبر لوحة تتبع Force Touch." }, component: lazyScreen(() => require("./ExpoHapticsDemo").ExpoHapticsDemo) }, + { id: "expo-location", title: "Expo Location", titles: { ar: "الموقع عبر Expo" }, summary: "expo-location on desktop: permission, Location Services and provider status from CoreLocation, with an on-demand position and address.", summaries: { ar: "‏expo-location على سطح المكتب: حالة الإذن وخدمات الموقع والمزوّد من CoreLocation، مع تحديد الموقع والعنوان عند الطلب." }, component: lazyScreen(() => require("./ExpoLocationDemo").ExpoLocationDemo) }, ]); diff --git a/examples/kitchen-sink/screens/windows/SafeAreaDemo.tsx b/examples/kitchen-sink/screens/windows/SafeAreaDemo.tsx new file mode 100644 index 000000000..bc48ad6d9 --- /dev/null +++ b/examples/kitchen-sink/screens/windows/SafeAreaDemo.tsx @@ -0,0 +1,32 @@ +import { useState } from "react"; +import { Text, View } from "react-native"; +import { SafeAreaProvider, SafeAreaView, useSafeAreaFrame, useSafeAreaInsets } from "react-native-safe-area-context"; + +// Test IDs: windows-safe-area-provider-ready once the native provider reports a frame; +// windows-safe-area-view-laid-out once SafeAreaView's child has a size. +const ID = "windows-safe-area"; + +function Metrics() { + const insets = useSafeAreaInsets(); + const frame = useSafeAreaFrame(); + const ready = frame.width > 0 && frame.height > 0 && [insets.top, insets.right, insets.bottom, insets.left].every(Number.isFinite); + return <Text selectable className="font-mono text-xs text-foreground" testID={ready ? `${ID}-provider-ready` : undefined}> + {`frame ${Math.round(frame.width)}×${Math.round(frame.height)}, insets ${JSON.stringify(insets)}`} + </Text>; +} + +/** react-native-safe-area-context through its own API; it supports macOS upstream. */ +export function SafeAreaDemo() { + const [laidOut, setLaidOut] = useState(false); + return <SafeAreaProvider style={{ flex: 1 }}> + <View style={{ padding: 24, gap: 12 }}> + <Text className="text-sm text-muted">The provider measures its native view; insets are 0 in a standard window.</Text> + <Metrics /> + <SafeAreaView edges={["top", "bottom"]} style={{ borderWidth: 1, borderColor: "#888", borderRadius: 8 }}> + <View style={{ padding: 12 }} onLayout={event => setLaidOut(event.nativeEvent.layout.width > 0)}> + <Text className="text-foreground" testID={laidOut ? `${ID}-view-laid-out` : undefined}>Content inside SafeAreaView</Text> + </View> + </SafeAreaView> + </View> + </SafeAreaProvider>; +} diff --git a/examples/kitchen-sink/screens/windows/index.tsx b/examples/kitchen-sink/screens/windows/index.tsx index 87f72dd90..4174cbedf 100644 --- a/examples/kitchen-sink/screens/windows/index.tsx +++ b/examples/kitchen-sink/screens/windows/index.tsx @@ -1,4 +1,4 @@ -import { defineScreens } from "../../shell/registry"; +import { defineScreens, lazyScreen } from "../../shell/registry"; import { WindowControls } from "./WindowControls"; import { WindowManager } from "./WindowManager"; @@ -6,4 +6,5 @@ import { WindowManager } from "./WindowManager"; export default defineScreens("windows", [ { id: "window-manager", title: "Window manager", titles: { ar: "مدير النوافذ" }, summary: "Open, resize, rename, blur (macOS) and close a child window; main window frame and focus events.", summaries: { ar: "فتح نافذة فرعية وتغيير حجمها وإعادة تسميتها وإزالة التركيز عنها (macOS) وإغلاقها؛ وإطار النافذة الرئيسية وأحداث التركيز." }, component: WindowManager }, { id: "window-controls", title: "Window controls", titles: { ar: "أزرار التحكم في النافذة" }, summary: "Hide and show the macOS traffic lights; fullscreen state.", summaries: { ar: "إخفاء أزرار الإغلاق والتصغير والتكبير في macOS وإظهارها؛ وحالة ملء الشاشة." }, component: WindowControls }, + { id: "safe-area", title: "Safe area", titles: { ar: "المنطقة الآمنة" }, summary: "react-native-safe-area-context on desktop: provider frame and insets from the window's NSView, SafeAreaView padding and the insets hook.", summaries: { ar: "‏react-native-safe-area-context على سطح المكتب: إطار المزوّد والحواف من NSView للنافذة، وحشوة SafeAreaView وخطاف الحواف." }, component: lazyScreen(() => require("./SafeAreaDemo").SafeAreaDemo) }, ]); diff --git a/examples/kitchen-sink/shell/registry.ts b/examples/kitchen-sink/shell/registry.ts index 5f9a897bf..2d76791d5 100644 --- a/examples/kitchen-sink/shell/registry.ts +++ b/examples/kitchen-sink/shell/registry.ts @@ -1,5 +1,5 @@ -import { createElement, type ComponentType } from "react"; -import { View } from "react-native"; +import { Component, createElement, type ComponentType, type ReactNode } from "react"; +import { Text, View } from "react-native"; import { AREAS, type Area, type AreaId } from "./areas"; import type { Locale } from "./i18n"; @@ -29,6 +29,26 @@ const SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; export function screenTitle(screen: Screen, locale: Locale): string { return locale === "en" ? screen.title : screen.titles[locale]; } export function screenSummary(screen: Screen, locale: Locale): string { return locale === "en" ? screen.summary : screen.summaries[locale]; } +/** + * A screen whose module loads on its first render, so a library without a build for this platform + * breaks only its own screen, not the catalog at import. A screen that throws shows its error instead + * of unmounting the whole Kitchen Sink. + */ +export function lazyScreen(load: () => ComponentType): ComponentType { + let component: ComponentType | undefined; + const Loaded = () => { component ??= load(); return createElement(component); }; + return function LazyScreen() { return createElement(ScreenErrorBoundary, null, createElement(Loaded)); }; +} + +class ScreenErrorBoundary extends Component<{ children: ReactNode }, { error?: unknown }> { + state: { error?: unknown } = {}; + static getDerivedStateFromError(error: unknown) { return { error }; } + render() { + return this.state.error === undefined ? this.props.children + : createElement(Text, { selectable: true, style: { padding: 24, color: "#ef4444", fontFamily: "Menlo" } }, String(this.state.error)); + } +} + /** The test driver waits for this ID to confirm a navigated screen has rendered. */ export function screenRootTestID(area: string, screen: string) { return `${area}-${screen}-root`; } diff --git a/fixtures/native-greeting/package.json b/fixtures/native-greeting/package.json index 7f6cad135..b5217fc12 100644 --- a/fixtures/native-greeting/package.json +++ b/fixtures/native-greeting/package.json @@ -11,9 +11,9 @@ "windows" ], "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", - "react-native-windows": "0.81.35" + "react": "^19.2.0", + "react-native": "0.83.10", + "react-native-windows": "0.83.2" }, "codegenConfig": { "name": "RNGreetingSpec", diff --git a/fixtures/sdk-test-driver/package.json b/fixtures/sdk-test-driver/package.json index c1a87804e..43172181c 100644 --- a/fixtures/sdk-test-driver/package.json +++ b/fixtures/sdk-test-driver/package.json @@ -10,8 +10,8 @@ "*.podspec" ], "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6" + "react": "^19.2.0", + "react-native": "0.83.10" }, "codegenConfig": { "name": "RNSDKTestDriverSpec", diff --git a/package.json b/package.json index e4202e4ea..e88d1312b 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "e2e:run": "bun scripts/e2e/cli.ts run", "e2e:golden": "bun scripts/e2e/cli.ts golden", "gate": "bun scripts/e2e/cli.ts gate", + "gate:pointer": "bun scripts/e2e/cli.ts run e2e/pointer --backend driver --pointer", "test:native": "bun scripts/test-native.ts", "test:all": "bun run typecheck && bun run test && bun run test:sparkle && bun run test:updates && bun run test:integrations && bun run test:native && bun run test:expansion && bun run test:runtimes:all", "test:sparkle": "bun tests/sparkle.integration.ts", @@ -60,21 +61,21 @@ "test:customize:app": "bun scripts/test-customize-app.ts" }, "devDependencies": { - "@types/react": "19.1.10", + "@types/react": "19.2.18", "@types/ws": "8.18.1", "typescript": "5.9.3", "@legendapp/spark": "0.0.1", - "react": "19.1.4", - "react-dom": "19.1.4", - "react-native": "0.81.6", + "react": "19.2.0", + "react-dom": "19.2.0", + "react-native": "0.83.10", "@legendapp/spark-desktop-config": "0.0.1", "@legendapp/spark-window-options": "0.0.1", "@legendapp/spark-ui": "0.0.1", - "@expo/ui": "0.2.0-beta.9", - "expo-clipboard": "8.0.8", - "expo-linking": "8.0.12", - "expo-secure-store": "15.0.8", - "expo": "54.0.37", + "@expo/ui": "55.0.17", + "expo-clipboard": "55.0.17", + "expo-linking": "55.0.17", + "expo-secure-store": "55.0.18", + "expo": "55.0.32", "@legendapp/spark-desktop-app": "0.0.1", "@legendapp/spark-desktop-windows": "0.0.1", "@legendapp/spark-desktop-shortcuts": "0.0.1", @@ -82,12 +83,12 @@ "@legendapp/spark-native-menu": "0.0.1", "@legendapp/spark-file-dialog": "0.0.1", "@legendapp/spark-message-dialog": "0.0.1", - "expo-document-picker": "14.0.8", - "expo-file-system": "19.0.24", - "expo-sharing": "14.0.8", + "expo-document-picker": "55.0.17", + "expo-file-system": "55.0.26", + "expo-sharing": "55.0.25", "@legendapp/spark-audio": "0.0.1", "@react-native-async-storage/async-storage": "2.2.0", - "expo-audio": "1.1.1", + "expo-audio": "55.0.18", "diff": "8.0.4", "base64-js": "1.5.1", "uniwind": "1.6.3", @@ -108,13 +109,13 @@ "react-native-nitro-modules": "0.37.0", "@react-native-runtimes/core": "0.1.0-alpha.2", "@legendapp/spark-auth-session": "0.0.1", - "expo-web-browser": "15.0.10", - "expo-crypto": "15.0.8", + "expo-web-browser": "55.0.20", + "expo-crypto": "55.0.19", "@types/node": "24.13.6", "vitest": "5.0.1", "ws": "8.21.0", "@legendapp/list": "3.4.0", - "react-test-renderer": "19.1.4", + "react-test-renderer": "19.2.0", "@legendapp/spark-markdown": "0.0.1", "@legendapp/spark-html": "0.0.1", "react-native-enriched-markdown": "1.1.1", @@ -128,23 +129,23 @@ "react-native-nitro-image": "0.15.2" }, "overrides": { - "@expo/cli": "54.0.27", - "react": "19.1.4", - "react-native": "0.81.6", - "expo": "54.0.37", - "expo-constants": "18.0.14", - "expo-modules-core": "3.0.30", + "@expo/cli": "55.0.37", + "react": "19.2.0", + "react-native": "0.83.10", + "expo": "55.0.32", + "expo-constants": "55.0.17", + "expo-modules-core": "55.0.27", "react-native-nitro-modules": "0.37.0", "react-native-webview": "16.0.0", "@op-engineering/op-sqlite": "18.2.1", - "expo-asset": "12.0.13", - "expo-file-system": "19.0.24", - "expo-font": "14.0.12", - "expo-keep-awake": "15.0.8", - "expo-desktop-modules-core": "54.0.14", - "expo-desktop-stubs": "54.0.15", - "react-native-macos": "0.81.7", - "react-native-windows": "0.81.35", + "expo-asset": "55.0.20", + "expo-file-system": "55.0.26", + "expo-font": "55.0.8", + "expo-keep-awake": "55.0.8", + "expo-desktop-modules-core": "55.0.2", + "expo-desktop-stubs": "55.0.2", + "react-native-macos": "0.83.0", + "react-native-windows": "0.83.2", "postcss": "8.5.31", "dompurify": "3.4.17", "@tiptap/core": "3.32.0", @@ -169,14 +170,14 @@ "@tiptap/extension-bubble-menu": "3.32.0", "@tiptap/extension-floating-menu": "3.32.0", "fast-xml-parser": "5.11.2", - "react-dom": "19.1.4" + "react-dom": "19.2.0" }, "sparkWorkspacePatches": { "react-native-nitro-modules@0.37.0": "patches/workspace/react-native-nitro-modules@0.37.0.patch", "@op-engineering/op-sqlite@18.2.1": "patches/workspace/op-engineering-op-sqlite@18.2.1.patch", "react-native-webview@16.0.0": "patches/workspace/react-native-webview@16.0.0.patch", "@react-native-runtimes/core@0.1.0-alpha.2": "patches/workspace/react-native-runtimes-core@0.1.0-alpha.2.patch", - "expo-audio@1.1.1": "patches/workspace/expo-audio@1.1.1.patch" + "expo-audio@55.0.18": "patches/workspace/expo-audio@55.0.18.patch" }, "engines": { "node": ">=24.19.0" diff --git a/packages/ai/package.json b/packages/ai/package.json index b2c0cbd85..abf4f9db9 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -9,8 +9,8 @@ "@legendapp/spark-processes": "0.0.1" }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6" + "react": "^19.2.0", + "react-native": "0.83.10" }, "license": "MIT", "files": [ diff --git a/packages/audio/package.json b/packages/audio/package.json index f0c6d4c2e..4a2d86e35 100644 --- a/packages/audio/package.json +++ b/packages/audio/package.json @@ -11,9 +11,9 @@ "*.podspec" ], "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", - "expo-audio": "1.1.1" + "react": "^19.2.0", + "react-native": "0.83.10", + "expo-audio": "55.0.18" }, "peerDependenciesMeta": { "expo-audio": { diff --git a/packages/audio/src/index.macos.ts b/packages/audio/src/index.macos.ts new file mode 100644 index 000000000..9c4641a4b --- /dev/null +++ b/packages/audio/src/index.macos.ts @@ -0,0 +1 @@ +export * from "./desktop"; diff --git a/packages/auth-session/package.json b/packages/auth-session/package.json index cfac3acfe..14494da75 100644 --- a/packages/auth-session/package.json +++ b/packages/auth-session/package.json @@ -13,9 +13,9 @@ "@legendapp/spark-desktop-app": "0.0.1" }, "peerDependencies": { - "react-native": "0.81.6", - "expo-web-browser": "~15.0.10", - "expo-crypto": "~15.0.8" + "react-native": "0.83.10", + "expo-web-browser": "~55.0.20", + "expo-crypto": "~55.0.19" }, "peerDependenciesMeta": { "expo-web-browser": { diff --git a/packages/auth-session/src/index.macos.ts b/packages/auth-session/src/index.macos.ts new file mode 100644 index 000000000..a88bebe96 --- /dev/null +++ b/packages/auth-session/src/index.macos.ts @@ -0,0 +1 @@ +export * from "./index.ts"; diff --git a/packages/cli/src/add-desktop.ts b/packages/cli/src/add-desktop.ts index 34473efd4..ee985870b 100644 --- a/packages/cli/src/add-desktop.ts +++ b/packages/cli/src/add-desktop.ts @@ -53,7 +53,7 @@ export async function addDesktop(root: string, manifestFile: string | undefined) const req = createRequire(pkgFile); const installed = (name: string) => readJson(req.resolve(`${name}/package.json`)).version; // Never upgrade the mobile baseline as a side effect of adding desktop. - for (const [name, version] of Object.entries({ expo: "54.0.37", "react-native": "0.81.6", react: "19.1.4" })) { + for (const [name, version] of Object.entries({ expo: "55.0.32", "react-native": "0.83.10", react: "19.2.0" })) { let actual: string; try { actual = installed(name); } catch { throw new Error(`Install the app's dependencies first; ${name} is missing.`); } if (actual !== version) throw new Error(`The tested Expo Desktop beta needs ${name}@${version}; this app has ${actual}. Align the app's baseline separately, then retry. No files were changed.`); @@ -130,7 +130,7 @@ export async function addDesktop(root: string, manifestFile: string | undefined) } // Resolve local transitive SDK packages without replacing any mobile pins. const vendor = ["@react-native-runtimes/core", "react-native-nitro-modules", "@op-engineering/op-sqlite", "react-native-webview"]; - const overrides: Record<string, string> = { ...Object.fromEntries(local.map(name => [name, dependencies[name]!])), "@expo/cli": "54.0.27", + const overrides: Record<string, string> = { ...Object.fromEntries(local.map(name => [name, dependencies[name]!])), "@expo/cli": "55.0.37", "react-native-macos": defaults.overrides["react-native-macos"], "react-native-windows": defaults.overrides["react-native-windows"] }; for (const name of vendor) { if (archives[name]) { diff --git a/packages/cli/src/build.ts b/packages/cli/src/build.ts index 52fa74e14..a3202c84a 100644 --- a/packages/cli/src/build.ts +++ b/packages/cli/src/build.ts @@ -8,7 +8,7 @@ import { macOSXcodeArchitecture, projectPlatform } from "./platform.ts"; import { isUniversal, isExpoProject } from "@legendapp/spark-desktop-config/config.cjs"; import { buildWindows } from "./windows.ts"; import { copyHelpers } from "./helpers.ts"; -import { readAppConfig } from "./project.ts"; +import { lacksMacOSPod, readAppConfig } from "./project.ts"; import { cpSync, existsSync, @@ -241,12 +241,17 @@ async function buildUnlocked( if (productionHash) runtime.fingerprint = digest(runtime.fingerprint + productionHash); const resultFile = stateFile(root, `${mode}-build.json`); + // RNW's CLI config probes for PowerShell when Expo autolinking loads it on macOS. const excludedNames = [...new Set<string>([ + "react-native-windows", ...(readAppConfig(root).expo.autolinking?.exclude ?? []), ...chosen.excluded.map((p) => p.name), ])]; + // Only the macOS Podfile drops these; iOS and Android builds reading the shared config keep them. + const withoutMacOSPod = chosen.included.filter(lacksMacOSPod).map((p) => p.name).filter((name) => !excludedNames.includes(name)); writeJson(stateFile(root, "native-selection.json"), { - excluded: excludedNames, + excluded: [...excludedNames, ...withoutMacOSPod], + withoutMacOSPod, included: chosen.included.map((p) => ({ name: p.name, root: p.root })), }); const rnConfig = path.join(root, "react-native.config.js"); @@ -261,7 +266,7 @@ async function buildUnlocked( ); if (!isUniversal(root)) writeFileSync( rnConfig, - `${marker}\nconst selection = require('./.spark/native-selection.json');\nmodule.exports = { dependencies: Object.fromEntries([\n...selection.included.map(p => [p.name, { root: p.root }]),\n...selection.excluded.map(name => [name, { platforms: { ios: null, macos: null, android: null } }])\n]) };\n`, + `${marker}\nconst selection = require('./.spark/native-selection.json');\nmodule.exports = { dependencies: Object.fromEntries([\n...selection.included.map(p => [p.name, { root: p.root }]),\n...selection.excluded.map(name => [name, { platforms: selection.withoutMacOSPod.includes(name) ? { macos: null } : { ios: null, macos: null, android: null } }])\n]) };\n`, ); const pkg = readJson(path.join(root, "package.json")); if (!isUniversal(root)) { @@ -317,7 +322,7 @@ async function buildUnlocked( "--platform", "macos", "--template", - "expo-desktop-template-bare-minimum@54.81.6", + "expo-desktop-template-bare-minimum@55.83.10", "--no-install", // Template renaming is not idempotent (HelloWorld becomes // LegendHelloWorld, then LegendLegendHelloWorld). Native projects diff --git a/packages/cli/src/examples.ts b/packages/cli/src/examples.ts index 289bce751..5703a0f2d 100644 --- a/packages/cli/src/examples.ts +++ b/packages/cli/src/examples.ts @@ -12,9 +12,9 @@ export async function configureExample(root: string, example: Example) { if (example === "diff-lite") suffixes.push("processes"); const desktop = suffixes.map(name => name === "desktop" ? "@legendapp/spark" : `@legendapp/spark-${name}`); if (!pkg.dependencies["@legendapp/spark"]) throw new Error("SDK example requires @legendapp/spark"); - Object.assign(pkg.dependencies, { "expo-document-picker": "14.0.8", "expo-file-system": "19.0.24", "expo-sharing": "14.0.8" }); + Object.assign(pkg.dependencies, { "expo-document-picker": "55.0.17", "expo-file-system": "55.0.26", "expo-sharing": "55.0.25" }); if (example !== "document-editor") pkg.dependencies["@react-native-async-storage/async-storage"] = "2.2.0"; - if (example === "music-lite") pkg.dependencies["expo-audio"] = pkg.dependencies["expo-audio"] ?? pkg.overrides?.["expo-audio"] ?? pkg.resolutions?.["expo-audio"] ?? pkg.pnpm?.overrides?.["expo-audio"] ?? "1.1.1"; + if (example === "music-lite") pkg.dependencies["expo-audio"] = pkg.dependencies["expo-audio"] ?? pkg.overrides?.["expo-audio"] ?? pkg.resolutions?.["expo-audio"] ?? pkg.pnpm?.overrides?.["expo-audio"] ?? "55.0.18"; if (example === "diff-lite") pkg.dependencies["diff"] = "8.0.2"; writeJson(path.join(root, "package.json"), pkg); const config = readJson(path.join(root, "desktop.config.json")); diff --git a/packages/cli/src/expo-dev-patch.cjs b/packages/cli/src/expo-dev-patch.cjs index be36b166b..9defff5bf 100644 --- a/packages/cli/src/expo-dev-patch.cjs +++ b/packages/cli/src/expo-dev-patch.cjs @@ -3,18 +3,18 @@ const path = require('node:path'); const { createRequire } = require('node:module'); const { createHash } = require('node:crypto'); -const VERSION = '54.0.27'; +const VERSION = '55.0.37'; const extension = JSON.stringify(path.join(__dirname, 'expo-dev-extension.cjs')); // Process-local patches: installed Expo files remain untouched. Verify the exact // upstream inputs before starting, so an Expo upgrade cannot silently lose keys. const patches = [ - ['interface/commandsTable.js', 'eeb1700b0ae26613bf6dedab556479a4c15d3a3c94c63ecbb5db21c4899e1645', + ['interface/commandsTable.js', '6d809faa4721fd68e86af40372e6aef7f729902afc1a1484f66cade067db4792', 'function logCommandsTable(ui) {', `function logCommandsTable(ui) {\n ui = require(${extension}).integrateCommands(ui);`], ['interface/startInterface.js', 'c8e55431b18a37e1ab2be22f068c980e9cc41ffc105ea7478e34fc11b91a22bd', ' const onPressAsync = async (key)=>{', ` const onPressAsync = async (key)=>{\n try {\n if (await require(${extension}).handleKey(key)) return;\n } catch (error) {\n _log.exception(error);\n return;\n }`], - ['startAsync.js', '0e062078e63ad6264b1393d403ad56d65a38c6430d30fdac50cd656486cd418b', + ['startAsync.js', 'fb52b3bd7962ad13c57c1a6e4050c9cc1420197186df7d0259d74ddbd79ae46b', ' await (0, _profile.profile)(devServerManager.startAsync.bind(devServerManager))(startOptions);', // Dependency validation can wait on the network after Metro is listening. // Report actual server readiness before those optional startup checks. diff --git a/packages/cli/src/expo-dev-preload.cjs b/packages/cli/src/expo-dev-preload.cjs index fd8333ac7..bdb5ce267 100644 --- a/packages/cli/src/expo-dev-preload.cjs +++ b/packages/cli/src/expo-dev-preload.cjs @@ -6,6 +6,9 @@ const { preparePatch } = require('./expo-dev-patch.cjs'); if (!process.env.SPARK_EXPO_PRELOADED) { process.env.SPARK_EXPO_PRELOADED = '1'; if (!process.send) throw new Error('Start the Spark Expo integration through spark dev.'); + // Expo 55 binds --localhost to "localhost", which Node resolves to ::1 first; + // desktop runtimes and Spark's Metro clients connect to 127.0.0.1. + require('node:dns').setDefaultResultOrder('ipv4first'); const patches = preparePatch(process.cwd()); const load = Module._extensions['.js']; Module._extensions['.js'] = function(module, filename) { diff --git a/packages/cli/src/project.ts b/packages/cli/src/project.ts index 0a852b92b..2ee083e1e 100644 --- a/packages/cli/src/project.ts +++ b/packages/cli/src/project.ts @@ -1,6 +1,6 @@ import { assertEnrichedHtmlPins } from "./enriched-html-pins.ts"; import { nativeWindowOptions } from "@legendapp/spark-window-options"; -import { prepareNativePatches } from "@legendapp/spark-desktop-config/native-patches.cjs"; +import { prepareNativePatches, removeExpoApplicationAlias } from "@legendapp/spark-desktop-config/native-patches.cjs"; import { macOSReleaseSettings } from "./macos-release.ts"; import { projectPlatform, architecture, type DesktopPlatform } from "./platform.ts"; import { resolveHelpers } from "./helpers.ts"; @@ -168,6 +168,9 @@ export function nativePackages(root: string, { applyExcludes = true } = {}): Nat const installed = installedPackages(root); prepareNativePatches(root, installed); if (projectPlatform(root) === "windows") return windowsNativePackages(root, installed); + const rootOf = (name: string) => installed.find(pkg => pkg.name === name)?.root; + const [macOS, expoCore] = [rootOf("react-native-macos"), rootOf("expo-modules-core")]; + if (macOS && expoCore) removeExpoApplicationAlias(macOS, expoCore); const excluded: string[] = ["app.json", "desktop.config.json"].some(file => existsSync(path.join(root, file))) ? readAppConfig(root).expo?.autolinking?.exclude ?? [] : []; if (!applyExcludes) { @@ -231,6 +234,17 @@ export function dependencyStamp(root: string) { "react-native.config.js", ]); } +// CocoaPods skips a pod whose podspec lists platforms without macOS, but codegen still registers +// the library's components and modules, mapping them to classes that were never built. +export function lacksMacOSPod(pkg: Package) { + if (!pkg.json.codegenConfig || ["react-native", "react-native-macos"].includes(pkg.name)) return false; + const specs = readdirSync(pkg.root).filter(name => name.endsWith(".podspec")).map(name => readFileSync(path.join(pkg.root, name), "utf8")); + return specs.length > 0 && specs.every(spec => { + // `s.platform = :ios, '13.4'` or `s.platforms = { :ios => ..., osx: ... }`; a computed value proves nothing. + const platforms = spec.match(/\.platforms?\s*=\s*(\{[^}]*\}|.*)/)?.[1] ?? ""; + return /\b(ios|tvos|visionos)\b/.test(platforms) && !/\b(osx|macos)\b/.test(platforms) && !/\.(osx|macos)\./.test(spec); + }); +} export function selection( packages: NativePackage[], used: Set<string>, diff --git a/packages/cli/src/windows-geometry.ts b/packages/cli/src/windows-geometry.ts index f10753e04..e2c4fba70 100644 --- a/packages/cli/src/windows-geometry.ts +++ b/packages/cli/src/windows-geometry.ts @@ -19,7 +19,7 @@ export function patchWindowsGeometry(source: string) { export function prepareWindowsGeometry(root: string) { const req = createRequire(path.join(root, "package.json")); const manifest = req.resolve("react-native-windows/package.json"); - if (JSON.parse(readFileSync(manifest, "utf8")).version !== "0.81.35") throw new Error("spark's Windows geometry adapter requires react-native-windows 0.81.35"); + if (JSON.parse(readFileSync(manifest, "utf8")).version !== "0.83.2") throw new Error("spark's Windows geometry adapter requires react-native-windows 0.83.2"); const rnw = path.dirname(manifest), source = path.join(rnw, "Microsoft.ReactNative/Fabric/ComponentView.h"); const original = readFileSync(source, "utf8"); const patched = patchWindowsGeometry(original); if (patched !== original) writeFileSync(source, patched); diff --git a/packages/cli/src/windows.ts b/packages/cli/src/windows.ts index deaeb7b5f..c90b05aa3 100644 --- a/packages/cli/src/windows.ts +++ b/packages/cli/src/windows.ts @@ -60,7 +60,7 @@ export async function prepareWindows(root: string, mode: "go" | "dev") { const manifest = readFileSync(path.join(root, "package.json"), "utf8"); try { await preserveWindowsBuildOutputs(root, async () => { - await run(root, nodeCommand(root, "expo-desktop", "expo-desktop", ["prebuild", "--platform", "windows", "--template", "expo-desktop-template-bare-minimum@54.81.6", "--no-install"]), { env: { CI: "1" }, capture: true }); + await run(root, nodeCommand(root, "expo-desktop", "expo-desktop", ["prebuild", "--platform", "windows", "--template", "expo-desktop-template-bare-minimum@55.83.10", "--no-install"]), { env: { CI: "1" }, capture: true }); }); // The beta template expands all-platform dependencies. Keep the consumer's installed graph. } finally { diff --git a/packages/cli/templates/blank-typescript/package.json b/packages/cli/templates/blank-typescript/package.json index b876cfd4a..425f45995 100644 --- a/packages/cli/templates/blank-typescript/package.json +++ b/packages/cli/templates/blank-typescript/package.json @@ -14,39 +14,39 @@ }, "dependencies": { "@legendapp/spark": "0.0.1", - "expo": "54.0.37", + "expo": "55.0.32", "expo-desktop": "1.0.6", "expo-desktop-prebuild-config": "1.1.3", "expo-desktop-config-plugins": "1.3.2", - "expo-desktop-metro-config": "54.81.3", - "expo-desktop-template-bare-minimum": "54.81.6", - "react": "19.1.4", - "react-dom": "19.1.4", - "react-native": "0.81.6", - "react-native-macos": "0.81.7", + "expo-desktop-metro-config": "55.83.3", + "expo-desktop-template-bare-minimum": "55.83.10", + "react": "19.2.0", + "react-dom": "19.2.0", + "react-native": "0.83.10", + "react-native-macos": "0.83.0", "@react-native-community/cli": "20.1.3", "typescript": "5.9.3", - "@types/react": "19.1.10", + "@types/react": "19.2.18", "@react-native-runtimes/core": "0.1.0-alpha.2" }, "overrides": { - "expo": "54.0.37", - "@expo/cli": "54.0.27", + "expo": "55.0.32", + "@expo/cli": "55.0.37", "react-native-nitro-modules": "0.37.0", "react-native-webview": "16.0.0", "@op-engineering/op-sqlite": "18.2.1", - "expo-asset": "12.0.13", - "expo-constants": "18.0.14", - "expo-file-system": "19.0.24", - "expo-font": "14.0.12", - "expo-keep-awake": "15.0.8", - "expo-modules-core": "3.0.30", - "expo-desktop-modules-core": "54.0.14", - "expo-desktop-stubs": "54.0.15", - "react": "19.1.4", - "react-native": "0.81.6", - "react-native-macos": "0.81.7", - "react-native-windows": "0.81.35", + "expo-asset": "55.0.20", + "expo-constants": "55.0.17", + "expo-file-system": "55.0.26", + "expo-font": "55.0.8", + "expo-keep-awake": "55.0.8", + "expo-modules-core": "55.0.27", + "expo-desktop-modules-core": "55.0.2", + "expo-desktop-stubs": "55.0.2", + "react": "19.2.0", + "react-native": "0.83.10", + "react-native-macos": "0.83.0", + "react-native-windows": "0.83.2", "postcss": "8.5.31", "dompurify": "3.4.17", "@tiptap/core": "3.32.0", @@ -71,7 +71,7 @@ "@tiptap/extension-bubble-menu": "3.32.0", "@tiptap/extension-floating-menu": "3.32.0", "fast-xml-parser": "5.11.2", - "react-dom": "19.1.4", + "react-dom": "19.2.0", "react-native-enriched-markdown": "1.1.1", "react-native-enriched-html": "1.1.1" }, diff --git a/packages/cli/templates/universal/package.json b/packages/cli/templates/universal/package.json index 48c2d12a5..331519d27 100644 --- a/packages/cli/templates/universal/package.json +++ b/packages/cli/templates/universal/package.json @@ -17,50 +17,50 @@ "postinstall": "node node_modules/@legendapp/spark/init-template.cjs" }, "dependencies": { - "expo": "54.0.37", + "expo": "55.0.32", "expo-desktop": "1.0.6", "expo-desktop-prebuild-config": "1.1.3", "expo-desktop-config-plugins": "1.3.2", - "expo-desktop-metro-config": "54.81.3", - "expo-desktop-template-bare-minimum": "54.81.6", - "react": "19.1.4", - "react-native": "0.81.6", - "react-native-macos": "0.81.7", + "expo-desktop-metro-config": "55.83.3", + "expo-desktop-template-bare-minimum": "55.83.10", + "react": "19.2.0", + "react-native": "0.83.10", + "react-native-macos": "0.83.0", "@react-native-community/cli": "20.1.3", "typescript": "5.9.3", - "@types/react": "19.1.10", - "react-native-windows": "0.81.35", - "expo-desktop-modules-core": "54.0.14", - "expo-desktop-stubs": "54.0.15", - "@expo/ui": "0.2.0-beta.9", - "expo-dev-client": "6.0.21", - "expo-clipboard": "8.0.8", - "expo-secure-store": "15.0.8", - "expo-linking": "8.0.12", - "react-dom": "19.1.4", + "@types/react": "19.2.18", + "react-native-windows": "0.83.2", + "expo-desktop-modules-core": "55.0.2", + "expo-desktop-stubs": "55.0.2", + "@expo/ui": "55.0.17", + "expo-dev-client": "55.0.41", + "expo-clipboard": "55.0.17", + "expo-secure-store": "55.0.18", + "expo-linking": "55.0.17", + "react-dom": "19.2.0", "react-native-web": "0.21.0", "uniwind": "1.6.3", "tailwindcss": "4.2.4", "@legendapp/spark": "0.0.1" }, "overrides": { - "expo": "54.0.37", - "@expo/cli": "54.0.27", + "expo": "55.0.32", + "@expo/cli": "55.0.37", "react-native-nitro-modules": "0.37.0", "react-native-webview": "16.0.0", "@op-engineering/op-sqlite": "18.2.1", - "expo-asset": "12.0.13", - "expo-constants": "18.0.14", - "expo-file-system": "19.0.24", - "expo-font": "14.0.12", - "expo-keep-awake": "15.0.8", - "expo-modules-core": "3.0.30", - "expo-desktop-modules-core": "54.0.14", - "expo-desktop-stubs": "54.0.15", - "react": "19.1.4", - "react-native": "0.81.6", - "react-native-macos": "0.81.7", - "react-native-windows": "0.81.35", + "expo-asset": "55.0.20", + "expo-constants": "55.0.17", + "expo-file-system": "55.0.26", + "expo-font": "55.0.8", + "expo-keep-awake": "55.0.8", + "expo-modules-core": "55.0.27", + "expo-desktop-modules-core": "55.0.2", + "expo-desktop-stubs": "55.0.2", + "react": "19.2.0", + "react-native": "0.83.10", + "react-native-macos": "0.83.0", + "react-native-windows": "0.83.2", "postcss": "8.5.31", "dompurify": "3.4.17", "@tiptap/core": "3.32.0", @@ -85,7 +85,7 @@ "@tiptap/extension-bubble-menu": "3.32.0", "@tiptap/extension-floating-menu": "3.32.0", "fast-xml-parser": "5.11.2", - "react-dom": "19.1.4", + "react-dom": "19.2.0", "react-native-enriched-markdown": "1.1.1", "react-native-enriched-html": "1.1.1" }, diff --git a/packages/cli/templates/windows/package.json b/packages/cli/templates/windows/package.json index 67253e6e0..cc9c242c8 100644 --- a/packages/cli/templates/windows/package.json +++ b/packages/cli/templates/windows/package.json @@ -13,41 +13,41 @@ "postinstall": "node node_modules/@legendapp/spark/init-template.cjs" }, "dependencies": { - "expo": "54.0.37", + "expo": "55.0.32", "expo-desktop": "1.0.6", "expo-desktop-prebuild-config": "1.1.3", "expo-desktop-config-plugins": "1.3.2", - "expo-desktop-metro-config": "54.81.3", - "expo-desktop-template-bare-minimum": "54.81.6", - "react": "19.1.4", - "react-dom": "19.1.4", - "react-native": "0.81.6", + "expo-desktop-metro-config": "55.83.3", + "expo-desktop-template-bare-minimum": "55.83.10", + "react": "19.2.0", + "react-dom": "19.2.0", + "react-native": "0.83.10", "@react-native-community/cli": "20.1.3", "typescript": "5.9.3", - "@types/react": "19.1.10", - "react-native-windows": "0.81.35", - "expo-desktop-modules-core": "54.0.14", - "expo-desktop-stubs": "54.0.15", + "@types/react": "19.2.18", + "react-native-windows": "0.83.2", + "expo-desktop-modules-core": "55.0.2", + "expo-desktop-stubs": "55.0.2", "@legendapp/spark": "0.0.1" }, "overrides": { - "expo": "54.0.37", - "@expo/cli": "54.0.27", + "expo": "55.0.32", + "@expo/cli": "55.0.37", "react-native-nitro-modules": "0.37.0", "react-native-webview": "16.0.0", "@op-engineering/op-sqlite": "18.2.1", - "expo-asset": "12.0.13", - "expo-constants": "18.0.14", - "expo-file-system": "19.0.24", - "expo-font": "14.0.12", - "expo-keep-awake": "15.0.8", - "expo-modules-core": "3.0.30", - "expo-desktop-modules-core": "54.0.14", - "expo-desktop-stubs": "54.0.15", - "react": "19.1.4", - "react-native": "0.81.6", - "react-native-macos": "0.81.7", - "react-native-windows": "0.81.35", + "expo-asset": "55.0.20", + "expo-constants": "55.0.17", + "expo-file-system": "55.0.26", + "expo-font": "55.0.8", + "expo-keep-awake": "55.0.8", + "expo-modules-core": "55.0.27", + "expo-desktop-modules-core": "55.0.2", + "expo-desktop-stubs": "55.0.2", + "react": "19.2.0", + "react-native": "0.83.10", + "react-native-macos": "0.83.0", + "react-native-windows": "0.83.2", "postcss": "8.5.31", "dompurify": "3.4.17", "@tiptap/core": "3.32.0", @@ -72,7 +72,7 @@ "@tiptap/extension-bubble-menu": "3.32.0", "@tiptap/extension-floating-menu": "3.32.0", "fast-xml-parser": "5.11.2", - "react-dom": "19.1.4", + "react-dom": "19.2.0", "react-native-enriched-markdown": "1.1.1", "react-native-enriched-html": "1.1.1" }, diff --git a/packages/clipboard/package.json b/packages/clipboard/package.json index facea6a85..bee86ac35 100644 --- a/packages/clipboard/package.json +++ b/packages/clipboard/package.json @@ -12,9 +12,9 @@ "windows" ], "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", - "expo-clipboard": "~8.0.8" + "react": "^19.2.0", + "react-native": "0.83.10", + "expo-clipboard": "~55.0.17" }, "codegenConfig": { "name": "RNDesktopClipboardSpec", diff --git a/packages/clipboard/src/index.macos.ts b/packages/clipboard/src/index.macos.ts new file mode 100644 index 000000000..9c4641a4b --- /dev/null +++ b/packages/clipboard/src/index.macos.ts @@ -0,0 +1 @@ +export * from "./desktop"; diff --git a/packages/codex/package.json b/packages/codex/package.json index 0238ea4a2..839ab28ef 100644 --- a/packages/codex/package.json +++ b/packages/codex/package.json @@ -5,8 +5,8 @@ "main": "src/index.ts", "react-native": "src/index.ts", "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6" + "react": "^19.2.0", + "react-native": "0.83.10" }, "license": "MIT", "dependencies": { diff --git a/packages/commands/package.json b/packages/commands/package.json index 23f6fc431..bcf3a34b6 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -15,7 +15,7 @@ "@legendapp/spark-desktop-app": "0.0.1" }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6" + "react": "^19.2.0", + "react-native": "0.83.10" } } diff --git a/packages/config-plugin/accessibility-state.cjs b/packages/config-plugin/accessibility-state.cjs index 0310c5b1e..eebc8cd50 100644 --- a/packages/config-plugin/accessibility-state.cjs +++ b/packages/config-plugin/accessibility-state.cjs @@ -1,4 +1,4 @@ -// RN macOS 0.81.7 Fabric applies accessibilityState only on iOS: RCTViewComponentView's accessibilityState block is +// RN macOS 0.83.0 Fabric applies accessibilityState only on iOS: RCTViewComponentView's accessibilityState block is // inside #if !TARGET_OS_OSX, so a disabled Pressable with an accessibilityRole reports itself enabled to VoiceOver and a // selected one unselected. A view whose accessibility traits never changed (no accessibilityRole) reported // isAccessibilityEnabled NO, as a plain NSView does, enabled or not. The patch answers both from the view's current @@ -38,7 +38,7 @@ function patchAccessibilityStateSource(source) { function installAccessibilityStatePatch(root) { const manifest = require.resolve("react-native-macos/package.json", { paths: [root] }); const version = JSON.parse(fs.readFileSync(manifest, "utf8")).version; - if (version !== "0.81.7") throw new Error(`spark's accessibility state patch requires react-native-macos@0.81.7; found ${version}.`); + if (version !== "0.83.0") throw new Error(`spark's accessibility state patch requires react-native-macos@0.83.0; found ${version}.`); const target = path.join(path.dirname(manifest), file); const source = fs.readFileSync(target, "utf8"), patched = patchAccessibilityStateSource(source); if (source !== patched) { diff --git a/packages/config-plugin/app.plugin.cjs b/packages/config-plugin/app.plugin.cjs index ba0831147..2ea1129d1 100644 --- a/packages/config-plugin/app.plugin.cjs +++ b/packages/config-plugin/app.plugin.cjs @@ -8,6 +8,8 @@ const { withEntitlementsPlist, withXcodeProject, } = require("expo-desktop-config-plugins"); +const { withMod } = require("@expo/config-plugins"); +const { installHostFiles, installHostProject } = require("./macos-host.cjs"); const { identity } = require("./identity.cjs"); const { resolveEntitlements } = require("./entitlements.cjs"); const { version: sparkVersion } = require("@legendapp/spark/package.json"); @@ -33,6 +35,15 @@ module.exports = function withSparkDesktop(config) { mod.modResults = resolveEntitlements({ macos: { entitlements: declaredEntitlements } }, packages); return mod; }); + // Expo Desktop 55's template hosts React Native in a SwiftUI App; Spark keeps its AppKit host. + config = withMod(config, { platform: "macos", mod: "dangerous", action: (mod) => { + installHostFiles(mod.modRequest.platformProjectRoot); + return mod; + } }); + config = withXcodeProject(config, (mod) => { + installHostProject(mod.modResults); + return mod; + }); config = withAppDelegate(config, (mod) => { // Own this adapter; application customizations belong in configuration/plugins. mod.modResults.contents = fs.readFileSync( @@ -56,6 +67,7 @@ module.exports = function withSparkDesktop(config) { if (!(key in generated)) delete mod.modResults[key]; Object.assign(mod.modResults, generated); mod.modResults.SparkFrameworkVersion = sparkVersion; + mod.modResults.NSMainStoryboardFile = "Main"; mod.modResults.NSAppTransportSecurity = { NSAllowsLocalNetworking: true }; return mod; }); diff --git a/packages/config-plugin/autolinking.cjs b/packages/config-plugin/autolinking.cjs index a46ea1426..57ccc8e70 100644 --- a/packages/config-plugin/autolinking.cjs +++ b/packages/config-plugin/autolinking.cjs @@ -1,4 +1,4 @@ -// Expo 54's file scanner ignores Bun file: dependencies' symlinked podspecs. +// Expo's file scanner ignores Bun file: dependencies' symlinked podspecs. const fs = require("node:fs"); const path = require("node:path"); const before = ` return (await fs_1.default.promises.readdir(targetPath, { withFileTypes: true })) @@ -22,7 +22,7 @@ function patchAutolinkingSource(source) { function installAutolinkingPatch(root) { const manifest = require.resolve("expo-modules-autolinking/package.json", { paths: [root] }); const version = JSON.parse(fs.readFileSync(manifest, "utf8")).version; - if (version !== "3.0.27") throw new Error(`spark's autolinking patch requires expo-modules-autolinking@3.0.27; found ${version}.`); + if (version !== "55.0.28") throw new Error(`spark's autolinking patch requires expo-modules-autolinking@55.0.28; found ${version}.`); const file = path.join(path.dirname(manifest), "build/utils.js"); const source = fs.readFileSync(file, "utf8"); const patched = patchAutolinkingSource(source); @@ -32,11 +32,12 @@ function installAutolinkingPatch(root) { fs.renameSync(temporary, file); // Do not modify package-cache hardlinks. } } -// Expo autolinking ignores an app-level `ios: null` override when the package's own -// react-native.config.js declares iOS, so pass the native selection's exclusions to its CLI. +// Packages such as Codex declare iOS for the desktop autolinker in their own +// react-native.config.js, so pass the native selection's exclusions to its CLI. function excludeNativeModules(podfile, excluded) { - const call = /use_native_modules!\(config_command(?: \+ \[[^\]]*\])?\)/; + const call = /use_native_modules!\(config_command(?: \+ \[[^\]]*\])*\)/; if (!call.test(podfile)) throw new Error("The macOS Podfile no longer calls use_native_modules!(config_command); review spark's native selection."); - return podfile.replace(call, `use_native_modules!(config_command + ${JSON.stringify(excluded.flatMap(name => ["--exclude", name]))})`); + // RN writes the filtered codegen input beneath sourceDir; Expo's iOS default points at the wrong tree. + return podfile.replace(call, `use_native_modules!(config_command + ["--source-dir", __dir__] + ${JSON.stringify(excluded.flatMap(name => ["--exclude", name]))})`); } module.exports = { patchAutolinkingSource, installAutolinkingPatch, excludeNativeModules }; diff --git a/packages/config-plugin/fabric-lifecycle.cjs b/packages/config-plugin/fabric-lifecycle.cjs index 44c7a247b..f6cdbb7cf 100644 --- a/packages/config-plugin/fabric-lifecycle.cjs +++ b/packages/config-plugin/fabric-lifecycle.cjs @@ -112,7 +112,7 @@ function patchSurface(source) { function installSurfaceLifecyclePatch(root) { const manifest = require.resolve("react-native-macos/package.json", { paths: [root] }); const version = JSON.parse(fs.readFileSync(manifest, "utf8")).version; - if (version !== "0.81.7") throw new Error(`spark's Fabric lifecycle patch requires react-native-macos@0.81.7; found ${version}.`); + if (version !== "0.83.0") throw new Error(`spark's Fabric lifecycle patch requires react-native-macos@0.83.0; found ${version}.`); const file = path.join(path.dirname(manifest), "React/Fabric/Surface/RCTFabricSurface.mm"); const before = fs.readFileSync(file, "utf8"), after = patchSurface(before); if (before !== after) { diff --git a/packages/config-plugin/keyboard-events.cjs b/packages/config-plugin/keyboard-events.cjs index 023c65933..8203d37e9 100644 --- a/packages/config-plugin/keyboard-events.cjs +++ b/packages/config-plugin/keyboard-events.cjs @@ -1,4 +1,4 @@ -// RN macOS 0.81.7 legacy views embedded in Fabric can have no React tag. +// RN macOS 0.83.0 legacy views embedded in Fabric can have no React tag. const fs = require("node:fs"); const path = require("node:path"); const replacements = { @@ -28,7 +28,7 @@ function patchKeyboardSource(file, source) { function installKeyboardEventsPatch(root) { const manifest = require.resolve("react-native-macos/package.json", { paths: [root] }); const version = JSON.parse(fs.readFileSync(manifest, "utf8")).version; - if (version !== "0.81.7") throw new Error(`spark's keyboard patch requires react-native-macos@0.81.7; found ${version}.`); + if (version !== "0.83.0") throw new Error(`spark's keyboard patch requires react-native-macos@0.83.0; found ${version}.`); // Validate all targets before changing either file. const changes = Object.keys(replacements).map(relative => { const file = path.join(path.dirname(manifest), relative); diff --git a/packages/config-plugin/macos-host.cjs b/packages/config-plugin/macos-host.cjs new file mode 100644 index 000000000..4e9bf8066 --- /dev/null +++ b/packages/config-plugin/macos-host.cjs @@ -0,0 +1,39 @@ +// Expo Desktop 55's template hosts React Native in a SwiftUI App. Spark keeps its AppKit +// host: AppDelegate.mm replaces the Swift delegate, with main.m and the menu storyboard. +const fs = require("node:fs"); +const path = require("node:path"); +const { IOSConfig } = require("@expo/config-plugins"); + +function installHostFiles(platformRoot) { + for (const swift of fs.readdirSync(platformRoot).map((name) => path.join(platformRoot, name, "AppDelegate.swift")).filter((file) => fs.existsSync(file))) { + for (const name of ["AppDelegate.h", "main.m", "Main.storyboard"]) + fs.copyFileSync(require.resolve(`@legendapp/spark-desktop-host/macos/${name}`), path.join(path.dirname(swift), name)); + // Upstream AppDelegate mods expect an Objective-C delegate; Spark's withAppDelegate owns its final contents. + fs.copyFileSync(require.resolve("@legendapp/spark-desktop-host/AppDelegate.mm"), path.join(path.dirname(swift), "AppDelegate.mm")); + fs.rmSync(swift); + } +} + +function installHostProject(project) { + const objects = project.hash.project.objects, references = project.pbxFileReferenceSection(), builds = project.pbxBuildFileSection(); + const key = Object.keys(references).find((id) => references[id].path === "AppDelegate.swift"); + if (!key) return project; + Object.assign(references[key], { path: "AppDelegate.mm", lastKnownFileType: "sourcecode.cpp.objcpp" }); + references[`${key}_comment`] = "AppDelegate.mm"; + const build = Object.keys(builds).find((id) => builds[id].fileRef === key); + Object.assign(builds[build], { fileRef_comment: "AppDelegate.mm" }); + builds[`${build}_comment`] = "AppDelegate.mm in Sources"; + const group = Object.values(objects.PBXGroup).find((entry) => entry.children?.some((child) => child.value === key)); + group.children.find((child) => child.value === key).comment = "AppDelegate.mm"; + for (const phase of Object.values(objects.PBXSourcesBuildPhase)) for (const file of phase.files ?? []) if (file.value === build) file.comment = "AppDelegate.mm in Sources"; + // The template also has an iOS target; add the host to the target that compiled the Swift delegate. + const targetUuid = Object.keys(objects.PBXNativeTarget).find((id) => objects.PBXNativeTarget[id].buildPhases?.some((phase) => + objects.PBXSourcesBuildPhase[phase.value]?.files?.some((file) => file.value === build))); + IOSConfig.XcodeUtils.addBuildSourceFileToGroup({ filepath: "main.m", groupName: group.path.replaceAll('"', ""), project, targetUuid }); + IOSConfig.XcodeUtils.addResourceFileToGroup({ filepath: "Main.storyboard", groupName: group.path.replaceAll('"', ""), project, isBuildFile: true, targetUuid }); + const storyboard = Object.keys(references).find((id) => references[id].path === '"Main.storyboard"'); + references[storyboard].lastKnownFileType = "file.storyboard"; + return project; +} + +module.exports = { installHostFiles, installHostProject }; diff --git a/packages/config-plugin/native-patches.cjs b/packages/config-plugin/native-patches.cjs index f1104f25b..97e98f3ec 100644 --- a/packages/config-plugin/native-patches.cjs +++ b/packages/config-plugin/native-patches.cjs @@ -10,6 +10,17 @@ function readManifest(file) { return JSON.parse(fs.readFileSync(file, "utf8")); } +// Find a package's manifest even when its exports map hides package.json, as Node finds the package. +function resolveManifest(req, name) { + try { return req.resolve(`${name}/package.json`); } + catch (error) { + if (error.code !== "ERR_PACKAGE_PATH_NOT_EXPORTED") throw error; + const file = (req.resolve.paths(name) ?? []).map(directory => path.join(directory, name, "package.json")).find(fs.existsSync); + if (!file) throw error; + return file; + } +} + function prepareNativePatches(root, installed = []) { const manifestFile = path.join(root, "package.json"); if (!fs.existsSync(manifestFile)) return {}; @@ -51,7 +62,7 @@ function prepareNativePatches(root, installed = []) { for (const name of active) { if (optionalOnly && name !== "react-native-nitro-modules" && !cameraPackages.includes(name) && !optionalBackends.includes(name)) continue; let file; - try { file = req.resolve(`${name}/package.json`); } + try { file = resolveManifest(req, name); } catch (error) { if (error.code !== "MODULE_NOT_FOUND") throw error; continue; @@ -60,10 +71,14 @@ function prepareNativePatches(root, installed = []) { const packageRoot = fs.realpathSync(path.dirname(file)); if (visited.has(packageRoot)) continue; visited.add(packageRoot); - packages[name] = { root: packageRoot }; - queue.push({ req: createRequire(file), optionalOnly }); const manifest = JSON.parse(fs.readFileSync(file, "utf8")); const pin = pins[name]; + if (pin.optional && manifest.version !== pin.version) { + console.warn(`Spark's desktop patch for ${name} needs ${pin.version}; found ${manifest.version}, so it stays unpatched.`); + continue; + } + packages[name] = { root: packageRoot }; + queue.push({ req: createRequire(file), optionalOnly }); if (manifest.version !== pin.version) { const owner = readManifest(req.resolve("./package.json")); throw new Error(`Spark needs ${name}@${pin.version}; found ${manifest.version} at ${packageRoot}, required by ${owner.name ?? root}. Add an override/resolution for the pinned version.`); @@ -82,12 +97,15 @@ function prepareNativePatches(root, installed = []) { writes.push([target, next]); continue; } - if (relative === "package.json" && manifest.spark?.dependencyPatch) { + // The receipt rewrites package.json with two-space JSON, so its patch hunks target that form. + if (relative === "package.json") { const original = JSON.parse(current); - delete original.spark.dependencyPatch; - delete original.spark.sdk; - delete original.spark.requires; - if (!Object.keys(original.spark).length) delete original.spark; + if (manifest.spark?.dependencyPatch) { + delete original.spark.dependencyPatch; + delete original.spark.sdk; + delete original.spark.requires; + if (!Object.keys(original.spark).length) delete original.spark; + } current = JSON.stringify(original, null, 2) + "\n"; } if (applyPatch(current, reversePatch(patch)) !== false) continue; @@ -103,7 +121,7 @@ function prepareNativePatches(root, installed = []) { } const patched = JSON.parse(fs.readFileSync(file, "utf8")); patched.spark ??= {}; - if (name !== "react-native-nitro-modules") { + if (name !== "react-native-nitro-modules" && !pin.optional) { patched.spark.sdk = true; patched.spark.requires = pin.requires ?? []; } @@ -119,9 +137,23 @@ function prepareNativePatches(root, installed = []) { } } for (const name of active) { - if (!packages[name]) throw new Error(`Spark needs ${name}@${pins[name].version}. Finish installing dependencies before native preparation.`); + if (!packages[name] && !pins[name].optional) throw new Error(`Spark needs ${name}@${pins[name].version}. Finish installing dependencies before native preparation.`); } return packages; } -module.exports = { prepareNativePatches }; +// RN macOS 0.83 declares UIApplication itself, so Expo's guarded duplicate breaks the build. +// The 55 template's Podfile removes it during pod install, after Spark fingerprints +// expo-modules-core; remove it first so the native fingerprint stays stable. +const applicationAlias = "#ifndef UIApplication\n@compatibility_alias UIApplication NSApplication;\n#endif"; +function removeExpoApplicationAlias(rn, expo) { + const file = path.join(expo, "ios/Platform/Platform.h"), compat = path.join(rn, "ReactApple/Libraries/RCTUIKit/RCTUIKitCompat.h"); + if (!fs.existsSync(file) || !fs.existsSync(compat) || !fs.readFileSync(compat, "utf8").includes("@compatibility_alias UIApplication NSApplication;")) return; + const source = fs.readFileSync(file, "utf8"); + if (!source.includes(applicationAlias)) return; + const temporary = `${file}.spark-${process.pid}.tmp`; + fs.writeFileSync(temporary, source.replace(applicationAlias, "")); + fs.renameSync(temporary, file); // Preserve package-manager cache hardlinks. +} + +module.exports = { prepareNativePatches, removeExpoApplicationAlias }; diff --git a/packages/config-plugin/native-patches.d.cts b/packages/config-plugin/native-patches.d.cts index f655dacd2..e374c6918 100644 --- a/packages/config-plugin/native-patches.d.cts +++ b/packages/config-plugin/native-patches.d.cts @@ -1 +1,2 @@ export function prepareNativePatches(root: string, installed?: { name: string; root: string; json: any }[]): Record<string, { root: string }>; +export function removeExpoApplicationAlias(reactNativeMacOSRoot: string, expoModulesCoreRoot: string): void; diff --git a/packages/config-plugin/package.json b/packages/config-plugin/package.json index 4d5cacc5a..66674751c 100644 --- a/packages/config-plugin/package.json +++ b/packages/config-plugin/package.json @@ -13,11 +13,11 @@ "expo-desktop-config-plugins": "1.3.2", "@legendapp/spark-desktop-host": "0.0.1", "@legendapp/spark-window-options": "0.0.1", - "@expo/config-plugins": "54.0.5", + "@expo/config-plugins": "55.0.12", "diff": "8.0.4" }, "private": true, "devDependencies": { - "@expo/config-types": "54.0.10" + "@expo/config-types": "55.0.6" } } diff --git a/packages/config-plugin/rtl-scroll-view.cjs b/packages/config-plugin/rtl-scroll-view.cjs index 20709493f..eb1202d3f 100644 --- a/packages/config-plugin/rtl-scroll-view.cjs +++ b/packages/config-plugin/rtl-scroll-view.cjs @@ -1,4 +1,4 @@ -// RN macOS 0.81.7 Fabric mirrors ScrollView content in RTL: it flips the content container with a +// RN macOS 0.83.0 Fabric mirrors ScrollView content in RTL: it flips the content container with a // layer transform, but the scroll view flip that cancels it on iOS is iOS-only. AppKit ignores layer // transforms when it scrolls NSClipView and routes events, so macOS flips nothing (Yoga already // places RTL content), reports offsets unconverted, and anchors RTL content to its right end. @@ -132,7 +132,7 @@ function patchScrollViewSource(source) { function installRTLScrollViewPatch(root) { const manifest = require.resolve("react-native-macos/package.json", { paths: [root] }); const version = JSON.parse(fs.readFileSync(manifest, "utf8")).version; - if (version !== "0.81.7") throw new Error(`spark's RTL scroll patch requires react-native-macos@0.81.7; found ${version}.`); + if (version !== "0.83.0") throw new Error(`spark's RTL scroll patch requires react-native-macos@0.83.0; found ${version}.`); const target = path.join(path.dirname(manifest), file); const before = fs.readFileSync(target, "utf8"), after = patchScrollViewSource(before); if (before !== after) { diff --git a/packages/config-plugin/rtl-text-alignment.cjs b/packages/config-plugin/rtl-text-alignment.cjs index de35e0f4f..372b0b357 100644 --- a/packages/config-plugin/rtl-text-alignment.cjs +++ b/packages/config-plugin/rtl-text-alignment.cjs @@ -1,4 +1,4 @@ -// RN macOS 0.81.7 Fabric leaves natural ("auto" or unset) textAlign as NSTextAlignmentNatural. TextKit resolves that +// RN macOS 0.83.0 Fabric leaves natural ("auto" or unset) textAlign as NSTextAlignmentNatural. TextKit resolves that // from the user's language, not the Yoga layout direction, so text in an RTL subtree of an LTR-localized app aligns // left. The patch resolves natural alignment from the layout direction. "left" and "right" keep the upstream // behavior, shared with iOS: they swap in RTL. @@ -48,7 +48,7 @@ function patchTextAlignmentSource(source) { function installRTLTextAlignmentPatch(root) { const manifest = require.resolve("react-native-macos/package.json", { paths: [root] }); const version = JSON.parse(fs.readFileSync(manifest, "utf8")).version; - if (version !== "0.81.7") throw new Error(`spark's RTL text alignment patch requires react-native-macos@0.81.7; found ${version}.`); + if (version !== "0.83.0") throw new Error(`spark's RTL text alignment patch requires react-native-macos@0.83.0; found ${version}.`); const target = path.join(path.dirname(manifest), file); const source = fs.readFileSync(target, "utf8"), patched = patchTextAlignmentSource(source); if (source !== patched) { diff --git a/packages/config-plugin/rtl-text-input-alignment.cjs b/packages/config-plugin/rtl-text-input-alignment.cjs index 889153951..501a9db21 100644 --- a/packages/config-plugin/rtl-text-input-alignment.cjs +++ b/packages/config-plugin/rtl-text-input-alignment.cjs @@ -1,4 +1,4 @@ -// RN macOS 0.81.7 Fabric gives a TextInput no layout direction: its text attributes have none, and the iOS +// RN macOS 0.83.0 Fabric gives a TextInput no layout direction: its text attributes have none, and the iOS // semanticContentAttribute that makes UIKit's natural alignment follow it isn't set on macOS. TextKit resolves // NSTextAlignmentNatural from the user's language, so in an RTL layout a TextInput with natural ("auto" or unset) // textAlign draws its text and placeholder from the left. The patch aligns them to the leading edge of the input's @@ -100,7 +100,7 @@ function patchTextInputAlignmentSource(source) { function installRTLTextInputAlignmentPatch(root) { const manifest = require.resolve("react-native-macos/package.json", { paths: [root] }); const version = JSON.parse(fs.readFileSync(manifest, "utf8")).version; - if (version !== "0.81.7") throw new Error(`spark's RTL TextInput alignment patch requires react-native-macos@0.81.7; found ${version}.`); + if (version !== "0.83.0") throw new Error(`spark's RTL TextInput alignment patch requires react-native-macos@0.83.0; found ${version}.`); const target = path.join(path.dirname(manifest), file); const source = fs.readFileSync(target, "utf8"), patched = patchTextInputAlignmentSource(source); if (source !== patched) { diff --git a/packages/config-plugin/text-input-accessibility.cjs b/packages/config-plugin/text-input-accessibility.cjs index b65596a64..af0d3b3ec 100644 --- a/packages/config-plugin/text-input-accessibility.cjs +++ b/packages/config-plugin/text-input-accessibility.cjs @@ -1,4 +1,4 @@ -// RN macOS 0.81.7 Fabric sets a TextInput's accessibilityLabel on its accessibilityElement, the backed text input view. +// RN macOS 0.83.0 Fabric sets a TextInput's accessibilityLabel on its accessibilityElement, the backed text input view. // A multiline input's backed view is RCTWrappedTextView, a plain view around a scroll view, while the NSTextView inside // it is the accessibility element, so VoiceOver announces a labelled multiline input without its label. The patch // keeps the label on that text view. @@ -35,7 +35,7 @@ function patchTextInputAccessibilitySource(source) { function installTextInputAccessibilityPatch(root) { const manifest = require.resolve("react-native-macos/package.json", { paths: [root] }); const version = JSON.parse(fs.readFileSync(manifest, "utf8")).version; - if (version !== "0.81.7") throw new Error(`spark's TextInput accessibility patch requires react-native-macos@0.81.7; found ${version}.`); + if (version !== "0.83.0") throw new Error(`spark's TextInput accessibility patch requires react-native-macos@0.83.0; found ${version}.`); const target = path.join(path.dirname(manifest), file); const source = fs.readFileSync(target, "utf8"), patched = patchTextInputAccessibilitySource(source); if (source !== patched) { diff --git a/packages/context-menu/package.json b/packages/context-menu/package.json index 675b7b331..505b38af8 100644 --- a/packages/context-menu/package.json +++ b/packages/context-menu/package.json @@ -12,8 +12,8 @@ "react-native.config.js" ], "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6" + "react": "^19.2.0", + "react-native": "0.83.10" }, "codegenConfig": { "name": "RNContextMenuSpec", diff --git a/packages/desktop-app/macos/SparkTestDriver.mm b/packages/desktop-app/macos/SparkTestDriver.mm index 5504df4ed..880e545f1 100644 --- a/packages/desktop-app/macos/SparkTestDriver.mm +++ b/packages/desktop-app/macos/SparkTestDriver.mm @@ -1,4 +1,5 @@ #import "SparkTestDriver.h" +#import <CoreGraphics/CoreGraphics.h> #import <QuartzCore/QuartzCore.h> #include <atomic> #include <fcntl.h> @@ -17,6 +18,8 @@ static NSString *socketPath; static NSData *token; static id activity; +static NSMutableArray<dispatch_block_t> *pointerRestores; +static NSInteger pointerEventNumber = 0; static std::atomic<bool> authenticated{false}; static CFTimeInterval lastMount = 0; static const CFTimeInterval SparkSettleInterval = 0.1; @@ -30,7 +33,7 @@ static void SparkDriverRefuse(NSString *message) { BOOL SparkTestDriverActive(void) { return active; } BOOL SparkTestDriverEnding(void) { return ending; } void SparkTestDriverDidMount(void) { lastMount = CACurrentMediaTime(); } -static NSArray<NSString *> *const SparkBuiltInCommands = @[@"ping", @"navigate", @"openFile", @"waitFor", @"setAppAppearance", @"capture", @"quit"]; +static NSArray<NSString *> *const SparkBuiltInCommands = @[@"ping", @"navigate", @"openFile", @"waitFor", @"setAppAppearance", @"capture", @"pointerEvents", @"restorePointer", @"quit"]; static NSMutableDictionary<NSString *, SparkTestDriverHandler> *SparkModuleCommands(void) { static NSMutableDictionary *commands; static dispatch_once_t once; @@ -228,6 +231,63 @@ static void SparkOpenURL(NSURL *url, NSDictionary *request, SparkDriverReply rep reply(@{ @"ok": @YES }); }); } +static void SparkRestorePointer(void) { + for (dispatch_block_t restore in pointerRestores.reverseObjectEnumerator) restore(); + pointerRestores = nil; +} +static void SparkPreparePointer(NSWindow *window, NSNumber *x, NSNumber *y, SparkDriverReply reply) { + if (!pointerRestores) { + pointerRestores = [NSMutableArray new]; + NSApplicationActivationPolicy policy = NSApp.activationPolicy; + NSRunningApplication *previous = NSWorkspace.sharedWorkspace.frontmostApplication; + [pointerRestores addObject:[^{ + [NSApp setActivationPolicy:policy]; + if (previous.processIdentifier != getpid()) [previous activateWithOptions:0]; + } copy]]; + if (policy == NSApplicationActivationPolicyProhibited) [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + } + // Pointer input opts into foreground clicks; ordinary driver windows stay hidden. + if (!window.isVisible) { + NSWindowAnimationBehavior animation = window.animationBehavior; + [pointerRestores addObject:[^{ [window orderOut:nil]; window.animationBehavior = animation; } copy]]; + window.animationBehavior = NSWindowAnimationBehaviorNone; + } + [window makeKeyAndOrderFront:nil]; + if (!NSApp.isActive) [NSApp activateIgnoringOtherApps:YES]; + [window layoutIfNeeded]; + NSView *root = SparkFrameView(window); + if ([x doubleValue] < 0 || [y doubleValue] < 0 || [x doubleValue] >= NSWidth(root.bounds) || [y doubleValue] >= NSHeight(root.bounds)) { + reply(SparkFailure(@"E_INVALID_ARGUMENT", @"pointerEvents expects x,y inside the captured window frame (top-left points)")); return; + } + NSPoint local = NSMakePoint(NSMinX(root.bounds) + [x doubleValue], root.isFlipped ? NSMinY(root.bounds) + [y doubleValue] : NSMaxY(root.bounds) - [y doubleValue]); + NSPoint windowPoint = [root convertPoint:local toView:nil]; + NSMutableArray<NSString *> *events = [NSMutableArray new]; + NSTimeInterval now = NSProcessInfo.processInfo.systemUptime; + NSInteger eventNumber = ++pointerEventNumber; + for (NSUInteger i = 0; i < 2; i++) { + NSEvent *event = [NSEvent mouseEventWithType:i == 0 ? NSEventTypeLeftMouseDown : NSEventTypeLeftMouseUp location:windowPoint modifierFlags:0 timestamp:now + i * 0.05 windowNumber:window.windowNumber context:nil eventNumber:eventNumber clickCount:1 pressure:i == 0 ? 1 : 0]; + NSData *data = event.CGEvent ? CFBridgingRelease(CGEventCreateData(NULL, event.CGEvent)) : nil; + if (!data) { reply(SparkFailure(@"E_EVENT", @"Could not encode mouse events for the target window")); return; } + [events addObject:[data base64EncodedStringWithOptions:0]]; + } + reply(@{ @"ok": @YES, @"pid": @(getpid()), @"down": events[0], @"up": events[1] }); +} +static void SparkPointerEvents(NSDictionary *request, SparkDriverReply reply) { + NSString *failure = nil; + NSWindow *window = SparkResolveWindow(request[@"window"], &failure); + if (!window) { reply(SparkFailure(@"E_NO_WINDOW", failure)); return; } + id x = request[@"x"], y = request[@"y"]; + if (![x isKindOfClass:NSNumber.class] || ![y isKindOfClass:NSNumber.class] || + !isfinite([x doubleValue]) || !isfinite([y doubleValue])) { + reply(SparkFailure(@"E_INVALID_ARGUMENT", @"pointerEvents expects finite x,y")); return; + } + // Gesture recognizers attach after their views mount; do not click mid-mount. A zero timeout skips the wait. + NSTimeInterval timeout = SparkTimeout(request[@"timeoutMs"], 5); + SparkWaitUntil(timeout, ^{ return (BOOL)(timeout == 0 || SparkSettled()); }, ^(BOOL settled) { + if (!settled) { reply(SparkFailure(@"E_NOT_SETTLED", @"The UI kept mounting changes until the timeout")); return; } + SparkPreparePointer(window, x, y, reply); + }); +} static void SparkExecute(NSDictionary *request, SparkDriverReply reply) { NSString *command = request[@"command"]; if ([command isEqual:@"ping"]) reply(@{ @"ok": @YES }); @@ -261,6 +321,8 @@ static void SparkExecute(NSDictionary *request, SparkDriverReply reply) { } else if ([command isEqual:@"setAppAppearance"]) SparkSetAppearance(request, reply); else if ([command isEqual:@"capture"]) SparkCapture(request, reply); + else if ([command isEqual:@"pointerEvents"]) SparkPointerEvents(request, reply); + else if ([command isEqual:@"restorePointer"]) { SparkRestorePointer(); reply(@{ @"ok": @YES }); } else if ([command isKindOfClass:NSString.class] && SparkModuleCommands()[command]) SparkModuleCommands()[command](request, reply); else reply(SparkFailure(@"E_UNKNOWN_COMMAND", [NSString stringWithFormat:@"Unknown command %@", command])); } @@ -318,7 +380,7 @@ static void SparkServe(void) { } close(listenFD); unlink(socketPath.fileSystemRepresentation); - dispatch_async(dispatch_get_main_queue(), ^{ SparkDriverTerminate(); }); + dispatch_async(dispatch_get_main_queue(), ^{ SparkRestorePointer(); SparkDriverTerminate(); }); } BOOL SparkTestDriverStart(void) { diff --git a/packages/desktop-app/package.json b/packages/desktop-app/package.json index 7bf269c96..b62a4030f 100644 --- a/packages/desktop-app/package.json +++ b/packages/desktop-app/package.json @@ -12,8 +12,8 @@ "cpp" ], "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6" + "react": "^19.2.0", + "react-native": "0.83.10" }, "codegenConfig": { "name": "RNDesktopAppSpec", diff --git a/packages/desktop-host/macos/AppDelegate.h b/packages/desktop-host/macos/AppDelegate.h new file mode 100644 index 000000000..63db97316 --- /dev/null +++ b/packages/desktop-host/macos/AppDelegate.h @@ -0,0 +1,6 @@ +#import <RCTAppDelegate.h> +#import <Cocoa/Cocoa.h> + +@interface AppDelegate : RCTAppDelegate + +@end diff --git a/packages/desktop-host/macos/Main.storyboard b/packages/desktop-host/macos/Main.storyboard new file mode 100644 index 000000000..a3afa65f0 --- /dev/null +++ b/packages/desktop-host/macos/Main.storyboard @@ -0,0 +1,684 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES"> + <dependencies> + <deployment identifier="macosx"/> + <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21701"/> + </dependencies> + <scenes> + <!--Application--> + <scene sceneID="JPo-4y-FX3"> + <objects> + <application id="hnw-xV-0zn" sceneMemberID="viewController"> + <menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6"> + <items> + <menuItem title="HelloWorld" id="1Xt-HY-uBw"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="HelloWorld" systemMenu="apple" id="uQy-DD-JDr"> + <items> + <menuItem title="About HelloWorld" id="5kV-Vb-QxS"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/> + <menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/> + <menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/> + <menuItem title="Services" id="NMo-om-nkz"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/> + </menuItem> + <menuItem isSeparatorItem="YES" id="4je-JR-u6R"/> + <menuItem title="Hide HelloWorld" keyEquivalent="h" id="Olw-nP-bQN"> + <connections> + <action selector="hide:" target="Ady-hI-5gd" id="PnN-Uc-m68"/> + </connections> + </menuItem> + <menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + <connections> + <action selector="hideOtherApplications:" target="Ady-hI-5gd" id="VT4-aY-XCT"/> + </connections> + </menuItem> + <menuItem title="Show All" id="Kd2-mp-pUS"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="unhideAllApplications:" target="Ady-hI-5gd" id="Dhg-Le-xox"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/> + <menuItem title="Quit HelloWorld" keyEquivalent="q" id="4sb-4s-VLi"> + <connections> + <action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="File" id="dMs-cI-mzQ"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="File" id="bib-Uj-vzu"> + <items> + <menuItem title="New" keyEquivalent="n" id="Was-JA-tGl"> + <connections> + <action selector="newDocument:" target="Ady-hI-5gd" id="4Si-XN-c54"/> + </connections> + </menuItem> + <menuItem title="Open…" keyEquivalent="o" id="IAo-SY-fd9"> + <connections> + <action selector="openDocument:" target="Ady-hI-5gd" id="bVn-NM-KNZ"/> + </connections> + </menuItem> + <menuItem title="Open Recent" id="tXI-mr-wws"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Open Recent" systemMenu="recentDocuments" id="oas-Oc-fiZ"> + <items> + <menuItem title="Clear Menu" id="vNY-rz-j42"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="clearRecentDocuments:" target="Ady-hI-5gd" id="Daa-9d-B3U"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem isSeparatorItem="YES" id="m54-Is-iLE"/> + <menuItem title="Close" keyEquivalent="w" id="DVo-aG-piG"> + <connections> + <action selector="performClose:" target="Ady-hI-5gd" id="HmO-Ls-i7Q"/> + </connections> + </menuItem> + <menuItem title="Save…" keyEquivalent="s" id="pxx-59-PXV"> + <connections> + <action selector="saveDocument:" target="Ady-hI-5gd" id="teZ-XB-qJY"/> + </connections> + </menuItem> + <menuItem title="Save As…" keyEquivalent="S" id="Bw7-FT-i3A"> + <connections> + <action selector="saveDocumentAs:" target="Ady-hI-5gd" id="mDf-zr-I0C"/> + </connections> + </menuItem> + <menuItem title="Revert to Saved" keyEquivalent="r" id="KaW-ft-85H"> + <connections> + <action selector="revertDocumentToSaved:" target="Ady-hI-5gd" id="iJ3-Pv-kwq"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="aJh-i4-bef"/> + <menuItem title="Page Setup…" keyEquivalent="P" id="qIS-W8-SiK"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + <connections> + <action selector="runPageLayout:" target="Ady-hI-5gd" id="Din-rz-gC5"/> + </connections> + </menuItem> + <menuItem title="Print…" keyEquivalent="p" id="aTl-1u-JFS"> + <connections> + <action selector="print:" target="Ady-hI-5gd" id="qaZ-4w-aoO"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Edit" id="5QF-Oa-p0T"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Edit" id="W48-6f-4Dl"> + <items> + <menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg"> + <connections> + <action selector="undo:" target="Ady-hI-5gd" id="M6e-cu-g7V"/> + </connections> + </menuItem> + <menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam"> + <connections> + <action selector="redo:" target="Ady-hI-5gd" id="oIA-Rs-6OD"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/> + <menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG"> + <connections> + <action selector="cut:" target="Ady-hI-5gd" id="YJe-68-I9s"/> + </connections> + </menuItem> + <menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU"> + <connections> + <action selector="copy:" target="Ady-hI-5gd" id="G1f-GL-Joy"/> + </connections> + </menuItem> + <menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL"> + <connections> + <action selector="paste:" target="Ady-hI-5gd" id="UvS-8e-Qdg"/> + </connections> + </menuItem> + <menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + <connections> + <action selector="pasteAsPlainText:" target="Ady-hI-5gd" id="cEh-KX-wJQ"/> + </connections> + </menuItem> + <menuItem title="Delete" id="pa3-QI-u2k"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="delete:" target="Ady-hI-5gd" id="0Mk-Ml-PaM"/> + </connections> + </menuItem> + <menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m"> + <connections> + <action selector="selectAll:" target="Ady-hI-5gd" id="VNm-Mi-diN"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/> + <menuItem title="Find" id="4EN-yA-p0u"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Find" id="1b7-l0-nxx"> + <items> + <menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W"> + <connections> + <action selector="performFindPanelAction:" target="Ady-hI-5gd" id="cD7-Qs-BN4"/> + </connections> + </menuItem> + <menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + <connections> + <action selector="performFindPanelAction:" target="Ady-hI-5gd" id="WD3-Gg-5AJ"/> + </connections> + </menuItem> + <menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye"> + <connections> + <action selector="performFindPanelAction:" target="Ady-hI-5gd" id="NDo-RZ-v9R"/> + </connections> + </menuItem> + <menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV"> + <connections> + <action selector="performFindPanelAction:" target="Ady-hI-5gd" id="HOh-sY-3ay"/> + </connections> + </menuItem> + <menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt"> + <connections> + <action selector="performFindPanelAction:" target="Ady-hI-5gd" id="U76-nv-p5D"/> + </connections> + </menuItem> + <menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd"> + <connections> + <action selector="centerSelectionInVisibleArea:" target="Ady-hI-5gd" id="IOG-6D-g5B"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Spelling and Grammar" id="Dv1-io-Yv7"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Spelling" id="3IN-sU-3Bg"> + <items> + <menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI"> + <connections> + <action selector="showGuessPanel:" target="Ady-hI-5gd" id="vFj-Ks-hy3"/> + </connections> + </menuItem> + <menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7"> + <connections> + <action selector="checkSpelling:" target="Ady-hI-5gd" id="fz7-VC-reM"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="bNw-od-mp5"/> + <menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleContinuousSpellChecking:" target="Ady-hI-5gd" id="7w6-Qz-0kB"/> + </connections> + </menuItem> + <menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleGrammarChecking:" target="Ady-hI-5gd" id="muD-Qn-j4w"/> + </connections> + </menuItem> + <menuItem title="Correct Spelling Automatically" id="78Y-hA-62v"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticSpellingCorrection:" target="Ady-hI-5gd" id="2lM-Qi-WAP"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Substitutions" id="9ic-FL-obx"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Substitutions" id="FeM-D8-WVr"> + <items> + <menuItem title="Show Substitutions" id="z6F-FW-3nz"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="orderFrontSubstitutionsPanel:" target="Ady-hI-5gd" id="oku-mr-iSq"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/> + <menuItem title="Smart Copy/Paste" id="9yt-4B-nSM"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleSmartInsertDelete:" target="Ady-hI-5gd" id="3IJ-Se-DZD"/> + </connections> + </menuItem> + <menuItem title="Smart Quotes" id="hQb-2v-fYv"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticQuoteSubstitution:" target="Ady-hI-5gd" id="ptq-xd-QOA"/> + </connections> + </menuItem> + <menuItem title="Smart Dashes" id="rgM-f4-ycn"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticDashSubstitution:" target="Ady-hI-5gd" id="oCt-pO-9gS"/> + </connections> + </menuItem> + <menuItem title="Smart Links" id="cwL-P1-jid"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticLinkDetection:" target="Ady-hI-5gd" id="Gip-E3-Fov"/> + </connections> + </menuItem> + <menuItem title="Data Detectors" id="tRr-pd-1PS"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticDataDetection:" target="Ady-hI-5gd" id="R1I-Nq-Kbl"/> + </connections> + </menuItem> + <menuItem title="Text Replacement" id="HFQ-gK-NFA"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticTextReplacement:" target="Ady-hI-5gd" id="DvP-Fe-Py6"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Transformations" id="2oI-Rn-ZJC"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Transformations" id="c8a-y6-VQd"> + <items> + <menuItem title="Make Upper Case" id="vmV-6d-7jI"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="uppercaseWord:" target="Ady-hI-5gd" id="sPh-Tk-edu"/> + </connections> + </menuItem> + <menuItem title="Make Lower Case" id="d9M-CD-aMd"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="lowercaseWord:" target="Ady-hI-5gd" id="iUZ-b5-hil"/> + </connections> + </menuItem> + <menuItem title="Capitalize" id="UEZ-Bs-lqG"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="capitalizeWord:" target="Ady-hI-5gd" id="26H-TL-nsh"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Speech" id="xrE-MZ-jX0"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Speech" id="3rS-ZA-NoH"> + <items> + <menuItem title="Start Speaking" id="Ynk-f8-cLZ"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="startSpeaking:" target="Ady-hI-5gd" id="654-Ng-kyl"/> + </connections> + </menuItem> + <menuItem title="Stop Speaking" id="Oyz-dy-DGm"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="stopSpeaking:" target="Ady-hI-5gd" id="dX8-6p-jy9"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Format" id="jxT-CU-nIS"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Format" id="GEO-Iw-cKr"> + <items> + <menuItem title="Font" id="Gi5-1S-RQB"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Font" systemMenu="font" id="aXa-aM-Jaq"> + <items> + <menuItem title="Show Fonts" keyEquivalent="t" id="Q5e-8K-NDq"> + <connections> + <action selector="orderFrontFontPanel:" target="YLy-65-1bz" id="WHr-nq-2xA"/> + </connections> + </menuItem> + <menuItem title="Bold" tag="2" keyEquivalent="b" id="GB9-OM-e27"> + <connections> + <action selector="addFontTrait:" target="YLy-65-1bz" id="hqk-hr-sYV"/> + </connections> + </menuItem> + <menuItem title="Italic" tag="1" keyEquivalent="i" id="Vjx-xi-njq"> + <connections> + <action selector="addFontTrait:" target="YLy-65-1bz" id="IHV-OB-c03"/> + </connections> + </menuItem> + <menuItem title="Underline" keyEquivalent="u" id="WRG-CD-K1S"> + <connections> + <action selector="underline:" target="Ady-hI-5gd" id="FYS-2b-JAY"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="5gT-KC-WSO"/> + <menuItem title="Bigger" tag="3" keyEquivalent="+" id="Ptp-SP-VEL"> + <connections> + <action selector="modifyFont:" target="YLy-65-1bz" id="Uc7-di-UnL"/> + </connections> + </menuItem> + <menuItem title="Smaller" tag="4" keyEquivalent="-" id="i1d-Er-qST"> + <connections> + <action selector="modifyFont:" target="YLy-65-1bz" id="HcX-Lf-eNd"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="kx3-Dk-x3B"/> + <menuItem title="Kern" id="jBQ-r6-VK2"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Kern" id="tlD-Oa-oAM"> + <items> + <menuItem title="Use Default" id="GUa-eO-cwY"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="useStandardKerning:" target="Ady-hI-5gd" id="6dk-9l-Ckg"/> + </connections> + </menuItem> + <menuItem title="Use None" id="cDB-IK-hbR"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="turnOffKerning:" target="Ady-hI-5gd" id="U8a-gz-Maa"/> + </connections> + </menuItem> + <menuItem title="Tighten" id="46P-cB-AYj"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="tightenKerning:" target="Ady-hI-5gd" id="hr7-Nz-8ro"/> + </connections> + </menuItem> + <menuItem title="Loosen" id="ogc-rX-tC1"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="loosenKerning:" target="Ady-hI-5gd" id="8i4-f9-FKE"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Ligatures" id="o6e-r0-MWq"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Ligatures" id="w0m-vy-SC9"> + <items> + <menuItem title="Use Default" id="agt-UL-0e3"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="useStandardLigatures:" target="Ady-hI-5gd" id="7uR-wd-Dx6"/> + </connections> + </menuItem> + <menuItem title="Use None" id="J7y-lM-qPV"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="turnOffLigatures:" target="Ady-hI-5gd" id="iX2-gA-Ilz"/> + </connections> + </menuItem> + <menuItem title="Use All" id="xQD-1f-W4t"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="useAllLigatures:" target="Ady-hI-5gd" id="KcB-kA-TuK"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Baseline" id="OaQ-X3-Vso"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Baseline" id="ijk-EB-dga"> + <items> + <menuItem title="Use Default" id="3Om-Ey-2VK"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="unscript:" target="Ady-hI-5gd" id="0vZ-95-Ywn"/> + </connections> + </menuItem> + <menuItem title="Superscript" id="Rqc-34-cIF"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="superscript:" target="Ady-hI-5gd" id="3qV-fo-wpU"/> + </connections> + </menuItem> + <menuItem title="Subscript" id="I0S-gh-46l"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="subscript:" target="Ady-hI-5gd" id="Q6W-4W-IGz"/> + </connections> + </menuItem> + <menuItem title="Raise" id="2h7-ER-AoG"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="raiseBaseline:" target="Ady-hI-5gd" id="4sk-31-7Q9"/> + </connections> + </menuItem> + <menuItem title="Lower" id="1tx-W0-xDw"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="lowerBaseline:" target="Ady-hI-5gd" id="OF1-bc-KW4"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem isSeparatorItem="YES" id="Ndw-q3-faq"/> + <menuItem title="Show Colors" keyEquivalent="C" id="bgn-CT-cEk"> + <connections> + <action selector="orderFrontColorPanel:" target="Ady-hI-5gd" id="mSX-Xz-DV3"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="iMs-zA-UFJ"/> + <menuItem title="Copy Style" keyEquivalent="c" id="5Vv-lz-BsD"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + <connections> + <action selector="copyFont:" target="Ady-hI-5gd" id="GJO-xA-L4q"/> + </connections> + </menuItem> + <menuItem title="Paste Style" keyEquivalent="v" id="vKC-jM-MkH"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + <connections> + <action selector="pasteFont:" target="Ady-hI-5gd" id="JfD-CL-leO"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Text" id="Fal-I4-PZk"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Text" id="d9c-me-L2H"> + <items> + <menuItem title="Align Left" keyEquivalent="{" id="ZM1-6Q-yy1"> + <connections> + <action selector="alignLeft:" target="Ady-hI-5gd" id="zUv-R1-uAa"/> + </connections> + </menuItem> + <menuItem title="Center" keyEquivalent="|" id="VIY-Ag-zcb"> + <connections> + <action selector="alignCenter:" target="Ady-hI-5gd" id="spX-mk-kcS"/> + </connections> + </menuItem> + <menuItem title="Justify" id="J5U-5w-g23"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="alignJustified:" target="Ady-hI-5gd" id="ljL-7U-jND"/> + </connections> + </menuItem> + <menuItem title="Align Right" keyEquivalent="}" id="wb2-vD-lq4"> + <connections> + <action selector="alignRight:" target="Ady-hI-5gd" id="r48-bG-YeY"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="4s2-GY-VfK"/> + <menuItem title="Writing Direction" id="H1b-Si-o9J"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Writing Direction" id="8mr-sm-Yjd"> + <items> + <menuItem title="Paragraph" enabled="NO" id="ZvO-Gk-QUH"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem id="YGs-j5-SAR"> + <string key="title"> Default</string> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="makeBaseWritingDirectionNatural:" target="Ady-hI-5gd" id="qtV-5e-UBP"/> + </connections> + </menuItem> + <menuItem id="Lbh-J2-qVU"> + <string key="title"> Left to Right</string> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="makeBaseWritingDirectionLeftToRight:" target="Ady-hI-5gd" id="S0X-9S-QSf"/> + </connections> + </menuItem> + <menuItem id="jFq-tB-4Kx"> + <string key="title"> Right to Left</string> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="makeBaseWritingDirectionRightToLeft:" target="Ady-hI-5gd" id="5fk-qB-AqJ"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="swp-gr-a21"/> + <menuItem title="Selection" enabled="NO" id="cqv-fj-IhA"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem id="Nop-cj-93Q"> + <string key="title"> Default</string> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="makeTextWritingDirectionNatural:" target="Ady-hI-5gd" id="lPI-Se-ZHp"/> + </connections> + </menuItem> + <menuItem id="BgM-ve-c93"> + <string key="title"> Left to Right</string> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="makeTextWritingDirectionLeftToRight:" target="Ady-hI-5gd" id="caW-Bv-w94"/> + </connections> + </menuItem> + <menuItem id="RB4-Sm-HuC"> + <string key="title"> Right to Left</string> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="makeTextWritingDirectionRightToLeft:" target="Ady-hI-5gd" id="EXD-6r-ZUu"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem isSeparatorItem="YES" id="fKy-g9-1gm"/> + <menuItem title="Show Ruler" id="vLm-3I-IUL"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleRuler:" target="Ady-hI-5gd" id="FOx-HJ-KwY"/> + </connections> + </menuItem> + <menuItem title="Copy Ruler" keyEquivalent="c" id="MkV-Pr-PK5"> + <modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/> + <connections> + <action selector="copyRuler:" target="Ady-hI-5gd" id="71i-fW-3W2"/> + </connections> + </menuItem> + <menuItem title="Paste Ruler" keyEquivalent="v" id="LVM-kO-fVI"> + <modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/> + <connections> + <action selector="pasteRuler:" target="Ady-hI-5gd" id="cSh-wd-qM2"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="View" id="H8h-7b-M4v"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="View" id="HyV-fh-RgO"> + <items> + <menuItem title="Show Toolbar" keyEquivalent="t" id="snW-S8-Cw5"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + <connections> + <action selector="toggleToolbarShown:" target="Ady-hI-5gd" id="BXY-wc-z0C"/> + </connections> + </menuItem> + <menuItem title="Customize Toolbar…" id="1UK-8n-QPP"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="runToolbarCustomizationPalette:" target="Ady-hI-5gd" id="pQI-g3-MTW"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="hB3-LF-h0Y"/> + <menuItem title="Show Sidebar" keyEquivalent="s" id="kIP-vf-haE"> + <modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/> + <connections> + <action selector="toggleSidebar:" target="Ady-hI-5gd" id="iwa-gc-5KM"/> + </connections> + </menuItem> + <menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa"> + <modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/> + <connections> + <action selector="toggleFullScreen:" target="Ady-hI-5gd" id="dU3-MA-1Rq"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Window" id="aUF-d1-5bR"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo"> + <items> + <menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV"> + <connections> + <action selector="performMiniaturize:" target="Ady-hI-5gd" id="VwT-WD-YPe"/> + </connections> + </menuItem> + <menuItem title="Zoom" id="R4o-n2-Eq4"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="performZoom:" target="Ady-hI-5gd" id="DIl-cC-cCs"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/> + <menuItem title="Bring All to Front" id="LE2-aR-0XJ"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="arrangeInFront:" target="Ady-hI-5gd" id="DRN-fu-gQh"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Help" id="wpr-3q-Mcd"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ"> + <items> + <menuItem title="HelloWorld Help" keyEquivalent="?" id="FKE-Sm-Kum"> + <connections> + <action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + </items> + </menu> + <connections> + <outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/> + </connections> + </application> + <customObject id="Voe-Tx-rLC" customClass="AppDelegate"/> + <customObject id="YLy-65-1bz" customClass="NSFontManager"/> + <customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="75" y="0.0"/> + </scene> + </scenes> +</document> diff --git a/packages/desktop-host/macos/main.m b/packages/desktop-host/macos/main.m new file mode 100644 index 000000000..1f154fcf6 --- /dev/null +++ b/packages/desktop-host/macos/main.m @@ -0,0 +1,5 @@ +#import <Cocoa/Cocoa.h> + +int main(int argc, const char *argv[]) { + return NSApplicationMain(argc, argv); +} diff --git a/packages/desktop-host/package.json b/packages/desktop-host/package.json index 4e4b7ea3e..43ffb5bd3 100644 --- a/packages/desktop-host/package.json +++ b/packages/desktop-host/package.json @@ -4,6 +4,7 @@ "license": "MIT", "files": [ "AppDelegate.mm", + "macos", "windows", "SparkCustomize.mm" ], diff --git a/packages/desktop-links/package.json b/packages/desktop-links/package.json index 334f711a4..1959a66a3 100644 --- a/packages/desktop-links/package.json +++ b/packages/desktop-links/package.json @@ -13,9 +13,9 @@ "common" ], "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", - "expo-linking": "~8.0.12" + "react": "^19.2.0", + "react-native": "0.83.10", + "expo-linking": "~55.0.17" }, "codegenConfig": { "name": "RNDesktopLinksSpec", diff --git a/packages/desktop-links/src/index.macos.ts b/packages/desktop-links/src/index.macos.ts new file mode 100644 index 000000000..d158c5764 --- /dev/null +++ b/packages/desktop-links/src/index.macos.ts @@ -0,0 +1 @@ +export * from "./api"; diff --git a/packages/desktop-shortcuts/package.json b/packages/desktop-shortcuts/package.json index 5c6378e43..b7f66fd91 100644 --- a/packages/desktop-shortcuts/package.json +++ b/packages/desktop-shortcuts/package.json @@ -10,8 +10,8 @@ "*.podspec" ], "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6" + "react": "^19.2.0", + "react-native": "0.83.10" }, "codegenConfig": { "name": "RNDesktopShortcutsSpec", diff --git a/packages/desktop-windows/package.json b/packages/desktop-windows/package.json index dbba113bd..50833017e 100644 --- a/packages/desktop-windows/package.json +++ b/packages/desktop-windows/package.json @@ -10,8 +10,8 @@ "*.podspec" ], "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6" + "react": "^19.2.0", + "react-native": "0.83.10" }, "codegenConfig": { "name": "RNDesktopWindowsSpec", diff --git a/packages/desktop-windows/src/macos-adapter.ts b/packages/desktop-windows/src/macos-adapter.ts index 5f735d018..553b22fd6 100644 --- a/packages/desktop-windows/src/macos-adapter.ts +++ b/packages/desktop-windows/src/macos-adapter.ts @@ -51,5 +51,6 @@ export async function createMacOSWindow(options: WindowOpenOptions): Promise<voi await macosCommand(() => macosNative().openWindow(JSON.stringify({ ...macosOptions(options), identifier: options.id, moduleName: options.component, initialProperties: options.props ?? {}, title: options.title, deferOrderFront: true, sparkOverlay: options.kind === "overlay" }))); } export async function updateMacOSWindow(id: string, options: WindowUpdateOptions): Promise<void> { - await macosCommand(() => macosNative().setWindowOptions(id, JSON.stringify(macosOptions(options)))); + const native = macosNative(), json = JSON.stringify(macosOptions(options)); + await macosCommand(() => id === "main" ? native.setMainWindowOptions(json) : native.setWindowOptions(id, json)); } diff --git a/packages/desktop/package.json b/packages/desktop/package.json index d18dd4c4d..c4f7cfadf 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -109,6 +109,7 @@ "@legendapp/spark-sqlite": "0.0.1", "@react-native-runtimes/core": "0.1.0-alpha.2", "react-native-nitro-modules": "0.37.0", + "@react-native-windows/find-dotnet-tools": "0.0.0-canary.3", "@legendapp/spark-cli": "0.0.1", "@legendapp/spark-ui": "0.0.1", "@legendapp/spark-audio": "0.0.1", @@ -132,10 +133,10 @@ "node": ">=24.19.0" }, "peerDependencies": { - "expo": "54.0.37", - "react": "^19.1.0", - "react-native": "0.81.6", - "react-dom": "^19.1.0", + "expo": "55.0.32", + "react": "^19.2.0", + "react-native": "0.83.10", + "react-dom": "^19.2.0", "react-native-enriched-markdown": "1.1.1", "react-native-enriched-html": "1.1.1" }, diff --git a/packages/documents/package.json b/packages/documents/package.json index ea65684bb..b14a894c2 100644 --- a/packages/documents/package.json +++ b/packages/documents/package.json @@ -16,7 +16,7 @@ "@legendapp/spark-desktop-links": "0.0.1" }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6" + "react": "^19.2.0", + "react-native": "0.83.10" } } diff --git a/packages/drag-drop/package.json b/packages/drag-drop/package.json index 923d853dd..9d11147f8 100644 --- a/packages/drag-drop/package.json +++ b/packages/drag-drop/package.json @@ -12,8 +12,8 @@ "react-native.config.js" ], "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6" + "react": "^19.2.0", + "react-native": "0.83.10" }, "codegenConfig": { "name": "RNDesktopDragSpec", diff --git a/packages/file-dialog/package.json b/packages/file-dialog/package.json index c4de80a2b..735fe5181 100644 --- a/packages/file-dialog/package.json +++ b/packages/file-dialog/package.json @@ -18,8 +18,8 @@ } }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6" + "react": "^19.2.0", + "react-native": "0.83.10" }, "license": "MIT", "files": [ diff --git a/packages/file-system/package.json b/packages/file-system/package.json index 9e134e39d..118333701 100644 --- a/packages/file-system/package.json +++ b/packages/file-system/package.json @@ -12,8 +12,8 @@ "react-native.config.js" ], "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6" + "react": "^19.2.0", + "react-native": "0.83.10" }, "codegenConfig": { "name": "RNDesktopFileSystemSpec", diff --git a/packages/global-shortcuts/package.json b/packages/global-shortcuts/package.json index bc1eb5b31..c80a06ea6 100644 --- a/packages/global-shortcuts/package.json +++ b/packages/global-shortcuts/package.json @@ -12,8 +12,8 @@ "react-native.config.js" ], "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6" + "react": "^19.2.0", + "react-native": "0.83.10" }, "codegenConfig": { "name": "RNDesktopGlobalShortcutsSpec", diff --git a/packages/html/package.json b/packages/html/package.json index e2764906c..b625f1e00 100644 --- a/packages/html/package.json +++ b/packages/html/package.json @@ -9,8 +9,8 @@ "src" ], "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", "react-native-enriched-html": "1.1.1" }, "dependencies": { diff --git a/packages/markdown/package.json b/packages/markdown/package.json index e09a2489c..c1ff75207 100644 --- a/packages/markdown/package.json +++ b/packages/markdown/package.json @@ -9,8 +9,8 @@ "src" ], "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", "react-native-enriched-markdown": "1.1.1" }, "dependencies": { diff --git a/packages/message-dialog/package.json b/packages/message-dialog/package.json index 30e613ae3..baa1ae09b 100644 --- a/packages/message-dialog/package.json +++ b/packages/message-dialog/package.json @@ -12,8 +12,8 @@ "react-native.config.js" ], "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6" + "react": "^19.2.0", + "react-native": "0.83.10" }, "codegenConfig": { "name": "RNDesktopMessageDialogSpec", diff --git a/packages/native-menu/package.json b/packages/native-menu/package.json index 33c0ecaa1..77022f6cf 100644 --- a/packages/native-menu/package.json +++ b/packages/native-menu/package.json @@ -18,8 +18,8 @@ } }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6" + "react": "^19.2.0", + "react-native": "0.83.10" }, "license": "MIT", "files": [ diff --git a/packages/notifications/package.json b/packages/notifications/package.json index 86cffa985..7a6e32a7c 100644 --- a/packages/notifications/package.json +++ b/packages/notifications/package.json @@ -12,8 +12,8 @@ "react-native.config.js" ], "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6" + "react": "^19.2.0", + "react-native": "0.83.10" }, "codegenConfig": { "name": "RNDesktopNotificationsSpec", diff --git a/packages/processes/package.json b/packages/processes/package.json index d4d3eacb8..f6c03a3ea 100644 --- a/packages/processes/package.json +++ b/packages/processes/package.json @@ -12,8 +12,8 @@ "react-native.config.js" ], "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6" + "react": "^19.2.0", + "react-native": "0.83.10" }, "codegenConfig": { "name": "RNDesktopProcessesSpec", diff --git a/packages/rich-text/package.json b/packages/rich-text/package.json index cfb4b6125..9983795b0 100644 --- a/packages/rich-text/package.json +++ b/packages/rich-text/package.json @@ -9,8 +9,8 @@ "src" ], "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6" + "react": "^19.2.0", + "react-native": "0.83.10" }, "dependencies": { "@legendapp/spark-desktop-shortcuts": "0.0.1" diff --git a/packages/secure-storage/package.json b/packages/secure-storage/package.json index 6e0cd46e4..c69babf9b 100644 --- a/packages/secure-storage/package.json +++ b/packages/secure-storage/package.json @@ -12,9 +12,9 @@ "windows" ], "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", - "expo-secure-store": "~15.0.8" + "react": "^19.2.0", + "react-native": "0.83.10", + "expo-secure-store": "~55.0.18" }, "codegenConfig": { "name": "RNDesktopSecureStorageSpec", diff --git a/packages/secure-storage/src/index.macos.ts b/packages/secure-storage/src/index.macos.ts new file mode 100644 index 000000000..9c4641a4b --- /dev/null +++ b/packages/secure-storage/src/index.macos.ts @@ -0,0 +1 @@ +export * from "./desktop"; diff --git a/packages/settings-ui/package.json b/packages/settings-ui/package.json index d880ec73d..da339d739 100644 --- a/packages/settings-ui/package.json +++ b/packages/settings-ui/package.json @@ -13,8 +13,8 @@ "@legendapp/spark-desktop-app": "0.0.1" }, "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", + "react": "^19.2.0", + "react-native": "0.83.10", "@legendapp/list": ">=3.3.0" } } diff --git a/packages/system/package.json b/packages/system/package.json index 8109e2017..e10a07962 100644 --- a/packages/system/package.json +++ b/packages/system/package.json @@ -12,8 +12,8 @@ "react-native.config.js" ], "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6" + "react": "^19.2.0", + "react-native": "0.83.10" }, "codegenConfig": { "name": "RNDesktopSystemSpec", diff --git a/packages/tray/package.json b/packages/tray/package.json index 6b5aa239f..25f885bf0 100644 --- a/packages/tray/package.json +++ b/packages/tray/package.json @@ -12,8 +12,8 @@ "react-native.config.js" ], "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6" + "react": "^19.2.0", + "react-native": "0.83.10" }, "codegenConfig": { "name": "RNDesktopTraySpec", diff --git a/packages/ui/package.json b/packages/ui/package.json index e742df670..09aa1622f 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -14,11 +14,11 @@ "uniwind.web.ts" ], "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6", - "@expo/ui": "0.2.0-beta.9", + "react": "^19.2.0", + "react-native": "0.83.10", + "@expo/ui": "55.0.17", "uniwind": "^1.6.3", - "react-native-windows": "0.81.35" + "react-native-windows": "0.83.2" }, "peerDependenciesMeta": { "@expo/ui": { diff --git a/packages/ui/src/index.android.tsx b/packages/ui/src/index.android.tsx index 75fde4081..9a73d1f4d 100644 --- a/packages/ui/src/index.android.tsx +++ b/packages/ui/src/index.android.tsx @@ -1,7 +1,18 @@ import type { ComponentProps } from "react"; -function ExpoButton(props: ComponentProps<typeof import("@expo/ui/jetpack-compose").Button>) { - const Component = (require("@expo/ui/jetpack-compose") as typeof import("@expo/ui/jetpack-compose")).Button; - return <Component {...props} />; +type Compose = typeof import("@expo/ui/jetpack-compose"); +const compose = () => require("@expo/ui/jetpack-compose") as Compose; +const buttons = { default: "Button", bordered: "OutlinedButton", borderless: "TextButton" } as const; +function ExpoButton({ variant, label, ...props }: Omit<ComponentProps<Compose["Button"]>, "children"> & { variant: keyof typeof buttons; label: string }) { + const { Host, Text, [buttons[variant]]: Component } = compose(); + return <Host style={{ flex: 1 }}><Component {...props}><Text>{label}</Text></Component></Host>; +} +function Segments({ labels, selected, enabled, onSelect }: { labels: string[]; selected: number; enabled: boolean; onSelect: (index: number) => void }) { + const { Host, Text, SingleChoiceSegmentedButtonRow, SegmentedButton } = compose(); + const { selectable } = require("@expo/ui/jetpack-compose/modifiers") as typeof import("@expo/ui/jetpack-compose/modifiers"); + return <Host style={{ flex: 1 }}><SingleChoiceSegmentedButtonRow>{labels.map((label, index) => + <SegmentedButton key={index} selected={index === selected} enabled={enabled} onClick={() => onSelect(index)} + modifiers={[selectable(index === selected, () => onSelect(index), "radioButton")]}><SegmentedButton.Label><Text>{label}</Text></SegmentedButton.Label></SegmentedButton>)} + </SingleChoiceSegmentedButtonRow></Host>; } import { View, Text } from "react-native"; import { requireOptionalNativeModule } from "expo-modules-core"; @@ -18,9 +29,9 @@ export function getControlAvailability(control: ControlKind): Availability { export function Button(props: ButtonProps) { validateButton(props); const control = useControl(props, getControlAvailability("button")); const { children, onPress, variant = "default", style, testID, accessibilityLabel = children } = props; - return <View ref={control.ref} style={[{ width: 160, height: 48 }, style]} testID={testID}> - {control.failed ? <Text accessibilityLabel={accessibilityLabel} accessibilityState={{ disabled: true }}>{children}</Text> : - <ExpoButton style={{ flex: 1 }} onPress={() => { if (control.active()) onPress?.(); }} disabled={control.disabled} variant={variant} {...{ accessibilityLabel }}>{children}</ExpoButton>} + return <View ref={control.ref} style={[{ width: 160, height: 48 }, style]} testID={testID} accessibilityLabel={accessibilityLabel}> + {control.failed ? <Text accessibilityState={{ disabled: true }}>{children}</Text> : + <ExpoButton onClick={() => { if (control.active()) onPress?.(); }} enabled={!control.disabled} variant={variant} label={children} />} </View>; } function Selection(props: SelectProps) { @@ -28,10 +39,9 @@ function Selection(props: SelectProps) { if (typeof props.onValueChange !== "function") throw new SparkError("E_INVALID_ARGUMENT", "Expected onValueChange"); const control = useControl(props, getControlAvailability("select")); return <View ref={control.ref} style={[{ width: 280, minHeight: 48, flexDirection: "row" }, props.style]} testID={props.testID} accessibilityLabel={props.accessibilityLabel}> - {control.failed ? <Text accessibilityState={{ disabled: true }}>{props.options[selected].label}</Text> : props.options.map(option => - <ExpoButton key={option.value} disabled={control.disabled} variant={option.value === props.value ? "bordered" : "borderless"} style={{ flex: 1 }} - {...{ accessibilityRole: "radio", accessibilityState: { checked: option.value === props.value, disabled: control.disabled } }} - onPress={() => { if (control.active()) props.onValueChange(option.value); }}>{option.label}</ExpoButton>)} + {control.failed ? <Text accessibilityState={{ disabled: true }}>{props.options[selected].label}</Text> : + <Segments labels={props.options.map(option => option.label)} selected={selected} enabled={!control.disabled} + onSelect={index => { if (control.active()) props.onValueChange(props.options[index].value); }} />} </View>; } /** Android presents its choices inline; there is no dropdown backend in the pinned Expo UI. */ diff --git a/packages/ui/src/index.ios.tsx b/packages/ui/src/index.ios.tsx index 737f5e00b..bcd598037 100644 --- a/packages/ui/src/index.ios.tsx +++ b/packages/ui/src/index.ios.tsx @@ -7,11 +7,15 @@ function Host(props: ComponentProps<typeof import("@expo/ui/swift-ui").Host>) { const Component = (require("@expo/ui/swift-ui") as typeof import("@expo/ui/swift-ui")).Host; return <Component {...props} />; } -function Picker(props: ComponentProps<typeof import("@expo/ui/swift-ui").Picker>) { - const Component = (require("@expo/ui/swift-ui") as typeof import("@expo/ui/swift-ui")).Picker; +function Picker(props: ComponentProps<typeof import("@expo/ui/swift-ui").Picker<number>>) { + const Component = (require("@expo/ui/swift-ui") as typeof import("@expo/ui/swift-ui")).Picker<number>; return <Component {...props} />; } -import { frame, disabled as disabledModifier, accessibilityLabel as labelModifier } from "@expo/ui/swift-ui/modifiers"; +function Option(props: ComponentProps<typeof import("@expo/ui/swift-ui").Text>) { + const Component = (require("@expo/ui/swift-ui") as typeof import("@expo/ui/swift-ui")).Text; + return <Component {...props} />; +} +import { frame, buttonStyle, pickerStyle, tag, disabled as disabledModifier, accessibilityLabel as labelModifier } from "@expo/ui/swift-ui/modifiers"; import { View, Text } from "react-native"; import { SparkError, type Availability } from "@legendapp/spark-desktop-app/src/contracts"; import { requireOptionalNativeModule } from "expo-modules-core"; @@ -30,8 +34,8 @@ export function Button(props: ButtonProps) { const { children, onPress, variant = "default", style, testID, accessibilityLabel = children } = props; return <View ref={control.ref} style={[{ width: 160, height: 44 }, style]}> {control.failed ? <Text accessibilityLabel={accessibilityLabel} accessibilityState={{ disabled: true }} testID={testID}>{children}</Text> : - <Host style={{ flex: 1 }}><ExpoButton onPress={() => { if (control.active()) onPress?.(); }} disabled={control.disabled} - variant={variant} modifiers={[...fill, labelModifier(accessibilityLabel)]} testID={testID}>{children}</ExpoButton></Host>} + <Host style={{ flex: 1 }}><ExpoButton label={children} onPress={() => { if (control.active()) onPress?.(); }} + modifiers={[...fill, buttonStyle(variant === "default" ? "automatic" : variant), disabledModifier(control.disabled), labelModifier(accessibilityLabel)]} testID={testID} /></Host>} </View>; } function Selection({ segmented, ...props }: SelectProps & { segmented: boolean }) { @@ -43,16 +47,17 @@ function Selection({ segmented, ...props }: SelectProps & { segmented: boolean } const modifiers = [disabledModifier(control.disabled), ...(accessibilityLabel ? [labelModifier(accessibilityLabel)] : [])]; return <View ref={control.ref} style={[{ width: 240, height: 44 }, style]}> {control.failed ? <Text accessibilityLabel={accessibilityLabel} accessibilityState={{ disabled: true }} testID={testID}>{options[selectedIndex].label}</Text> : - <Host style={{ flex: 1 }}><Picker key={selectionRevision} variant={segmented ? "segmented" : "menu"} options={options.map(option => option.label)} selectedIndex={selectedIndex} - onOptionSelected={event => { + <Host style={{ flex: 1 }}><Picker key={selectionRevision} selection={selectedIndex} + onSelectionChange={index => { if (!control.active()) return; - const index = event?.nativeEvent?.index; if (!Number.isInteger(index) || !options[index]) { control.error(new SparkError("E_INVALID_DATA", "Invalid native selection")); return; } // Expo's SwiftUI picker retains local selection and ignores an unchanged - // selectedIndex. A fresh picker also restores a parent-vetoed selection. + // selection. A fresh picker also restores a parent-vetoed selection. revise(value => value + 1); onValueChange(options[index].value); - }} modifiers={modifiers} testID={testID} /></Host>} + }} modifiers={[pickerStyle(segmented ? "segmented" : "menu"), ...modifiers]} testID={testID}> + {options.map((option, index) => <Option key={option.value} modifiers={[tag(index)]}>{option.label}</Option>)} + </Picker></Host>} </View>; } export function Select(props: SelectProps) { return <Selection {...props} segmented={false} />; } diff --git a/packages/ui/src/index.macos.tsx b/packages/ui/src/index.macos.tsx new file mode 100644 index 000000000..9c4641a4b --- /dev/null +++ b/packages/ui/src/index.macos.tsx @@ -0,0 +1 @@ +export * from "./desktop"; diff --git a/packages/updates/package.json b/packages/updates/package.json index 07bfa1300..8d256fa00 100644 --- a/packages/updates/package.json +++ b/packages/updates/package.json @@ -11,8 +11,8 @@ "Sparkle.podspec.json" ], "peerDependencies": { - "react": "^19.1.0", - "react-native": "0.81.6" + "react": "^19.2.0", + "react-native": "0.83.10" }, "codegenConfig": { "name": "RNDesktopUpdatesSpec", diff --git a/patches/dependencies.json b/patches/dependencies.json index 76da427f3..74a6bf33b 100644 --- a/patches/dependencies.json +++ b/patches/dependencies.json @@ -10,5 +10,257 @@ "url": "https://registry.npmjs.org/react-native-enriched-html/-/react-native-enriched-html-1.1.1.tgz", "integrity": "sha512-OlJLESOgA4eqgWQe3BXOK9kFk7iypOnQo22sIce5k/x2eGsOtnAwDPqHnaVfnOZlzzNWh+wXRzxuSL1tSBOkgA==", "patch": "react-native-enriched-html@1.1.1.patch" + }, + "react-native-mmkv": { + "version": "4.3.2", + "url": "https://registry.npmjs.org/react-native-mmkv/-/react-native-mmkv-4.3.2.tgz", + "integrity": "sha512-49OAyfkg0/TMWiWELZN6VuVQPZPhizwL4DTmp8b7B1md3dB/s3LH3mGfC3T+lp9W0y/rqxZMEnotLFTIbOAenQ==", + "patch": "react-native-mmkv@4.3.2.patch", + "optional": true + }, + "react-native-unistyles": { + "version": "3.6.0", + "url": "https://registry.npmjs.org/react-native-unistyles/-/react-native-unistyles-3.6.0.tgz", + "integrity": "sha512-eYqixPy6auBj6k/ica9fCYIPgFQsLzUwRoxGZ04hX3hkANTNjaz3/LzNfXPfgJbwF/fQrhaK0qII4nhtSqUPfA==", + "patch": "react-native-unistyles@3.6.0.patch", + "optional": true + }, + "react-native-quick-crypto": { + "version": "1.1.9", + "url": "https://registry.npmjs.org/react-native-quick-crypto/-/react-native-quick-crypto-1.1.9.tgz", + "integrity": "sha512-u60Kr4EdAYI1ksGq7uwJfOXNJR/iWkCQ1AxfoVbjM/pEpiydddzsFXFYqwyv7EeB7eTDgg5/LiCGwNxA7W7NKA==", + "patch": "react-native-quick-crypto@1.1.9.patch", + "optional": true + }, + "react-native-quick-base64": { + "version": "3.0.1", + "url": "https://registry.npmjs.org/react-native-quick-base64/-/react-native-quick-base64-3.0.1.tgz", + "integrity": "sha512-EjUP2U7WqKmlMmoY7XGyHomy8bM0q4+yCDCRg4ZezQ6zedYRwc7yVk4V2O/iSftKaLEzhuW98lpyPMdk1iPHXQ==", + "patch": "react-native-quick-base64@3.0.1.patch", + "optional": true + }, + "react-native-nitro-fetch": { + "version": "1.9.0", + "url": "https://registry.npmjs.org/react-native-nitro-fetch/-/react-native-nitro-fetch-1.9.0.tgz", + "integrity": "sha512-4rxaPcXKudZW1Ma1Y63rHF8/7hbanOyqr01Gac6ROUYApcCH8/YtS0d02cm0ZEbSzwPvmcvFRzPoT4cHWxhJGA==", + "patch": "react-native-nitro-fetch@1.9.0.patch", + "optional": true + }, + "@react-native-community/blur": { + "version": "4.4.1", + "url": "https://registry.npmjs.org/@react-native-community/blur/-/blur-4.4.1.tgz", + "integrity": "sha512-XBSsRiYxE/MOEln2ayunShfJtWztHwUxLFcSL20o+HNNRnuUDv+GXkF6FmM2zE8ZUfrnhQ/zeTqvnuDPGw6O8A==", + "patch": "react-native-community-blur@4.4.1.patch", + "optional": true + }, + "react-native-worklets-core": { + "version": "1.6.3", + "url": "https://registry.npmjs.org/react-native-worklets-core/-/react-native-worklets-core-1.6.3.tgz", + "integrity": "sha512-r3Q40XQBccx/iAI5tlyiua+micvO1UGzzUOskNweZUXyfrrE+rb5aqxqruBPqXf90rO+bBiplylLMEAXCLTyGA==", + "patch": "react-native-worklets-core@1.6.3.patch", + "optional": true + }, + "react-native-fast-image": { + "version": "10.9.9", + "url": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-10.9.9.tgz", + "integrity": "sha512-Dev69p1kcTxx7qLLFwax5hB/3X5wMNpviUIXS6K6KnITyhu+O2AEUhenZoq2OmGnT9UzHBIYHkEKoFebZNK/lA==", + "patch": "react-native-fast-image@10.9.9.patch", + "optional": true + }, + "react-native-release-profiler": { + "version": "0.4.5", + "url": "https://registry.npmjs.org/react-native-release-profiler/-/react-native-release-profiler-0.4.5.tgz", + "integrity": "sha512-1w05jDlKRdIMxTuSPep67eMc+FW805SJZjWJwBVsPIQHmRYttxBdrdbzF0mG5XVvMVRNji4RHLidu54+V54lRQ==", + "patch": "react-native-release-profiler@0.4.5.patch", + "optional": true + }, + "react-native-blurhash": { + "version": "2.1.3", + "url": "https://registry.npmjs.org/react-native-blurhash/-/react-native-blurhash-2.1.3.tgz", + "integrity": "sha512-tYyFketZkrrEIABJ5Z1Pt6N2dqPqtSp7w4Lce17sx1bvspdppgVu/ehtZoPRZu02fYyX92RkyCeGXtCbtUNntg==", + "patch": "react-native-blurhash@2.1.3.patch", + "optional": true + }, + "react-native-screen-corner-radius": { + "version": "0.2.3", + "url": "https://registry.npmjs.org/react-native-screen-corner-radius/-/react-native-screen-corner-radius-0.2.3.tgz", + "integrity": "sha512-8JgiHI/S2Xf6swquEjopHW//wI4a/nV/dEImJMR8YXQVWVdnjbhcMi0NGk1s1cW8xuunwlFT173PozsS3Gvgxg==", + "patch": "react-native-screen-corner-radius@0.2.3.patch", + "optional": true + }, + "react-native-app-logs": { + "version": "0.3.1", + "url": "https://registry.npmjs.org/react-native-app-logs/-/react-native-app-logs-0.3.1.tgz", + "integrity": "sha512-Nyxwx92vJRTIBdIRiHlFrz0IFJq+rKIsq6XFRiYT7ZBA1jQIPBYX/kx9kCRwqLDGnSBzec2oYwPScGEllRpNCw==", + "patch": "react-native-app-logs@0.3.1.patch", + "optional": true + }, + "react-native-nitro-zxing": { + "version": "0.3.0", + "url": "https://registry.npmjs.org/react-native-nitro-zxing/-/react-native-nitro-zxing-0.3.0.tgz", + "integrity": "sha512-SQ/qwmfakdPcOMVzph99vFOPbPkVg3JZaTGrjVVXG6RS8ZUqMocHqbeM4V17qPDTzFK+rsAp6T7u2aFe3Hot/w==", + "patch": "react-native-nitro-zxing@0.3.0.patch", + "optional": true + }, + "expo-keep-awake": { + "version": "55.0.8", + "url": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-55.0.8.tgz", + "integrity": "sha512-PfIpMfM+STOBwkR5XOE+yVtER86c44MD+W8QD8JxuO0sT9pF7Y1SJYakWlpvX8xsGA+bjKLxftm9403s9kQhKA==", + "patch": "expo-keep-awake@55.0.8.patch", + "optional": true + }, + "react-native-screens": { + "version": "4.23.0", + "url": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.23.0.tgz", + "integrity": "sha512-XhO3aK0UeLpBn4kLecd+J+EDeRRJlI/Ro9Fze06vo1q163VeYtzfU9QS09/VyDFMWR1qxDC1iazCArTPSFFiPw==", + "patch": "react-native-screens@4.23.0.patch", + "optional": true + }, + "@react-navigation/native-stack": { + "version": "7.21.0", + "url": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.21.0.tgz", + "integrity": "sha512-bYrNW87nDtbMXAJsosYwu59xwTqeuaurfCDA+62MQ6hY4+2Iv71+Sik3ulW92fOqg8s2SZinOJosHEWoi9Ayqw==", + "patch": "react-navigation-native-stack@7.21.0.patch", + "optional": true + }, + "expo-web-browser": { + "version": "55.0.20", + "url": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-55.0.20.tgz", + "integrity": "sha512-masGrfbi/jqMezm/8cHHZ8chW+ifxumzt7QOCPiL+9aix0BSwuwDIBryEm2QPVmqJX51jckS0n245vWhwheQuQ==", + "patch": "expo-web-browser@55.0.20.patch", + "optional": true + }, + "expo-application": { + "version": "55.0.19", + "url": "https://registry.npmjs.org/expo-application/-/expo-application-55.0.19.tgz", + "integrity": "sha512-RFer4T46/OaIkNQEweA9PQj6xlXKor71IPNydWxlupx/XG05Yv/269HJJ3Q9zejdY9M7R3jTeTlAYYmXwbLBgA==", + "patch": "expo-application@55.0.19.patch", + "optional": true + }, + "expo-device": { + "version": "55.0.21", + "url": "https://registry.npmjs.org/expo-device/-/expo-device-55.0.21.tgz", + "integrity": "sha512-W2DNWWnjk3KSdAy2BtK9ie1lByO6IsJIpriQAnvS1+d/e12wXhq14iZLUB5iA0CCVftU3eliQRIzkyT2cdL9Ng==", + "patch": "expo-device@55.0.21.patch", + "optional": true + }, + "expo-localization": { + "version": "55.0.19", + "url": "https://registry.npmjs.org/expo-localization/-/expo-localization-55.0.19.tgz", + "integrity": "sha512-rw8pcC4QL8HC3K8Vs5pdVCB+ayFkaVD0f+Ul+jHgM6W7XdzGtCzd0x7rI3M/9PzP1YpAgQ4jYZid/g+UKi9IIA==", + "patch": "expo-localization@55.0.19.patch", + "optional": true + }, + "expo-haptics": { + "version": "55.0.18", + "url": "https://registry.npmjs.org/expo-haptics/-/expo-haptics-55.0.18.tgz", + "integrity": "sha512-bEH0bf5bBGn7U2eTZVdiXBUk4vTfNGbfv9/9PCYLg3gL7L/WE2g8WKs2vYg1B4/BDSucyO8prZofH3IgvLJojQ==", + "patch": "expo-haptics@55.0.18.patch", + "optional": true + }, + "react-native-svg": { + "version": "15.15.3", + "url": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.3.tgz", + "integrity": "sha512-/k4KYwPBLGcx2f5d4FjE+vCScK7QOX14cl2lIASJ28u4slHHtIhL0SZKU7u9qmRBHxTCKPoPBtN6haT1NENJNA==", + "patch": "react-native-svg@15.15.3.patch", + "optional": true + }, + "expo-font": { + "version": "55.0.8", + "url": "https://registry.npmjs.org/expo-font/-/expo-font-55.0.8.tgz", + "integrity": "sha512-WyP75pnKqhLNktYwDn3xKAUNt5rLihRDv8XWGhhz6VEhVqypixpT86NA3uGtiDTlM3gGjhrYCY7o7ypXgCUOZg==", + "patch": "expo-font@55.0.8.patch", + "optional": true + }, + "expo-blur": { + "version": "55.0.18", + "url": "https://registry.npmjs.org/expo-blur/-/expo-blur-55.0.18.tgz", + "integrity": "sha512-eXuVAahonZXLrXa//isvq72ed1to/MmbHUyd2WTkRH2/FjC9X91rZ4BUNIFBZnAxaNXQrKw/ITfG/VMVz1DIhQ==", + "patch": "expo-blur@55.0.18.patch", + "optional": true + }, + "expo-image": { + "version": "55.0.11", + "url": "https://registry.npmjs.org/expo-image/-/expo-image-55.0.11.tgz", + "integrity": "sha512-PVIBYQJW/h1f6Zb9xnoWlgfqyOPVm2yb6eo6ZogaKbvMrhb/Q/fiERbagi4oqmR6IPljWPEpkXXQyFBUh7TjpQ==", + "patch": "expo-image@55.0.11.patch", + "optional": true + }, + "expo-symbols": { + "version": "55.0.9", + "url": "https://registry.npmjs.org/expo-symbols/-/expo-symbols-55.0.9.tgz", + "integrity": "sha512-F85C/8ExQjd2gYjasLVKMT8wPj+1+19TVTqg4jAeVjVZklqiQtLO72io9Ji1xAjYNgmDeUI0diVHlFMMTC4Ekg==", + "patch": "expo-symbols@55.0.9.patch", + "optional": true + }, + "@react-native-masked-view/masked-view": { + "version": "0.3.2", + "url": "https://registry.npmjs.org/@react-native-masked-view/masked-view/-/masked-view-0.3.2.tgz", + "integrity": "sha512-XwuQoW7/GEgWRMovOQtX3A4PrXhyaZm0lVUiY8qJDvdngjLms9Cpdck6SmGAUNqQwcj2EadHC1HwL0bEyoa/SQ==", + "patch": "react-native-masked-view-masked-view@0.3.2.patch", + "optional": true + }, + "expo-glass-effect": { + "version": "55.0.11", + "url": "https://registry.npmjs.org/expo-glass-effect/-/expo-glass-effect-55.0.11.tgz", + "integrity": "sha512-wqq7GUOqSkfoFJzreZvBG0jzjsq5c582m3glhWSjcmIuByxXXWp6j6GY6hyFuYKzpOXhbuvusVxGCQi0yWnp3g==", + "patch": "expo-glass-effect@55.0.11.patch", + "optional": true + }, + "expo-system-ui": { + "version": "55.0.22", + "url": "https://registry.npmjs.org/expo-system-ui/-/expo-system-ui-55.0.22.tgz", + "integrity": "sha512-/ma8XGdwXvWkeZN++pHQt66jEQ4ZsaV9B5nP/+PAmXa4hAukPxG2bXjZaNOOOlTssWlaJRclJgGeC+Wo2uJlXQ==", + "patch": "expo-system-ui@55.0.22.patch", + "optional": true + }, + "expo-image-picker": { + "version": "55.0.24", + "url": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-55.0.24.tgz", + "integrity": "sha512-scbtvQIgiqiOxQx/lFEocJkWPzYANHjwFLnzfKjdZNWh3RJjjMmtHaEoeu7lM9RzqE3EwF+JQecgy+FATXNfiQ==", + "patch": "expo-image-picker@55.0.24.patch", + "optional": true + }, + "expo-image-manipulator": { + "version": "55.0.21", + "url": "https://registry.npmjs.org/expo-image-manipulator/-/expo-image-manipulator-55.0.21.tgz", + "integrity": "sha512-BlVl692hVDKuWzOCnbUsfp1u0XqujOABm/Kv3ISMg0qRMchvD6vUSJ/XOIj80+6B9CCm1hNgklgeJ0SIEHiCVQ==", + "patch": "expo-image-manipulator@55.0.21.patch", + "optional": true + }, + "expo-location": { + "version": "55.1.15", + "url": "https://registry.npmjs.org/expo-location/-/expo-location-55.1.15.tgz", + "integrity": "sha512-4wTDpVZg2/ZH4FnLkobY+PeIrfPz0uFTQqx7LIkxgviucNcRUPGstODBd+XRBOd8mIU1tN1vf6TSs8qEzOOJAw==", + "patch": "expo-location@55.1.15.patch", + "optional": true + }, + "@react-native-community/datetimepicker": { + "version": "9.2.2", + "url": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-9.2.2.tgz", + "integrity": "sha512-0cZGrK61UZ8VxpKKV/lun/L/b5wKus55IhvqqdewUayHwIc73lrLI0l5CIlHw8tA0/yQEzHEMEdtHQglAD+hKA==", + "patch": "react-native-community-datetimepicker@9.2.2.patch", + "optional": true + }, + "react-native-keyboard-controller": { + "version": "1.22.9", + "url": "https://registry.npmjs.org/react-native-keyboard-controller/-/react-native-keyboard-controller-1.22.9.tgz", + "integrity": "sha512-nLoXvTbIy/9cI4p/bAXUP7Vfi6Mf2Q7g/gsh2H29C7hgUSQQLdoQjl8inhKC25oxaEibB0CMR++h3HP/k/+BVw==", + "patch": "react-native-keyboard-controller@1.22.9.patch", + "optional": true + }, + "@sentry/react-native": { + "version": "8.32.0", + "url": "https://registry.npmjs.org/@sentry/react-native/-/react-native-8.32.0.tgz", + "integrity": "sha512-Z7W2t0oDrm46JU+FgdB0AL+Ru5urBVmFhYl/RjuPi2AnAjddmbNyDZGzpOLSYtl6oWqqz2u4zGuj3ZFjrCMJOg==", + "patch": "sentry-react-native@8.32.0.patch", + "optional": true + }, + "react-native-graph": { + "version": "1.4.0", + "url": "https://registry.npmjs.org/react-native-graph/-/react-native-graph-1.4.0.tgz", + "integrity": "sha512-Mh0P3kXWrbg9WxPAgce3a+XzKSE36sE9lSi+VwXw/5Ps1VChsa4eApI8LHqpcdvlWH7qjCNk5cSdFkGDuA8Kgw==", + "patch": "react-native-graph@1.4.0.patch", + "optional": true } } diff --git a/patches/expo-application@55.0.19.patch b/patches/expo-application@55.0.19.patch new file mode 100644 index 000000000..7292a8fb3 --- /dev/null +++ b/patches/expo-application@55.0.19.patch @@ -0,0 +1,54 @@ +diff --git a/ios/ApplicationModule.swift b/ios/ApplicationModule.swift +index 30eebcb..9626a66 100644 +--- a/ios/ApplicationModule.swift ++++ b/ios/ApplicationModule.swift +@@ -24,13 +24,23 @@ public class ApplicationModule: Module { + } + + AsyncFunction("getIosIdForVendorAsync") { () -> String? in ++ #if os(macOS) ++ // macOS has no identifier for vendor. ++ return nil ++ #else + return UIDevice.current.identifierForVendor?.uuidString ++ #endif + } + + AsyncFunction("getInstallationTimeAsync") { () -> Double in ++ #if os(macOS) ++ // ~/Documents is the user's folder on a Mac (shared and privacy-protected); the app bundle is the installation. ++ let urlToDocumentsFolder = Bundle.main.bundleURL ++ #else + guard let urlToDocumentsFolder = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last else { + throw UrlDocumentDirectoryException() + } ++ #endif + + do { + let fileAttributes = try FileManager.default.attributesOfItem(atPath: urlToDocumentsFolder.path) +diff --git a/ios/EXApplication/EXProvisioningProfile.m b/ios/EXApplication/EXProvisioningProfile.m +index 5c06b26..b88d888 100644 +--- a/ios/EXApplication/EXProvisioningProfile.m ++++ b/ios/EXApplication/EXProvisioningProfile.m +@@ -42,6 +42,9 @@ - (EXAppReleaseType)appReleaseType { + // provisioning profile does not exist + #if TARGET_IPHONE_SIMULATOR + return EXAppReleaseSimulator; ++#elif TARGET_OS_OSX ++ // Mac apps carry embedded.provisionprofile, not embedded.mobileprovision. ++ return EXAppReleaseTypeUnknown; + #else + return EXAppReleaseAppStore; + #endif +diff --git a/ios/EXApplication.podspec b/ios/EXApplication.podspec +index 3c178ae..6f4e6d7 100644 +--- a/ios/EXApplication.podspec ++++ b/ios/EXApplication.podspec +@@ -12,6 +12,7 @@ Pod::Spec.new do |s| + s.homepage = package['homepage'] + s.platforms = { + :ios => '15.1', ++ :osx => '14.0', + :tvos => '15.1' + } + s.swift_version = '5.9' diff --git a/patches/expo-blur@55.0.18.patch b/patches/expo-blur@55.0.18.patch new file mode 100644 index 000000000..5d0e1263f --- /dev/null +++ b/patches/expo-blur@55.0.18.patch @@ -0,0 +1,130 @@ +diff --git a/ios/BlurEffectView.swift b/ios/BlurEffectView.swift +index 185f69a..b1e0ed6 100644 +--- a/ios/BlurEffectView.swift ++++ b/ios/BlurEffectView.swift +@@ -1,8 +1,41 @@ + // Copyright 2015-present 650 Industries. All rights reserved. + +-import UIKit + import ExpoModulesCore + ++#if os(macOS) ++import AppKit ++ ++/** ++ AppKit has no blur radius: intensity fades the material in, and the tint picks a material and appearance. ++ */ ++final class BlurEffectView: NSVisualEffectView { ++ @Clamping(lowerBound: 0.01, upperBound: 1) ++ var intensity: Double = 0.5 { ++ didSet { ++ alphaValue = CGFloat(intensity) ++ } ++ } ++ ++ var tint: TintStyle = .light { ++ didSet { ++ material = tint.toMaterial() ++ appearance = tint.toAppearance() ++ } ++ } ++ ++ init() { ++ super.init(frame: .zero) ++ blendingMode = .withinWindow ++ state = .active ++ material = TintStyle.default.toMaterial() ++ alphaValue = CGFloat(intensity) ++ } ++ ++ required init?(coder aDecoder: NSCoder) { nil } ++} ++#else ++import UIKit ++ + /** + This class is based on https://gist.github.com/darrarski/29a2a4515508e385c90b3ffe6f975df7 + */ +@@ -57,6 +90,8 @@ final class BlurEffectView: UIVisualEffectView { + } + } + ++#endif ++ + private func isDetoxPresent() -> Bool { + let args = ProcessInfo.processInfo.arguments + +diff --git a/ios/ExpoBlur.podspec b/ios/ExpoBlur.podspec +index e4fb69d..33dcde0 100644 +--- a/ios/ExpoBlur.podspec ++++ b/ios/ExpoBlur.podspec +@@ -12,6 +12,7 @@ Pod::Spec.new do |s| + s.homepage = package['homepage'] + s.platforms = { + :ios => '15.1', ++ :osx => '14.0', + :tvos => '15.1' + } + s.source = { git: 'https://github.com/expo/expo.git' } +diff --git a/ios/ExpoBlurView.swift b/ios/ExpoBlurView.swift +index e0dd06b..b357b6f 100644 +--- a/ios/ExpoBlurView.swift ++++ b/ios/ExpoBlurView.swift +@@ -8,7 +8,11 @@ public final class ExpoBlurView: ExpoView { + public required init(appContext: AppContext? = nil) { + super.init(appContext: appContext) + ++ #if os(macOS) ++ blurEffectView.autoresizingMask = [.width, .height] ++ #else + blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight] ++ #endif + clipsToBounds = true + + addSubview(blurEffectView) +diff --git a/ios/TintStyle.swift b/ios/TintStyle.swift +index ca79ed4..144d5f5 100644 +--- a/ios/TintStyle.swift ++++ b/ios/TintStyle.swift +@@ -23,6 +23,35 @@ public enum TintStyle: String, Enumerable { + case systemThickMaterialDark + case systemChromeMaterialDark + ++#if os(macOS) ++ func toMaterial() -> NSVisualEffectView.Material { ++ switch self { ++ case .systemUltraThinMaterial, .systemUltraThinMaterialLight, .systemUltraThinMaterialDark: ++ return .hudWindow ++ case .systemThinMaterial, .systemThinMaterialLight, .systemThinMaterialDark: ++ return .popover ++ case .systemThickMaterial, .systemThickMaterialLight, .systemThickMaterialDark: ++ return .windowBackground ++ case .systemChromeMaterial, .systemChromeMaterialLight, .systemChromeMaterialDark: ++ return .titlebar ++ case .systemMaterial, .systemMaterialLight, .systemMaterialDark, .regular, .prominent, .default: ++ return .contentBackground ++ case .extraLight, .light, .dark: ++ return .fullScreenUI ++ } ++ } ++ ++ func toAppearance() -> NSAppearance? { ++ switch self { ++ case .dark, .systemUltraThinMaterialDark, .systemThinMaterialDark, .systemMaterialDark, .systemThickMaterialDark, .systemChromeMaterialDark: ++ return NSAppearance(named: .darkAqua) ++ case .extraLight, .light, .systemUltraThinMaterialLight, .systemThinMaterialLight, .systemMaterialLight, .systemThickMaterialLight, .systemChromeMaterialLight: ++ return NSAppearance(named: .aqua) ++ default: ++ return nil ++ } ++ } ++#else + func toBlurEffect() -> UIBlurEffect.Style { + #if os(tvOS) + switch self { +@@ -116,4 +145,5 @@ public enum TintStyle: String, Enumerable { + } + #endif + } ++#endif + } diff --git a/patches/expo-device@55.0.21.patch b/patches/expo-device@55.0.21.patch new file mode 100644 index 000000000..fc8b1c988 --- /dev/null +++ b/patches/expo-device@55.0.21.patch @@ -0,0 +1,149 @@ +diff --git a/ios/DeviceModule.swift b/ios/DeviceModule.swift +index 899b214..c5a3d45 100644 +--- a/ios/DeviceModule.swift ++++ b/ios/DeviceModule.swift +@@ -1,5 +1,9 @@ + import ExpoModulesCore ++#if canImport(UIKit) + import UIKit ++#else ++import SystemConfiguration ++#endif + import MachO + + public class DeviceModule: Module { +@@ -19,15 +23,27 @@ public class DeviceModule: Module { + } + + Constant("modelId") { ++#if os(macOS) ++ sysctlString("hw.model") ++#else + UIDevice.modelIdentifier ++#endif + } + + Constant("modelName") { ++#if os(macOS) ++ nil as String? ++#else + UIDevice.DeviceMap.modelName ++#endif + } + + Constant("deviceYearClass") { ++#if os(macOS) ++ nil as Int? ++#else + UIDevice.DeviceMap.deviceYearClass ++#endif + } + + Constant("totalMemory") { +@@ -35,11 +51,19 @@ public class DeviceModule: Module { + } + + Constant("osName") { ++#if os(macOS) ++ "macOS" ++#else + UIDevice.current.systemName ++#endif + } + + Constant("osVersion") { ++#if os(macOS) ++ ProcessInfo.processInfo.operatingSystemVersion.versionString ++#else + UIDevice.current.systemVersion ++#endif + } + + Constant("osBuildId") { +@@ -51,7 +75,11 @@ public class DeviceModule: Module { + } + + Constant("deviceName") { ++#if os(macOS) ++ SCDynamicStoreCopyComputerName(nil, nil) as String? ++#else + UIDevice.current.name ++#endif + } + + Constant("deviceType") { +@@ -72,12 +100,19 @@ public class DeviceModule: Module { + } + + AsyncFunction("isRootedExperimentalAsync") { () -> Bool in ++#if os(macOS) ++ return false ++#else + return UIDevice.current.isJailbroken ++#endif + } + } + } + + func getDeviceType() -> Int { ++ #if os(macOS) ++ return DeviceType.desktop.rawValue ++ #else + // if it's a macOS Catalyst app + if ProcessInfo.processInfo.isMacCatalystApp { + return DeviceType.desktop.rawValue +@@ -100,6 +135,7 @@ func getDeviceType() -> Int { + default: + return DeviceType.unknown.rawValue + } ++ #endif + } + + func isDevice() -> Bool { +@@ -141,6 +177,20 @@ func cpuArchitectures() -> [String]? { + return [String(cString: archRaw)] + } + ++#if os(macOS) ++func sysctlString(_ name: String) -> String? { ++ var size = 0 ++ guard sysctlbyname(name, nil, &size, nil, 0) == 0, size > 0 else { return nil } ++ var buffer = [CChar](repeating: 0, count: size) ++ guard sysctlbyname(name, &buffer, &size, nil, 0) == 0 else { return nil } ++ return String(cString: buffer) ++} ++ ++extension OperatingSystemVersion { ++ var versionString: String { "\(majorVersion).\(minorVersion).\(patchVersion)" } ++} ++#endif ++ + enum DeviceType: Int { + case unknown = 0 + case phone = 1 +diff --git a/ios/ExpoDevice.podspec b/ios/ExpoDevice.podspec +index 1b0c81f..0283bf0 100644 +--- a/ios/ExpoDevice.podspec ++++ b/ios/ExpoDevice.podspec +@@ -12,6 +12,7 @@ Pod::Spec.new do |s| + s.homepage = package['homepage'] + s.platforms = { + :ios => '15.1', ++ :osx => '14.0', + :tvos => '15.1' + } + s.swift_version = '5.9' +diff --git a/ios/UIDevice.swift b/ios/UIDevice.swift +index 336c648..709c496 100644 +--- a/ios/UIDevice.swift ++++ b/ios/UIDevice.swift +@@ -1,3 +1,4 @@ ++#if !os(macOS) + import UIKit + + struct ExpoDeviceType { +@@ -305,3 +306,4 @@ private struct JailbreakHelper { + ] + } + } ++#endif diff --git a/patches/expo-font@55.0.8.patch b/patches/expo-font@55.0.8.patch new file mode 100644 index 000000000..2b271a663 --- /dev/null +++ b/patches/expo-font@55.0.8.patch @@ -0,0 +1,55 @@ +diff --git a/ios/FontFamilyAliasManager.swift b/ios/FontFamilyAliasManager.swift +index f785185..7766c11 100644 +--- a/ios/FontFamilyAliasManager.swift ++++ b/ios/FontFamilyAliasManager.swift +@@ -67,6 +67,16 @@ private func maybeSwizzleUIFont() { + } else { + log.error("expo-font is unable to swizzle `UIFont.fontNames(forFamilyName:)`") + } ++#else ++ // React Native macOS resolves font families through NSFontManager. ++ let originalMembersMethod = class_getInstanceMethod(NSFontManager.self, #selector(NSFontManager.availableMembers(ofFontFamily:))) ++ let newMembersMethod = class_getInstanceMethod(NSFontManager.self, #selector(NSFontManager._expo_availableMembers(ofFontFamily:))) ++ ++ if let originalMembersMethod, let newMembersMethod { ++ method_exchangeImplementations(originalMembersMethod, newMembersMethod) ++ } else { ++ log.error("expo-font is unable to swizzle `NSFontManager.availableMembers(ofFontFamily:)`") ++ } + #endif + hasSwizzled = true + } +diff --git a/ios/UIFont+FontFamilyAlias.swift b/ios/UIFont+FontFamilyAlias.swift +index a5e3bf1..222a717 100644 +--- a/ios/UIFont+FontFamilyAlias.swift ++++ b/ios/UIFont+FontFamilyAlias.swift +@@ -26,4 +26,29 @@ public extension UIFont { + return fontNames + } + } ++#else ++/** ++ An extension to ``NSFontManager`` that adds a custom implementation of `availableMembers(ofFontFamily:)` that supports aliasing font families. ++ */ ++public extension NSFontManager { ++ /** ++ Returns the members of the specified font family or of the font its alias names. React Native reads the ++ PostScript name, the first element of each member. ++ */ ++ @objc ++ dynamic func _expo_availableMembers(ofFontFamily familyName: String) -> [[Any]]? { ++ // Get members from the original function. ++ let members = _expo_availableMembers(ofFontFamily: familyName) ++ ++ // If no members were found, let's try with the alias. ++ if members?.isEmpty ?? true, let postScriptName = FontFamilyAliasManager.familyName(forAlias: familyName) { ++ if let family = NSFont(name: postScriptName, size: 12)?.familyName, let members = _expo_availableMembers(ofFontFamily: family), !members.isEmpty { ++ return members ++ } ++ return [[postScriptName]] ++ } ++ ++ return members ++ } ++} + #endif diff --git a/patches/expo-glass-effect@55.0.11.patch b/patches/expo-glass-effect@55.0.11.patch new file mode 100644 index 000000000..f0f292ceb --- /dev/null +++ b/patches/expo-glass-effect@55.0.11.patch @@ -0,0 +1,304 @@ +diff --git a/build/GlassContainer.macos.js b/build/GlassContainer.macos.js +new file mode 100644 +index 0000000..7218bec +--- /dev/null ++++ b/build/GlassContainer.macos.js +@@ -0,0 +1,3 @@ ++// macOS uses the native glass views, like iOS. ++export * from './GlassContainer.ios'; ++export { default } from './GlassContainer.ios'; +diff --git a/build/GlassView.macos.js b/build/GlassView.macos.js +new file mode 100644 +index 0000000..181ff73 +--- /dev/null ++++ b/build/GlassView.macos.js +@@ -0,0 +1,3 @@ ++// macOS uses the native glass views, like iOS. ++export * from './GlassView.ios'; ++export { default } from './GlassView.ios'; +diff --git a/build/isGlassEffectAPIAvailable.macos.js b/build/isGlassEffectAPIAvailable.macos.js +new file mode 100644 +index 0000000..561ff57 +--- /dev/null ++++ b/build/isGlassEffectAPIAvailable.macos.js +@@ -0,0 +1,2 @@ ++// macOS uses the native glass views, like iOS. ++export * from './isGlassEffectAPIAvailable.ios'; +diff --git a/build/isLiquidGlassAvailable.macos.js b/build/isLiquidGlassAvailable.macos.js +new file mode 100644 +index 0000000..178f450 +--- /dev/null ++++ b/build/isLiquidGlassAvailable.macos.js +@@ -0,0 +1,2 @@ ++// macOS uses the native glass views, like iOS. ++export * from './isLiquidGlassAvailable.ios'; +diff --git a/ios/ExpoGlassEffect.podspec b/ios/ExpoGlassEffect.podspec +index c05b50d..241f6b2 100644 +--- a/ios/ExpoGlassEffect.podspec ++++ b/ios/ExpoGlassEffect.podspec +@@ -13,6 +13,7 @@ Pod::Spec.new do |s| + s.platforms = { + :ios => '15.1', + :tvos => '15.1', ++ :osx => '14.0', + } + s.source = { git: 'https://github.com/expo/expo.git' } + +diff --git a/ios/GlassContainer.swift b/ios/GlassContainer.swift +index 61fb67c..3866f84 100644 +--- a/ios/GlassContainer.swift ++++ b/ios/GlassContainer.swift +@@ -1,5 +1,6 @@ + // Copyright 2022-present 650 Industries. All rights reserved. + ++#if !os(macOS) + import ExpoModulesCore + + public final class GlassContainer: ExpoView { +@@ -54,3 +55,4 @@ public final class GlassContainer: ExpoView { + childComponentView.removeFromSuperview() + } + } ++#endif +diff --git a/ios/GlassEffectModule.swift b/ios/GlassEffectModule.swift +index e036302..2e47cfa 100644 +--- a/ios/GlassEffectModule.swift ++++ b/ios/GlassEffectModule.swift +@@ -23,6 +23,9 @@ public final class GlassEffectModule: Module { + Constant("isGlassEffectAPIAvailable") { + #if compiler(>=6.2) + if #available(iOS 26.0, tvOS 26.0, macOS 26.0, *) { ++ #if os(macOS) ++ return NSClassFromString("NSGlassEffectView") != nil ++ #endif + guard let glassEffectClass = NSClassFromString("UIGlassEffect") as? NSObject.Type else { + return false + } +diff --git a/ios/GlassStyle.swift b/ios/GlassStyle.swift +index bfd913c..239e7d2 100644 +--- a/ios/GlassStyle.swift ++++ b/ios/GlassStyle.swift +@@ -1,12 +1,14 @@ + import ExpoModulesCore ++#if !os(macOS) + import UIKit ++#endif + + public enum GlassStyle: String, Enumerable { + case clear + case regular + case none + +- #if compiler(>=6.2) // Xcode 26 ++ #if compiler(>=6.2) && !os(macOS) // Xcode 26 + @available(iOS 26.0, tvOS 26.0, macOS 26.0, *) + func toUIGlassEffectStyle() -> UIGlassEffect.Style? { + switch self { +@@ -37,6 +39,18 @@ internal enum GlassColorScheme: String, Enumerable { + case light + case dark + ++ #if os(macOS) ++ func toAppearance() -> NSAppearance? { ++ switch self { ++ case .auto: ++ return nil ++ case .light: ++ return NSAppearance(named: .aqua) ++ case .dark: ++ return NSAppearance(named: .darkAqua) ++ } ++ } ++ #else + func toUIUserInterfaceStyle() -> UIUserInterfaceStyle { + switch self { + case .auto: +@@ -47,4 +61,5 @@ internal enum GlassColorScheme: String, Enumerable { + return .dark + } + } ++ #endif + } +diff --git a/ios/GlassView.swift b/ios/GlassView.swift +index 35cd8f3..23d9a01 100644 +--- a/ios/GlassView.swift ++++ b/ios/GlassView.swift +@@ -1,5 +1,6 @@ + // Copyright 2022-present 650 Industries. All rights reserved. + ++#if !os(macOS) + import ExpoModulesCore + import React + +@@ -279,3 +280,4 @@ public final class GlassView: ExpoView { + childComponentView.removeFromSuperview() + } + } ++#endif +diff --git a/ios/GlassViewMacOS.swift b/ios/GlassViewMacOS.swift +new file mode 100644 +index 0000000..3e3816c +--- /dev/null ++++ b/ios/GlassViewMacOS.swift +@@ -0,0 +1,161 @@ ++// macOS: NSGlassEffectView (macOS 26) hosts the children; earlier systems show them without glass. ++#if os(macOS) ++import ExpoModulesCore ++ ++// React frames children from the top-left, so their container must be flipped. ++final class GlassContentView: NSView { ++ override var isFlipped: Bool { true } ++} ++ ++public final class GlassView: ExpoView { ++ private let contentView = GlassContentView() ++ private var glassView: NSView? ++ private var glassStyle: GlassStyle = .regular ++ private var glassTintColor: NSColor? ++ private var radius: CGFloat? ++ private var cornerRadii = [String: CGFloat]() ++ ++ public required init(appContext: AppContext? = nil) { ++ super.init(appContext: appContext) ++ contentView.autoresizingMask = [.width, .height] ++ #if compiler(>=6.2) ++ if #available(macOS 26.0, *) { ++ let glass = NSGlassEffectView(frame: bounds) ++ glass.autoresizingMask = [.width, .height] ++ glass.contentView = contentView ++ glassView = glass ++ addSubview(glass) ++ return ++ } ++ #endif ++ addSubview(contentView) ++ } ++ ++ // Children are framed by React; size the glass and its content to this view explicitly. ++ public override func layoutSubviews() { ++ super.layoutSubviews() ++ glassView?.frame = bounds ++ contentView.frame = glassView?.bounds ?? bounds ++ } ++ ++ func setGlassStyle(_ config: GlassEffectStyleConfig) { ++ setGlassStyle(config.style) ++ } ++ ++ func setGlassStyle(_ style: GlassStyle) { ++ glassStyle = style ++ updateEffect() ++ } ++ ++ func setTintColor(_ color: NSColor?) { ++ glassTintColor = color ++ updateEffect() ++ } ++ ++ // AppKit glass has no interactive variant. ++ func setInteractive(_ interactive: Bool) {} ++ ++ func setColorScheme(_ colorScheme: GlassColorScheme) { ++ glassView?.appearance = colorScheme.toAppearance() ++ } ++ ++ // NSGlassEffectView has one corner radius; per-corner radii use the largest given. ++ func setBorderRadius(_ radius: CGFloat?) { ++ self.radius = radius ++ updateEffect() ++ } ++ func setBorderCurve(_: String?) {} ++ func setBorderBottomLeftRadius(_ radius: CGFloat?) { setCornerRadius("bottomLeft", radius) } ++ func setBorderBottomRightRadius(_ radius: CGFloat?) { setCornerRadius("bottomRight", radius) } ++ func setBorderTopLeftRadius(_ radius: CGFloat?) { setCornerRadius("topLeft", radius) } ++ func setBorderTopRightRadius(_ radius: CGFloat?) { setCornerRadius("topRight", radius) } ++ func setBorderBottomStartRadius(_ radius: CGFloat?) { setCornerRadius("bottomStart", radius) } ++ func setBorderBottomEndRadius(_ radius: CGFloat?) { setCornerRadius("bottomEnd", radius) } ++ func setBorderTopStartRadius(_ radius: CGFloat?) { setCornerRadius("topStart", radius) } ++ func setBorderTopEndRadius(_ radius: CGFloat?) { setCornerRadius("topEnd", radius) } ++ ++ private func setCornerRadius(_ corner: String, _ value: CGFloat?) { ++ cornerRadii[corner] = value ++ updateEffect() ++ } ++ ++ private func updateEffect() { ++ #if compiler(>=6.2) ++ if #available(macOS 26.0, *), let glass = glassView as? NSGlassEffectView { ++ // `none` keeps the children but drops the glass. ++ if glassStyle == .none { ++ glass.contentView = nil ++ contentView.frame = bounds ++ addSubview(contentView) ++ } else if glass.contentView !== contentView { ++ contentView.removeFromSuperview() ++ glass.contentView = contentView ++ } ++ glass.isHidden = glassStyle == .none ++ glass.style = glassStyle == .clear ? .clear : .regular ++ glass.tintColor = glassTintColor ++ glass.cornerRadius = radius ?? cornerRadii.values.max() ?? 0 ++ } ++ #endif ++ } ++ ++ public override func mountChildComponentView(_ childComponentView: UIView, index: Int) { ++ if index < contentView.subviews.count { ++ contentView.addSubview(childComponentView, positioned: .below, relativeTo: contentView.subviews[index]) ++ } else { ++ contentView.addSubview(childComponentView) ++ } ++ } ++ ++ public override func unmountChildComponentView(_ childComponentView: UIView, index: Int) { ++ childComponentView.removeFromSuperview() ++ } ++} ++ ++public final class GlassContainer: ExpoView { ++ private let contentView = GlassContentView() ++ private var containerView: NSView? ++ ++ public required init(appContext: AppContext? = nil) { ++ super.init(appContext: appContext) ++ contentView.autoresizingMask = [.width, .height] ++ #if compiler(>=6.2) ++ if #available(macOS 26.0, *) { ++ let container = NSGlassEffectContainerView(frame: bounds) ++ container.autoresizingMask = [.width, .height] ++ container.contentView = contentView ++ containerView = container ++ addSubview(container) ++ return ++ } ++ #endif ++ addSubview(contentView) ++ } ++ ++ public override func layoutSubviews() { ++ super.layoutSubviews() ++ containerView?.frame = bounds ++ contentView.frame = bounds ++ } ++ ++ func setSpacing(_ spacing: CGFloat?) { ++ #if compiler(>=6.2) ++ if #available(macOS 26.0, *), let container = containerView as? NSGlassEffectContainerView { ++ container.spacing = spacing ?? 0 ++ } ++ #endif ++ } ++ ++ public override func mountChildComponentView(_ childComponentView: UIView, index: Int) { ++ if index < contentView.subviews.count { ++ contentView.addSubview(childComponentView, positioned: .below, relativeTo: contentView.subviews[index]) ++ } else { ++ contentView.addSubview(childComponentView) ++ } ++ } ++ ++ public override func unmountChildComponentView(_ childComponentView: UIView, index: Int) { ++ childComponentView.removeFromSuperview() ++ } ++} ++#endif diff --git a/patches/expo-haptics@55.0.18.patch b/patches/expo-haptics@55.0.18.patch new file mode 100644 index 000000000..552d5b59e --- /dev/null +++ b/patches/expo-haptics@55.0.18.patch @@ -0,0 +1,54 @@ +diff --git a/ios/ExpoHaptics.podspec b/ios/ExpoHaptics.podspec +index a7f5bc7..93bb683 100644 +--- a/ios/ExpoHaptics.podspec ++++ b/ios/ExpoHaptics.podspec +@@ -11,7 +11,8 @@ Pod::Spec.new do |s| + s.author = package['author'] + s.homepage = package['homepage'] + s.platforms = { +- :ios => '15.1' ++ :ios => '15.1', ++ :osx => '14.0' + } + s.swift_version = '5.9' + s.source = { git: 'https://github.com/expo/expo.git' } +diff --git a/ios/HapticsModule.swift b/ios/HapticsModule.swift +index 41659a0..e07500b 100644 +--- a/ios/HapticsModule.swift ++++ b/ios/HapticsModule.swift +@@ -1,5 +1,30 @@ + import ExpoModulesCore + ++#if os(macOS) ++import AppKit ++ ++// Macs give haptic feedback through Force Touch trackpads, while the user's finger rests on one. ++public class HapticsModule: Module { ++ public func definition() -> ModuleDefinition { ++ Name("ExpoHaptics") ++ ++ AsyncFunction("notificationAsync") { (_: String) in ++ NSHapticFeedbackManager.defaultPerformer.perform(.levelChange, performanceTime: .now) ++ } ++ .runOnQueue(.main) ++ ++ AsyncFunction("impactAsync") { (_: String) in ++ NSHapticFeedbackManager.defaultPerformer.perform(.generic, performanceTime: .now) ++ } ++ .runOnQueue(.main) ++ ++ AsyncFunction("selectionAsync") { ++ NSHapticFeedbackManager.defaultPerformer.perform(.alignment, performanceTime: .now) ++ } ++ .runOnQueue(.main) ++ } ++} ++#else + public class HapticsModule: Module { + public func definition() -> ModuleDefinition { + Name("ExpoHaptics") +@@ -66,3 +91,4 @@ public class HapticsModule: Module { + } + } + } ++#endif diff --git a/patches/expo-image-manipulator@55.0.21.patch b/patches/expo-image-manipulator@55.0.21.patch new file mode 100644 index 000000000..415740f70 --- /dev/null +++ b/patches/expo-image-manipulator@55.0.21.patch @@ -0,0 +1,312 @@ +diff --git a/ios/ExpoImageManipulator.podspec b/ios/ExpoImageManipulator.podspec +index ea6d6dc..5e9de70 100644 +--- a/ios/ExpoImageManipulator.podspec ++++ b/ios/ExpoImageManipulator.podspec +@@ -12,7 +12,8 @@ Pod::Spec.new do |s| + s.homepage = package['homepage'] + s.platforms = { + :ios => '15.1', +- :tvos => '15.1' ++ :tvos => '15.1', ++ :osx => '14.0' + } + s.swift_version = '5.9' + s.source = { git: 'https://github.com/expo/expo.git' } +diff --git a/ios/ImageManipulatorMacOS.swift b/ios/ImageManipulatorMacOS.swift +new file mode 100644 +index 0000000..329b05c +--- /dev/null ++++ b/ios/ImageManipulatorMacOS.swift +@@ -0,0 +1,154 @@ ++// Copyright 2024-present 650 Industries. All rights reserved. ++ ++// macOS: NSImage stands in for UIImage (ExpoModulesCore aliases it), sized in pixels at scale 1. ++#if os(macOS) ++import AppKit ++import ImageIO ++ ++// Files that don't import ExpoModulesCore don't see its UIImage alias. ++typealias UIImage = NSImage ++ ++extension NSImage { ++ // Decoded images are already oriented up, so only `.up` occurs. ++ enum Orientation { ++ case up, down, left, right, upMirrored, downMirrored, leftMirrored, rightMirrored ++ } ++ ++ var imageOrientation: Orientation { .up } ++ var scale: CGFloat { 1 } ++ var cgImage: CGImage? { cgImage(forProposedRect: nil, context: nil, hints: nil) } ++ ++ convenience init(cgImage: CGImage) { ++ self.init(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height)) ++ } ++ ++ convenience init(cgImage: CGImage, scale: CGFloat, orientation: Orientation) { ++ self.init(cgImage: cgImage) ++ } ++ ++ func jpegData(compressionQuality: CGFloat) -> Data? { ++ cgImage.flatMap { NSBitmapImageRep(cgImage: $0).representation(using: .jpeg, properties: [.compressionFactor: compressionQuality]) } ++ } ++ ++ func pngData() -> Data? { ++ cgImage.flatMap { NSBitmapImageRep(cgImage: $0).representation(using: .png, properties: [:]) } ++ } ++} ++ ++// Applies EXIF orientation while decoding, as UIImage(data:) does on iOS. ++internal func decodeImage(_ data: Data) -> NSImage? { ++ guard let source = CGImageSourceCreateWithData(data as CFData, nil), ++ let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any], ++ let width = properties[kCGImagePropertyPixelWidth] as? Int, ++ let height = properties[kCGImagePropertyPixelHeight] as? Int else { ++ return nil ++ } ++ let options: [CFString: Any] = [ ++ kCGImageSourceCreateThumbnailFromImageAlways: true, ++ kCGImageSourceCreateThumbnailWithTransform: true, ++ kCGImageSourceThumbnailMaxPixelSize: max(width, height) ++ ] ++ return CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary).map { NSImage(cgImage: $0) } ++} ++ ++internal func loadImageFromPhotoLibrary(url: URL) async throws -> NSImage { ++ throw ImageNotFoundException() ++} ++ ++// Spark's host exports SparkNamespace(); look it up at runtime so this keeps no link dependency on Spark. ++internal let appNamespace: String = { ++ typealias Namespace = @convention(c) () -> Unmanaged<NSString> ++ if let symbol = dlsym(UnsafeMutableRawPointer(bitPattern: -2), "SparkNamespace") { ++ return unsafeBitCast(symbol, to: Namespace.self)().takeUnretainedValue() as String ++ } ++ return Bundle.main.bundleIdentifier ?? ProcessInfo.processInfo.processName ++}() ++ ++// A bitmap context in CoreGraphics' own orientation: origin bottom-left, so CGImages draw upright. ++private func render(size: CGSize, drawing: (CGContext) -> Void) -> NSImage? { ++ guard let context = CGContext( ++ data: nil, ++ width: Int(size.width), ++ height: Int(size.height), ++ bitsPerComponent: 8, ++ bytesPerRow: 0, ++ space: CGColorSpaceCreateDeviceRGB(), ++ bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue ++ ) else { ++ return nil ++ } ++ drawing(context) ++ return context.makeImage().map { NSImage(cgImage: $0) } ++} ++ ++extension ImageFlipTransformer { ++ func transform(image: NSImage) async throws -> NSImage { ++ guard let cgImage = image.cgImage else { ++ throw ImageNotFoundException() ++ } ++ let size = CGSize(width: cgImage.width, height: cgImage.height) ++ let flipped = render(size: size) { context in ++ switch flip { ++ case .vertical: ++ context.concatenate(CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: size.height)) ++ case .horizontal: ++ context.concatenate(CGAffineTransform(a: -1, b: 0, c: 0, d: 1, tx: size.width, ty: 0)) ++ } ++ context.draw(cgImage, in: CGRect(origin: .zero, size: size)) ++ } ++ guard let flipped else { ++ throw ImageDrawingFailedException() ++ } ++ return flipped ++ } ++} ++ ++extension ImageRotateTransformer { ++ func transform(image: NSImage) async throws -> NSImage { ++ guard let cgImage = image.cgImage else { ++ throw ImageNotFoundException() ++ } ++ let rads = rotate * .pi / 180 ++ let size = CGSize(width: cgImage.width, height: cgImage.height) ++ let bounds = CGRect(origin: .zero, size: size).applying(CGAffineTransform(rotationAngle: rads)) ++ let rotatedSize = CGSize(width: bounds.width.rounded(.down), height: bounds.height.rounded(.down)) ++ let rotated = render(size: rotatedSize) { context in ++ context.translateBy(x: rotatedSize.width / 2, y: rotatedSize.height / 2) ++ // Positive angles turn clockwise, as on iOS; CoreGraphics turns counterclockwise. ++ context.rotate(by: -rads) ++ context.draw(cgImage, in: CGRect(origin: CGPoint(x: -size.width / 2, y: -size.height / 2), size: size)) ++ } ++ guard let rotated else { ++ throw ImageDrawingFailedException() ++ } ++ return rotated ++ } ++} ++ ++extension ImageResizeTransformer { ++ func transform(image: NSImage) async throws -> NSImage { ++ guard let cgImage = image.cgImage else { ++ throw ImageNotFoundException() ++ } ++ let imageRatio = CGFloat(cgImage.width) / CGFloat(cgImage.height) ++ var targetSize = CGSize.zero ++ ++ if let width = options.width { ++ targetSize.width = width ++ targetSize.height = width / imageRatio ++ } ++ if let height = options.height { ++ targetSize.height = height ++ targetSize.width = targetSize.width == 0 ? imageRatio * targetSize.height : targetSize.width ++ } ++ let resized = render(size: targetSize) { context in ++ context.interpolationQuality = .high ++ context.draw(cgImage, in: CGRect(origin: .zero, size: targetSize)) ++ } ++ guard let resized else { ++ throw ImageDrawingFailedException() ++ } ++ return resized ++ } ++} ++#endif +diff --git a/ios/ImageManipulatorModule.swift b/ios/ImageManipulatorModule.swift +index 1f8c73a..ca0588c 100644 +--- a/ios/ImageManipulatorModule.swift ++++ b/ios/ImageManipulatorModule.swift +@@ -2,7 +2,9 @@ + + import CoreGraphics + import Photos ++#if !os(macOS) + import UIKit ++#endif + import ExpoModulesCore + internal import SDWebImageWebPCoder + +diff --git a/ios/ImageManipulatorUtils.swift b/ios/ImageManipulatorUtils.swift +index 13739d5..b5d1d92 100644 +--- a/ios/ImageManipulatorUtils.swift ++++ b/ios/ImageManipulatorUtils.swift +@@ -9,7 +9,7 @@ internal typealias SaveImageResult = (url: URL, data: Data) + */ + internal func loadImage(atUrl url: URL, appContext: AppContext) async throws -> UIImage { + if url.scheme == "data" { +- guard let data = try? Data(contentsOf: url), let image = UIImage(data: data) else { ++ guard let data = try? Data(contentsOf: url), let image = decodeImage(data) else { + throw CorruptedImageDataException() + } + return image +@@ -35,12 +35,17 @@ internal func loadImage(atUrl url: URL, appContext: AppContext) async throws -> + } + + // Read local files directly so UIKit preserves HEIC/EXIF orientation metadata. +- guard let data = try? Data(contentsOf: url), let image = UIImage(data: data) else { ++ guard let data = try? Data(contentsOf: url), let image = decodeImage(data) else { + throw FileSystemReadPermissionException(url.absoluteString) + } + return image + } + ++#if !os(macOS) ++internal func decodeImage(_ data: Data) -> UIImage? { ++ return UIImage(data: data) ++} ++ + /** + Loads the image from user's photo library. + */ +@@ -67,6 +72,8 @@ internal func loadImageFromPhotoLibrary(url: URL) async throws -> UIImage { + } + } + ++#endif ++ + /** + Returns pixel data representation of the image. + */ +@@ -81,6 +88,7 @@ func imageData(from image: UIImage, format: ImageFormat, compression: Double) -> + } + } + ++#if !os(macOS) + /** + Checks if we are dealing with a ph asset URL and uses the correct method to fetch it. + */ +@@ -106,6 +114,7 @@ internal func drawInNewContext(size: CGSize, drawing: (UIGraphicsImageRendererCo + drawing(context) + } + } ++#endif + + /** + Saves the image as a file. +@@ -115,7 +124,12 @@ internal func saveImage(_ image: UIImage, options: ManipulateOptions, appContext + throw FileSystemNotFoundException() + } + ++ #if os(macOS) ++ // macOS shares the user's Caches folder between apps, so each app gets its own subfolder. ++ let directory = URL(fileURLWithPath: cachesDirectory.path).appendingPathComponent(appNamespace).appendingPathComponent("ImageManipulator") ++ #else + let directory = URL(fileURLWithPath: cachesDirectory.path).appendingPathComponent("ImageManipulator") ++ #endif + let filename = UUID().uuidString.appending(options.format.fileExtension) + let fileUrl = directory.appendingPathComponent(filename) + +diff --git a/ios/Transformers/ImageFlipTransformer.swift b/ios/Transformers/ImageFlipTransformer.swift +index 2272c13..a64a3a8 100644 +--- a/ios/Transformers/ImageFlipTransformer.swift ++++ b/ios/Transformers/ImageFlipTransformer.swift +@@ -6,6 +6,7 @@ + internal struct ImageFlipTransformer: ImageTransformer { + let flip: FlipType + ++#if !os(macOS) + @MainActor + func transform(image: UIImage) async -> UIImage { + let imageView = UIImageView(image: image) +@@ -22,4 +23,5 @@ internal struct ImageFlipTransformer: ImageTransformer { + imageView.layer.render(in: context.cgContext) + } + } ++#endif + } +diff --git a/ios/Transformers/ImageResizeTransformer.swift b/ios/Transformers/ImageResizeTransformer.swift +index e3ef679..1b578a4 100644 +--- a/ios/Transformers/ImageResizeTransformer.swift ++++ b/ios/Transformers/ImageResizeTransformer.swift +@@ -6,6 +6,7 @@ + internal struct ImageResizeTransformer: ImageTransformer { + let options: ResizeOptions + ++#if !os(macOS) + func transform(image: UIImage) async -> UIImage { + let imageWidth = image.size.width + let imageHeight = image.size.height +@@ -31,4 +32,5 @@ internal struct ImageResizeTransformer: ImageTransformer { + image.draw(in: CGRect(origin: .zero, size: targetSize)) + } + } ++#endif + } +diff --git a/ios/Transformers/ImageRotateTransformer.swift b/ios/Transformers/ImageRotateTransformer.swift +index 66aa5cb..158eaf0 100644 +--- a/ios/Transformers/ImageRotateTransformer.swift ++++ b/ios/Transformers/ImageRotateTransformer.swift +@@ -6,6 +6,7 @@ + internal struct ImageRotateTransformer: ImageTransformer { + let rotate: Double + ++#if !os(macOS) + @MainActor + func transform(image: UIImage) async throws -> UIImage { + guard let cgImage = image.cgImage else { +@@ -27,4 +28,5 @@ internal struct ImageRotateTransformer: ImageTransformer { + cgContext.draw(cgImage, in: CGRect(origin: origin, size: image.size)) + } + } ++#endif + } diff --git a/patches/expo-image-picker@55.0.24.patch b/patches/expo-image-picker@55.0.24.patch new file mode 100644 index 000000000..cc979ac44 --- /dev/null +++ b/patches/expo-image-picker@55.0.24.patch @@ -0,0 +1,327 @@ +diff --git a/ios/ExpoImagePicker.podspec b/ios/ExpoImagePicker.podspec +index c9bac13..348e96e 100644 +--- a/ios/ExpoImagePicker.podspec ++++ b/ios/ExpoImagePicker.podspec +@@ -11,7 +11,8 @@ Pod::Spec.new do |s| + s.author = package['author'] + s.homepage = package['homepage'] + s.platforms = { +- :ios => '15.1' ++ :ios => '15.1', ++ :osx => '14.0' + } + s.swift_version = '5.9' + s.source = { git: 'https://github.com/expo/expo.git' } +diff --git a/ios/ImagePickerExceptions.swift b/ios/ImagePickerExceptions.swift +index a0810e5..cb6e87c 100644 +--- a/ios/ImagePickerExceptions.swift ++++ b/ios/ImagePickerExceptions.swift +@@ -1,3 +1,4 @@ ++#if !os(macOS) + // Copyright 2022-present 650 Industries. All rights reserved. + + import ExpoModulesCore +@@ -139,3 +140,4 @@ internal final class FailedToPickLivePhotoException: Exception { + "Failed to read the selected item as a live photo" + } + } ++#endif +diff --git a/ios/ImagePickerHandler.swift b/ios/ImagePickerHandler.swift +index 476910d..4e1dd8d 100644 +--- a/ios/ImagePickerHandler.swift ++++ b/ios/ImagePickerHandler.swift +@@ -1,3 +1,4 @@ ++#if !os(macOS) + // Copyright 2022-present 650 Industries. All rights reserved. + + import PhotosUI +@@ -106,3 +107,4 @@ extension PHPickerViewController: PickerUIController { + self.presentationController?.delegate = handler + } + } ++#endif +diff --git a/ios/ImagePickerModule.swift b/ios/ImagePickerModule.swift +index 4b7bf34..f990a2f 100644 +--- a/ios/ImagePickerModule.swift ++++ b/ios/ImagePickerModule.swift +@@ -1,3 +1,4 @@ ++#if !os(macOS) + // Copyright 2022-present 650 Industries. All rights reserved. + + import UIKit +@@ -249,3 +250,4 @@ public class ImagePickerModule: Module, OnMediaPickingResultHandler { + } + } + } ++#endif +diff --git a/ios/ImagePickerModuleMacOS.swift b/ios/ImagePickerModuleMacOS.swift +new file mode 100644 +index 0000000..02df7a3 +--- /dev/null ++++ b/ios/ImagePickerModuleMacOS.swift +@@ -0,0 +1,111 @@ ++// Copyright 2022-present 650 Industries. All rights reserved. ++ ++#if os(macOS) ++import AppKit ++import AVFoundation ++import ExpoModulesCore ++import UniformTypeIdentifiers ++ ++// macOS has no photo library or camera picker UI. The library opens an NSOpenPanel on image and ++// movie files; launchCameraAsync is not defined, so the JavaScript layer reports it unavailable. ++internal struct MacImagePickerOptions: Record { ++ @Field var mediaTypes: [String] = ["images"] ++ @Field var allowsMultipleSelection: Bool = false ++ @Field var selectionLimit: Int = 0 ++ @Field var base64: Bool = false ++} ++ ++public class ImagePickerModule: Module { ++ public func definition() -> ModuleDefinition { ++ Name("ExponentImagePicker") ++ ++ AsyncFunction("getCameraPermissionsAsync") { () -> [String: Any] in ++ Self.cameraPermission(AVCaptureDevice.authorizationStatus(for: .video)) ++ } ++ ++ AsyncFunction("requestCameraPermissionsAsync") { (promise: Promise) in ++ AVCaptureDevice.requestAccess(for: .video) { _ in ++ promise.resolve(Self.cameraPermission(AVCaptureDevice.authorizationStatus(for: .video))) ++ } ++ } ++ ++ // Files the user picks in the open panel need no library permission. ++ AsyncFunction("getMediaLibraryPermissionsAsync") { (_: Bool) -> [String: Any] in ++ Self.permission("granted", canAskAgain: true, extra: ["accessPrivileges": "all"]) ++ } ++ ++ AsyncFunction("requestMediaLibraryPermissionsAsync") { (_: Bool) -> [String: Any] in ++ Self.permission("granted", canAskAgain: true, extra: ["accessPrivileges": "all"]) ++ } ++ ++ AsyncFunction("launchImageLibraryAsync") { (options: MacImagePickerOptions, promise: Promise) in ++ let panel = NSOpenPanel() ++ panel.canChooseDirectories = false ++ panel.allowsMultipleSelection = options.allowsMultipleSelection ++ panel.allowedContentTypes = options.mediaTypes.compactMap { type in ++ switch type { ++ case "images", "livePhotos": return UTType.image ++ case "videos": return UTType.movie ++ default: return nil ++ } ++ } ++ let finish = { (response: NSApplication.ModalResponse) in ++ guard response == .OK else { ++ return promise.resolve(["canceled": true, "assets": NSNull()]) ++ } ++ let urls = options.selectionLimit > 0 ? Array(panel.urls.prefix(options.selectionLimit)) : panel.urls ++ promise.resolve(["canceled": false, "assets": urls.map { Self.asset($0, base64: options.base64) }]) ++ } ++ if let window = NSApp.keyWindow ?? NSApp.mainWindow { ++ panel.beginSheetModal(for: window, completionHandler: finish) ++ } else { ++ panel.begin(completionHandler: finish) ++ } ++ } ++ .runOnQueue(.main) ++ } ++ ++ private static func asset(_ url: URL, base64: Bool) -> [String: Any] { ++ let type = UTType(filenameExtension: url.pathExtension) ++ let isVideo = type?.conforms(to: .movie) ?? false ++ var asset: [String: Any] = [ ++ "assetId": NSNull(), ++ "type": isVideo ? "video" : "image", ++ "uri": url.absoluteString, ++ "fileName": url.lastPathComponent, ++ "fileSize": (try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize) ?? NSNull(), ++ "mimeType": type?.preferredMIMEType ?? NSNull(), ++ "exif": NSNull(), ++ "base64": NSNull(), ++ "width": 0, ++ "height": 0 ++ ] ++ if isVideo { ++ let track = AVURLAsset(url: url).tracks(withMediaType: .video).first ++ let size = track.map { $0.naturalSize.applying($0.preferredTransform) } ?? .zero ++ asset["width"] = abs(size.width) ++ asset["height"] = abs(size.height) ++ asset["duration"] = AVURLAsset(url: url).duration.seconds * 1000 ++ } else if let rep = NSImageRep(contentsOf: url) { ++ asset["width"] = rep.pixelsWide ++ asset["height"] = rep.pixelsHigh ++ if base64, let data = try? Data(contentsOf: url) { ++ asset["base64"] = data.base64EncodedString() ++ } ++ } ++ return asset ++ } ++ ++ private static func cameraPermission(_ status: AVAuthorizationStatus) -> [String: Any] { ++ switch status { ++ case .authorized: return permission("granted", canAskAgain: true) ++ case .notDetermined: return permission("undetermined", canAskAgain: true) ++ default: return permission("denied", canAskAgain: false) ++ } ++ } ++ ++ private static func permission(_ status: String, canAskAgain: Bool, extra: [String: Any] = [:]) -> [String: Any] { ++ ["status": status, "granted": status == "granted", "canAskAgain": canAskAgain, "expires": "never"].merging(extra) { _, new in new } ++ } ++} ++#endif +diff --git a/ios/ImagePickerOptions.swift b/ios/ImagePickerOptions.swift +index bdaf98f..e945b97 100644 +--- a/ios/ImagePickerOptions.swift ++++ b/ios/ImagePickerOptions.swift +@@ -1,3 +1,4 @@ ++#if !os(macOS) + // Copyright 2022-present 650 Industries. All rights reserved. + + import ExpoModulesCore +@@ -257,3 +258,4 @@ internal enum CameraType: String, Enumerable { + case back + case front + } ++#endif +diff --git a/ios/ImagePickerPermissionRequesters.swift b/ios/ImagePickerPermissionRequesters.swift +index ad92115..2679aaf 100644 +--- a/ios/ImagePickerPermissionRequesters.swift ++++ b/ios/ImagePickerPermissionRequesters.swift +@@ -1,3 +1,4 @@ ++#if !os(macOS) + // Copyright 2022-present 650 Industries. All rights reserved. + + import Photos +@@ -122,3 +123,4 @@ extension DefaultMediaLibraryPermissionRequester { + return PHAccessLevel.readWrite + } + } ++#endif +diff --git a/ios/ImagePickerResponse.swift b/ios/ImagePickerResponse.swift +index 21b9bb1..3e693bf 100644 +--- a/ios/ImagePickerResponse.swift ++++ b/ios/ImagePickerResponse.swift +@@ -1,3 +1,4 @@ ++#if !os(macOS) + // Copyright 2022-present 650 Industries. All rights reserved. + + // swiftlint:disable redundant_optional_initialization +@@ -41,3 +42,4 @@ internal struct AssetInfo: Record { + @Field var duration: Double? = nil + @Field var pairedVideoAsset: AssetInfo? = nil + } ++#endif +diff --git a/ios/ImagePickerUtils.swift b/ios/ImagePickerUtils.swift +index cfd0bcb..eb76f4c 100644 +--- a/ios/ImagePickerUtils.swift ++++ b/ios/ImagePickerUtils.swift +@@ -1,3 +1,4 @@ ++#if !os(macOS) + // Copyright 2024-present 650 Industries. All rights reserved. + + /** +@@ -14,3 +15,4 @@ func asyncMap<ItemsType: Sequence, ResultType>( + } + return values + } ++#endif +diff --git a/ios/ImageUtils.swift b/ios/ImageUtils.swift +index d9d1cd4..f09532f 100644 +--- a/ios/ImageUtils.swift ++++ b/ios/ImageUtils.swift +@@ -1,3 +1,4 @@ ++#if !os(macOS) + // Copyright 2024-present 650 Industries. All rights reserved. + + import CoreGraphics +@@ -390,3 +391,4 @@ internal struct ImageUtils { + return CGSize(width: width, height: height) + } + } ++#endif +diff --git a/ios/MediaHandler.swift b/ios/MediaHandler.swift +index a0e3a44..e11aec4 100644 +--- a/ios/MediaHandler.swift ++++ b/ios/MediaHandler.swift +@@ -1,3 +1,4 @@ ++#if !os(macOS) + // Copyright 2022-present 650 Industries. All rights reserved. + + import ExpoModulesCore +@@ -564,3 +565,4 @@ extension PHAssetResourceManager { + } + } + } ++#endif +diff --git a/ios/NSItemProvider+Async.swift b/ios/NSItemProvider+Async.swift +index 08cdf4c..83b2542 100644 +--- a/ios/NSItemProvider+Async.swift ++++ b/ios/NSItemProvider+Async.swift +@@ -1,3 +1,4 @@ ++#if !os(macOS) + import Foundation + import Photos + +@@ -30,3 +31,4 @@ internal extension NSItemProvider { + } + } + } ++#endif +diff --git a/ios/StatusBarVisibilityController.swift b/ios/StatusBarVisibilityController.swift +index 907af58..61cce12 100644 +--- a/ios/StatusBarVisibilityController.swift ++++ b/ios/StatusBarVisibilityController.swift +@@ -1,3 +1,4 @@ ++#if !os(macOS) + // Copyright 2022-present 650 Industries. All rights reserved. + + /** +@@ -50,3 +51,4 @@ internal class StatusBarVisibilityController { + // prints "Hello Fabio!" + } + } ++#endif +diff --git a/ios/UIImage+fixOrientation.swift b/ios/UIImage+fixOrientation.swift +index 22849c2..d005706 100644 +--- a/ios/UIImage+fixOrientation.swift ++++ b/ios/UIImage+fixOrientation.swift +@@ -1,3 +1,4 @@ ++#if !os(macOS) + // Copyright 2022-present 650 Industries. All rights reserved. + + import UIKit +@@ -78,3 +79,4 @@ extension UIImage { + return UIImage(cgImage: resultCgImage) + } + } ++#endif +diff --git a/ios/UIImagePickerControllerExtentsion.swift b/ios/UIImagePickerControllerExtentsion.swift +index 865a59d..9b3ee35 100644 +--- a/ios/UIImagePickerControllerExtentsion.swift ++++ b/ios/UIImagePickerControllerExtentsion.swift +@@ -1,3 +1,4 @@ ++#if !os(macOS) + // Copyright 2016-present 650 Industries. All rights reserved. + + extension UIImagePickerController { +@@ -55,3 +56,4 @@ extension UIImagePickerController { + return nil + } + } ++#endif +diff --git a/ios/VideoUtils.swift b/ios/VideoUtils.swift +index f78c50d..2b88d16 100644 +--- a/ios/VideoUtils.swift ++++ b/ios/VideoUtils.swift +@@ -1,3 +1,4 @@ ++#if !os(macOS) + // Copyright 2024-present 650 Industries. All rights reserved. + + import AVFoundation +@@ -97,3 +98,4 @@ internal struct VideoUtils { + } + } + } ++#endif diff --git a/patches/expo-image@55.0.11.patch b/patches/expo-image@55.0.11.patch new file mode 100644 index 000000000..c06f0e58f --- /dev/null +++ b/patches/expo-image@55.0.11.patch @@ -0,0 +1,158 @@ +diff --git a/src/ExpoImage.macos.tsx b/src/ExpoImage.macos.tsx +new file mode 100644 +index 0000000..cd859ac +--- /dev/null ++++ b/src/ExpoImage.macos.tsx +@@ -0,0 +1,128 @@ ++// expo-image on macOS: React Native's Image stands in for the native view, which is UIKit ++// only. Sources (uri, headers, require(); the largest of several sizes), placeholders while ++// loading, contentFit, tintColor, the load events and reloadAsync work. Its SDWebImage ++// caches, blurhash/thumbhash and SF Symbol sources, contentPosition, transitions and ++// ImageRef sources are not supported; such a source reports onError. ++import React from 'react'; ++import { Image, StyleSheet, View, type ImageProps, type ImageResizeMode } from 'react-native'; ++ ++import type { ImageContentFit, ImageNativeProps, ImageSource } from './Image.types'; ++ ++const fits: Record<ImageContentFit, ImageResizeMode> = { ++ cover: 'cover', ++ contain: 'contain', ++ fill: 'stretch', ++ none: 'center', ++ 'scale-down': 'center', ++}; ++ ++function largest(sources: ImageNativeProps['source']): ImageSource | undefined { ++ if (!Array.isArray(sources)) return undefined; ++ const area = (source: ImageSource) => (source.width ?? 0) * (source.height ?? 0); ++ const drawable = sources.filter(source => source.uri && !/^(blurhash|thumbhash|sf):\//.test(source.uri)); ++ return drawable.reduce<ImageSource | undefined>((best, next) => (best && area(best) >= area(next) ? best : next), undefined); ++} ++ ++// RN Image copies source dimensions into its layout; let contentFit control the size. ++function ImageWithFit({ source, contentFit, style, ...props }: Omit<ImageProps, 'source'> & { ++ source: ImageSource & { scale?: number }; contentFit: ImageContentFit; ++}) { ++ const { uri, headers, scale = 1 } = source; ++ const [size, setSize] = React.useState<{ uri: string; width: number; height: number }>(); ++ React.useEffect(() => { ++ if (contentFit !== 'none' || !uri) return; ++ let active = true; ++ Image.getSizeWithHeaders(uri, headers ?? {}).then(({ width, height }) => { ++ if (active) setSize({ uri, width, height }); ++ }, () => {}); // The Image's onError reports load failures. ++ return () => { active = false; }; ++ }, [uri, headers, contentFit]); ++ const width = size && size.uri === uri ? size.width / scale : (source.width ?? 0); ++ const height = size && size.uri === uri ? size.height / scale : (source.height ?? 0); ++ return <Image {...props} source={{ uri, headers, scale }} resizeMode={fits[contentFit]} style={[ ++ StyleSheet.absoluteFill, style, ++ contentFit === 'none' && { ++ width, height, left: '50%', top: '50%', right: undefined, bottom: undefined, ++ transform: [{ translateX: -width / 2 }, { translateY: -height / 2 }], ++ }, ++ ]} />; ++} ++ ++class ExpoImage extends React.PureComponent<ImageNativeProps, { attempt: number; loaded: boolean }> { ++ state = { attempt: 0, loaded: false }; ++ ++ startAnimating = () => {}; ++ stopAnimating = () => {}; ++ lockResourceAsync = async () => {}; ++ unlockResourceAsync = async () => {}; ++ reloadAsync = async () => this.setState(({ attempt }) => ({ attempt: attempt + 1, loaded: false })); ++ ++ unsupported?: string; ++ ++ componentDidMount() { ++ // Image calls startAnimating and reloadAsync through this ref, which holds the native view on iOS. ++ const ref = this.props.nativeViewRef as React.MutableRefObject<unknown> | undefined; ++ if (ref) ref.current = this; ++ this.reportUnsupported(); ++ } ++ ++ componentDidUpdate() { ++ this.reportUnsupported(); ++ } ++ ++ // A source with nothing to draw still settles, once per source, so load and error handlers run. ++ reportUnsupported() { ++ const { source } = this.props; ++ const key = largest(source) || source == null || (Array.isArray(source) && !source.length) ? undefined : JSON.stringify(source); ++ if (key === this.unsupported) return; ++ this.unsupported = key; ++ if (key === undefined) return; ++ this.props.onLoadStart?.(); ++ this.props.onError?.({ error: 'expo-image cannot draw blurhash, thumbhash, SF Symbol or ImageRef sources on macOS' }); ++ this.props.onLoadEnd?.(); ++ } ++ ++ render() { ++ const { source, placeholder, contentFit = 'cover', placeholderContentFit = 'scale-down', tintColor, style, accessibilityLabel, alt, ++ onLoadStart, onProgress, onLoad, onError, onLoadEnd, onDisplay, containerViewRef, nativeViewRef, ...props } = this.props; ++ const image = largest(source); ++ const fallback = largest(placeholder); ++ const tint = tintColor == null ? null : { tintColor: tintColor as string }; ++ return ( ++ <View ref={containerViewRef as never} {...(props as object)} accessibilityLabel={accessibilityLabel ?? alt} style={[styles.container, style]}> ++ {!this.state.loaded && fallback && ( ++ <ImageWithFit source={fallback} contentFit={placeholderContentFit} style={tint} /> ++ )} ++ {image && ( ++ <ImageWithFit ++ key={this.state.attempt} ++ source={image} ++ contentFit={contentFit} ++ style={tint} ++ onLoadStart={() => onLoadStart?.()} ++ onProgress={event => onProgress?.({ loaded: event.nativeEvent.loaded, total: event.nativeEvent.total })} ++ onLoad={event => { ++ const loaded = (width: number, height: number) => { ++ this.setState({ loaded: true }); ++ onLoad?.({ cacheType: 'none', source: { url: image.uri!, width, height, mediaType: null } }); ++ onLoadEnd?.(); ++ onDisplay?.(); ++ }; ++ // React Native macOS scales a 1x image's size by the screen's; getSize reads its pixels. ++ const { width, height } = event.nativeEvent.source; ++ Image.getSize(image.uri!).then(size => loaded(size.width, size.height), () => loaded(width, height)); ++ }} ++ onError={event => { ++ onError?.({ error: String(event.nativeEvent.error) }); ++ onLoadEnd?.(); ++ }} ++ /> ++ )} ++ </View> ++ ); ++ } ++} ++ ++const styles = StyleSheet.create({ container: { overflow: 'hidden' } }); ++ ++export default ExpoImage; +diff --git a/src/ImageModule.macos.ts b/src/ImageModule.macos.ts +new file mode 100644 +index 0000000..1ffc9e3 +--- /dev/null ++++ b/src/ImageModule.macos.ts +@@ -0,0 +1,18 @@ ++// expo-image on macOS has no native module; see ExpoImage.macos.tsx. ++import { Image } from 'react-native'; ++ ++import type { ImageNativeModule } from './Image.types'; ++ ++const unsupported = (name: string) => () => Promise.reject(new Error(`Image.${name} is not supported on macOS`)); ++ ++export default { ++ Image: class {}, ++ prefetch: (urls: string[]) => Promise.all(urls.map(url => Image.prefetch(url))).then(() => true, () => false), ++ clearMemoryCache: async () => false, ++ clearDiskCache: async () => false, ++ configureCache: () => {}, ++ getCachePathAsync: async () => null, ++ loadAsync: unsupported('loadAsync'), ++ generateBlurhashAsync: unsupported('generateBlurhashAsync'), ++ generateThumbhashAsync: unsupported('generateThumbhashAsync'), ++} as unknown as ImageNativeModule; diff --git a/patches/expo-keep-awake@55.0.8.patch b/patches/expo-keep-awake@55.0.8.patch new file mode 100644 index 000000000..8ce7f6ff7 --- /dev/null +++ b/patches/expo-keep-awake@55.0.8.patch @@ -0,0 +1,41 @@ +diff --git a/ios/KeepAwakeModule.swift b/ios/KeepAwakeModule.swift +index 835b4f1..22aac25 100644 +--- a/ios/KeepAwakeModule.swift ++++ b/ios/KeepAwakeModule.swift +@@ -30,7 +30,9 @@ public final class KeepAwakeModule: Module { + return UIApplication.shared.isIdleTimerDisabled + } + #elseif os(macOS) +- return false ++ return DispatchQueue.main.sync { ++ return displaySleepActivity != nil ++ } + #endif + } + +@@ -48,10 +50,25 @@ public final class KeepAwakeModule: Module { + } + } + ++#if os(macOS) ++// macOS has no idle timer to disable; a process activity holds the display and system awake instead. ++private var displaySleepActivity: NSObjectProtocol? ++#endif ++ + private func setActivated(_ activated: Bool) { + #if os(iOS) || os(tvOS) + DispatchQueue.main.async { + UIApplication.shared.isIdleTimerDisabled = activated + } ++ #elseif os(macOS) ++ DispatchQueue.main.async { ++ if activated, displaySleepActivity == nil { ++ displaySleepActivity = ProcessInfo.processInfo.beginActivity( ++ options: [.idleDisplaySleepDisabled, .idleSystemSleepDisabled], reason: "expo-keep-awake") ++ } else if !activated, let activity = displaySleepActivity { ++ ProcessInfo.processInfo.endActivity(activity) ++ displaySleepActivity = nil ++ } ++ } + #endif + } diff --git a/patches/expo-localization@55.0.19.patch b/patches/expo-localization@55.0.19.patch new file mode 100644 index 000000000..e62546d33 --- /dev/null +++ b/patches/expo-localization@55.0.19.patch @@ -0,0 +1,32 @@ +diff --git a/ios/ExpoLocalization.podspec b/ios/ExpoLocalization.podspec +index 29b720d..117b6a0 100644 +--- a/ios/ExpoLocalization.podspec ++++ b/ios/ExpoLocalization.podspec +@@ -12,6 +12,7 @@ Pod::Spec.new do |s| + s.homepage = package['homepage'] + s.platforms = { + :ios => '15.1', ++ :osx => '14.0', + :tvos => '15.1' + } + s.swift_version = '5.9' +diff --git a/ios/LocalizationModule.swift b/ios/LocalizationModule.swift +index ffc4593..df54b76 100644 +--- a/ios/LocalizationModule.swift ++++ b/ios/LocalizationModule.swift +@@ -8,7 +8,14 @@ let CALENDAR_SETTINGS_CHANGED = "onCalendarSettingsChanged" + + let OBSERVED_EVENTS: Set<Notification.Name> = [ + // swiftlint:disable legacy_objc_type +- UIApplication.significantTimeChangeNotification, ++ // macOS has no significant-time-change notification; a time zone change is the closest. ++ { ++ #if os(macOS) ++ return Notification.Name.NSSystemTimeZoneDidChange ++ #else ++ return UIApplication.significantTimeChangeNotification ++ #endif ++ }(), + NSLocale.currentLocaleDidChangeNotification + // swiftlint:enable legacy_objc_type + ] diff --git a/patches/expo-location@55.1.15.patch b/patches/expo-location@55.1.15.patch new file mode 100644 index 000000000..c1b9c3428 --- /dev/null +++ b/patches/expo-location@55.1.15.patch @@ -0,0 +1,190 @@ +diff --git a/ios/EXLocation.h b/ios/EXLocation.h +index 6e6c661..4954506 100644 +--- a/ios/EXLocation.h ++++ b/ios/EXLocation.h +@@ -17,6 +17,8 @@ typedef NS_ENUM(NSUInteger, EXLocationAccuracy) { + + + (NSDictionary *)exportLocation:(CLLocation *)location; + + (CLLocationAccuracy)CLLocationAccuracyFromOption:(EXLocationAccuracy)accuracy; ++#if !TARGET_OS_OSX + + (CLActivityType)CLActivityTypeFromOption:(NSInteger)activityType; ++#endif + + @end +diff --git a/ios/EXLocation.m b/ios/EXLocation.m +index 0442c37..ac375c6 100644 +--- a/ios/EXLocation.m ++++ b/ios/EXLocation.m +@@ -42,6 +42,7 @@ + (CLLocationAccuracy)CLLocationAccuracyFromOption:(EXLocationAccuracy)accuracy + } + } + ++#if !TARGET_OS_OSX + + (CLActivityType)CLActivityTypeFromOption:(NSInteger)activityType + { + if (activityType >= CLActivityTypeOther && activityType <= CLActivityTypeOtherNavigation) { +@@ -52,6 +53,7 @@ + (CLActivityType)CLActivityTypeFromOption:(NSInteger)activityType + } + return CLActivityTypeOther; + } ++#endif + + @end + +diff --git a/ios/ExpoLocation.podspec b/ios/ExpoLocation.podspec +index df166fe..2214d91 100644 +--- a/ios/ExpoLocation.podspec ++++ b/ios/ExpoLocation.podspec +@@ -11,7 +11,8 @@ Pod::Spec.new do |s| + s.author = package['author'] + s.homepage = package['homepage'] + s.platforms = { +- :ios => '15.1' ++ :ios => '15.1', ++ :osx => '14.0' + } + s.source = { git: 'https://github.com/expo/expo.git' } + s.static_framework = true +diff --git a/ios/Providers/BaseLocationProvider.swift b/ios/Providers/BaseLocationProvider.swift +index ae9135a..98dcce1 100644 +--- a/ios/Providers/BaseLocationProvider.swift ++++ b/ios/Providers/BaseLocationProvider.swift +@@ -9,7 +9,9 @@ internal class BaseLocationProvider: NSObject, CLLocationManagerDelegate { + @MainActor + init(options: LocationOptions) { + super.init() ++ #if !os(macOS) + manager.allowsBackgroundLocationUpdates = false ++ #endif + manager.distanceFilter = options.distanceInterval + manager.desiredAccuracy = options.accuracy.toCLLocationAccuracy() + manager.delegate = self +diff --git a/ios/Providers/DeviceHeadingStreamer.swift b/ios/Providers/DeviceHeadingStreamer.swift +index 802bf57..47d89a3 100644 +--- a/ios/Providers/DeviceHeadingStreamer.swift ++++ b/ios/Providers/DeviceHeadingStreamer.swift +@@ -25,14 +25,19 @@ internal class DeviceHeadingStreamer: BaseStreamer { + } + let stream = DeviceHeadingStream { continuation in + self.continuation = continuation ++ // Macs have no compass; headingAvailable() is false there, so this is never reached. ++ #if !os(macOS) + manager.startUpdatingHeading() ++ #endif + } + headingStream = stream + return stream + } + + override func stopStreaming() { ++ #if !os(macOS) + manager.stopUpdatingHeading() ++ #endif + continuation?.finish() + + headingStream = nil +diff --git a/ios/Requesters/EXBackgroundLocationPermissionRequester.m b/ios/Requesters/EXBackgroundLocationPermissionRequester.m +index aa54f8c..a2e4918 100644 +--- a/ios/Requesters/EXBackgroundLocationPermissionRequester.m ++++ b/ios/Requesters/EXBackgroundLocationPermissionRequester.m +@@ -14,6 +14,12 @@ @interface EXBackgroundLocationPermissionRequester () + + @end + ++#if TARGET_OS_OSX ++#import <AppKit/AppKit.h> ++#define UIApplicationWillResignActiveNotification NSApplicationWillResignActiveNotification ++#define UIApplicationDidBecomeActiveNotification NSApplicationDidBecomeActiveNotification ++#endif ++ + @implementation EXBackgroundLocationPermissionRequester + + - (instancetype)init +diff --git a/ios/Requesters/EXBaseLocationRequester.h b/ios/Requesters/EXBaseLocationRequester.h +index ae63560..d29a8be 100644 +--- a/ios/Requesters/EXBaseLocationRequester.h ++++ b/ios/Requesters/EXBaseLocationRequester.h +@@ -5,6 +5,11 @@ + #import <ExpoModulesCore/EXPermissionsInterface.h> + #import <CoreLocation/CLLocationManagerDelegate.h> + ++#if TARGET_OS_OSX ++// macOS has no when-in-use authorization and never reports it; keep the shared status switches compiling. ++#define kCLAuthorizationStatusAuthorizedWhenInUse ((CLAuthorizationStatus)4) ++#endif ++ + @interface EXBaseLocationRequester : NSObject<EXPermissionsRequester, CLLocationManagerDelegate> + + @property (nonatomic, strong) CLLocationManager *locationManager; +diff --git a/ios/Requesters/EXBaseLocationRequester.m b/ios/Requesters/EXBaseLocationRequester.m +index 350eb5f..c8f6d82 100644 +--- a/ios/Requesters/EXBaseLocationRequester.m ++++ b/ios/Requesters/EXBaseLocationRequester.m +@@ -37,7 +37,12 @@ - (NSDictionary *)getPermissions { + + CLAuthorizationStatus systemStatus; + if (![EXBaseLocationRequester isConfiguredForAlwaysAuthorization] && ![EXBaseLocationRequester isConfiguredForWhenInUseAuthorization]) { ++#if TARGET_OS_OSX ++ // RCTFatal throws in Release; on macOS the missing key reports denied, as it does in Debug. ++ RCTLogError(@"This app is missing NSLocationWhenInUseUsageDescription in its macOS Info.plist, so location services will fail."); ++#else + RCTFatal(RCTErrorWithMessage(@"This app is missing usage descriptions, so location services will fail. Add one of the `NSLocation*UsageDescription` keys to your bundle's Info.plist. See https://bit.ly/3iLqy6S (https://docs.expo.dev/distribution/app-stores/#system-permissions-dialogs-on-ios) for more information.")); ++#endif + systemStatus = kCLAuthorizationStatusDenied; + } else { + systemStatus = [CLLocationManager authorizationStatus]; +diff --git a/ios/TaskConsumers/EXGeofencingTaskConsumer.m b/ios/TaskConsumers/EXGeofencingTaskConsumer.m +index 7e6482e..c94f09e 100644 +--- a/ios/TaskConsumers/EXGeofencingTaskConsumer.m ++++ b/ios/TaskConsumers/EXGeofencingTaskConsumer.m +@@ -71,8 +71,10 @@ - (void)startMonitoringRegionsForTask:(id<EXTaskInterface>)task + self->_regionStates = regionStates; + + locationManager.delegate = self; ++#if !TARGET_OS_OSX + locationManager.allowsBackgroundLocationUpdates = YES; + locationManager.pausesLocationUpdatesAutomatically = NO; ++#endif + + for (NSDictionary *regionDict in regions) { + NSString *identifier = regionDict[@"identifier"] ?: [[NSUUID UUID] UUIDString]; +diff --git a/ios/TaskConsumers/EXLocationTaskConsumer.m b/ios/TaskConsumers/EXLocationTaskConsumer.m +index fe1d9dd..75df305 100644 +--- a/ios/TaskConsumers/EXLocationTaskConsumer.m ++++ b/ios/TaskConsumers/EXLocationTaskConsumer.m +@@ -49,7 +49,9 @@ - (void)didRegisterTask:(id<EXTaskInterface>)task + self->_locationManager = locationManager; + + locationManager.delegate = self; ++#if !TARGET_OS_OSX + locationManager.allowsBackgroundLocationUpdates = YES; ++#endif + + // Set options-specific things in location manager. + [self setOptions:task.options]; +@@ -69,9 +71,11 @@ - (void)setOptions:(NSDictionary *)options + + locationManager.desiredAccuracy = [EXLocation CLLocationAccuracyFromOption:accuracy]; + locationManager.distanceFilter = [self numberToDouble:options[@"distanceInterval"] defaultValue:kCLDistanceFilterNone]; ++#if !TARGET_OS_OSX + locationManager.activityType = [EXLocation CLActivityTypeFromOption:[self numberToInteger:options[@"activityType"] defaultValue:CLActivityTypeOther]]; + locationManager.pausesLocationUpdatesAutomatically = [self numberToBool:options[@"pausesUpdatesAutomatically"] defaultValue:true]; + locationManager.showsBackgroundLocationIndicator = [self numberToBool:options[@"showsBackgroundLocationIndicator"] defaultValue:false]; ++#endif + + [locationManager startUpdatingLocation]; + [locationManager startMonitoringSignificantLocationChanges]; +@@ -145,9 +149,13 @@ - (BOOL)shouldReportDeferredLocations + if (_deferredLocations.count <= 0) { + return NO; + } ++#if TARGET_OS_OSX ++ if (NSApp.isActive) { ++#else + UIApplicationState appState = [[UIApplication sharedApplication] applicationState]; + + if (appState == UIApplicationStateActive) { ++#endif + // Don't defer location updates when app is in foreground state. + return YES; + } diff --git a/patches/expo-symbols@55.0.9.patch b/patches/expo-symbols@55.0.9.patch new file mode 100644 index 000000000..a008486db --- /dev/null +++ b/patches/expo-symbols@55.0.9.patch @@ -0,0 +1,286 @@ +diff --git a/build/SymbolView.macos.js b/build/SymbolView.macos.js +new file mode 100644 +index 0000000..27f2612 +--- /dev/null ++++ b/build/SymbolView.macos.js +@@ -0,0 +1,2 @@ ++// macOS renders SF Symbols natively, like iOS. ++export * from './SymbolView.ios'; +diff --git a/build/materialImageSource.macos.js b/build/materialImageSource.macos.js +new file mode 100644 +index 0000000..770d07d +--- /dev/null ++++ b/build/materialImageSource.macos.js +@@ -0,0 +1,2 @@ ++// macOS renders SF Symbols natively, like iOS. ++export * from './materialImageSource.ios'; +diff --git a/build/utils.macos.js b/build/utils.macos.js +new file mode 100644 +index 0000000..cd5ed0b +--- /dev/null ++++ b/build/utils.macos.js +@@ -0,0 +1,2 @@ ++// macOS renders SF Symbols natively, like iOS. ++export * from './utils.ios'; +diff --git a/ios/ExpoSymbols.podspec b/ios/ExpoSymbols.podspec +index 21fe2d7..a40eae2 100644 +--- a/ios/ExpoSymbols.podspec ++++ b/ios/ExpoSymbols.podspec +@@ -12,7 +12,8 @@ Pod::Spec.new do |s| + s.homepage = package['homepage'] + s.platforms = { + :ios => '15.1', +- :tvos => '15.1' ++ :tvos => '15.1', ++ :osx => '14.0' + } + s.swift_version = '5.9' + s.source = { git: 'https://github.com/expo/expo.git' } +diff --git a/ios/SymbolEffects.swift b/ios/SymbolEffects.swift +index dcfac39..c07d278 100644 +--- a/ios/SymbolEffects.swift ++++ b/ios/SymbolEffects.swift +@@ -1,6 +1,6 @@ + @available(iOS 17.0, tvOS 17.0, *) + internal protocol EffectAdding { +- func add(to view: UIImageView, with options: SymbolEffectOptions) ++ func add(to view: SymbolImageView, with options: SymbolEffectOptions) + } + + @available(iOS 17.0, tvOS 17.0, *) +@@ -9,7 +9,7 @@ internal struct BounceEffect: EffectAdding { + let wholeSymbol: Bool? + let direction: AnimationDirection? + +- func add(to view: UIImageView, with options: SymbolEffectOptions) { ++ func add(to view: SymbolImageView, with options: SymbolEffectOptions) { + var finalEffect = effect + if wholeSymbol ?? false { + finalEffect = finalEffect.wholeSymbol +@@ -28,7 +28,7 @@ internal struct PulseEffect: EffectAdding { + private let effect: PulseSymbolEffect = .pulse + let wholeSymbol: Bool? + +- func add(to view: UIImageView, with options: SymbolEffectOptions) { ++ func add(to view: SymbolImageView, with options: SymbolEffectOptions) { + var finalEffect = effect + if wholeSymbol ?? false { + finalEffect = finalEffect.wholeSymbol +@@ -43,7 +43,7 @@ internal struct ScaleEffect: EffectAdding { + let wholeSymbol: Bool? + let direction: AnimationDirection? + +- func add(to view: UIImageView, with options: SymbolEffectOptions) { ++ func add(to view: SymbolImageView, with options: SymbolEffectOptions) { + var finalEffect = effect + if wholeSymbol ?? false { + finalEffect = finalEffect.wholeSymbol +diff --git a/ios/SymbolModule.swift b/ios/SymbolModule.swift +index 304cde1..fa273d7 100644 +--- a/ios/SymbolModule.swift ++++ b/ios/SymbolModule.swift +@@ -14,7 +14,11 @@ public class SymbolModule: Module { + } + + Prop("scale") { (view, scale: SymbolScale?) in ++ #if os(macOS) ++ view.scale = scale?.imageSymbolScale() ++ #else + view.scale = scale?.imageSymbolScale() ?? .unspecified ++ #endif + } + + Prop("tintColor") { (view, color: UIColor?) in +@@ -34,7 +38,11 @@ public class SymbolModule: Module { + } + + Prop("resizeMode") { (view, resizeMode: SymbolContentMode?) in ++ #if os(macOS) ++ view.imageContentMode = resizeMode?.toContentMode() ?? .scaleProportionallyUpOrDown ++ #else + view.imageContentMode = resizeMode?.toContentMode() ?? .scaleAspectFit ++ #endif + } + + Prop("animationSpec") { (view, spec: AnimationSpec?) in +diff --git a/ios/SymbolRecords.swift b/ios/SymbolRecords.swift +index f276485..5f978a1 100644 +--- a/ios/SymbolRecords.swift ++++ b/ios/SymbolRecords.swift +@@ -7,6 +7,20 @@ enum SymbolScale: String, Enumerable { + case medium + case large + ++ #if os(macOS) ++ func imageSymbolScale() -> NSImage.SymbolScale? { ++ switch self { ++ case .small: ++ return .small ++ case .medium: ++ return .medium ++ case .large: ++ return .large ++ case .default, .unspecified: ++ return nil ++ } ++ } ++ #else + func imageSymbolScale() -> UIImage.SymbolScale { + switch self { + case .default: +@@ -21,6 +35,7 @@ enum SymbolScale: String, Enumerable { + return .unspecified + } + } ++ #endif + } + + enum SymbolWeight: String, Enumerable { +@@ -35,10 +50,20 @@ enum SymbolWeight: String, Enumerable { + case heavy + case black + +- func imageSymbolWeight() -> UIImage.SymbolWeight { ++ #if os(macOS) ++ typealias ImageSymbolWeight = NSFont.Weight ++ #else ++ typealias ImageSymbolWeight = UIImage.SymbolWeight ++ #endif ++ ++ func imageSymbolWeight() -> ImageSymbolWeight { + switch self { + case .unspecified: ++ #if os(macOS) ++ return .regular ++ #else + return .unspecified ++ #endif + case .ultraLight: + return .ultraLight + case .thin: +@@ -76,6 +101,19 @@ enum SymbolContentMode: String, Enumerable { + case bottomLeft + case bottomRight + ++ #if os(macOS) ++ // NSImageView can only stretch, fit, or draw at its natural size. ++ func toContentMode() -> NSImageScaling { ++ switch self { ++ case .scaleToFill: ++ return .scaleAxesIndependently ++ case .scaleAspectFit, .scaleAspectFill: ++ return .scaleProportionallyUpOrDown ++ default: ++ return .scaleNone ++ } ++ } ++ #else + func toContentMode() -> UIView.ContentMode { + switch self { + case .scaleToFill: +@@ -106,6 +144,7 @@ enum SymbolContentMode: String, Enumerable { + return .bottomRight + } + } ++ #endif + } + + enum SymbolType: String, Enumerable { +diff --git a/ios/SymbolView.swift b/ios/SymbolView.swift +index 5e59ad5..edfb848 100644 +--- a/ios/SymbolView.swift ++++ b/ios/SymbolView.swift +@@ -1,13 +1,25 @@ + import ExpoModulesCore + ++#if os(macOS) ++typealias SymbolImageView = NSImageView ++#else ++typealias SymbolImageView = UIImageView ++#endif ++ + class SymbolView: ExpoView { +- let imageView = UIImageView() ++ let imageView = SymbolImageView() + + // MARK: Properties + var name: String = "" ++ #if os(macOS) ++ var weight: NSFont.Weight = .regular ++ var scale: NSImage.SymbolScale? ++ var imageContentMode: NSImageScaling = .scaleAxesIndependently ++ #else + var weight: UIImage.SymbolWeight = .unspecified + var scale: UIImage.SymbolScale = .default + var imageContentMode: UIView.ContentMode = .scaleToFill ++ #endif + var symbolType: SymbolType = .monochrome + var tint: UIColor? + var animationSpec: AnimationSpec? +@@ -23,6 +35,44 @@ class SymbolView: ExpoView { + imageView.frame = bounds + } + ++ #if os(macOS) ++ func reloadSymbol() { ++ guard let image = NSImage(systemSymbolName: name, accessibilityDescription: nil) else { ++ return ++ } ++ imageView.imageScaling = imageContentMode ++ imageView.symbolConfiguration = getSymbolConfig() ++ // AppKit tints template images through the view rather than the image. ++ imageView.contentTintColor = symbolType == .hierarchical ? nil : tint ++ imageView.image = image ++ ++ // Effects need to be added last ++ imageView.removeAllSymbolEffects() ++ if animated { ++ addSymbolEffects() ++ } ++ } ++ ++ private func getSymbolConfig() -> NSImage.SymbolConfiguration { ++ var config = scale.map { NSImage.SymbolConfiguration(pointSize: NSFont.systemFontSize, weight: weight, scale: $0) } ++ ?? NSImage.SymbolConfiguration(pointSize: NSFont.systemFontSize, weight: weight) ++ ++ switch symbolType { ++ case .monochrome: ++ config = config.applying(.preferringMonochrome()) ++ case .hierarchical: ++ config = config.applying(NSImage.SymbolConfiguration(hierarchicalColor: tint ?? .systemBlue)) ++ case .palette: ++ if palette.count > 1 { ++ config = config.applying(NSImage.SymbolConfiguration(paletteColors: palette)) ++ } ++ case .multicolor: ++ config = config.applying(.preferringMulticolor()) ++ } ++ ++ return config ++ } ++ #else + func reloadSymbol() { + guard let image = UIImage(systemName: name) else { + return +@@ -45,6 +95,7 @@ class SymbolView: ExpoView { + } + } + } ++ #endif + + @available(iOS 17.0, tvOS 17.0, *) + private func addSymbolEffects() { +@@ -71,6 +122,7 @@ class SymbolView: ExpoView { + } + } + ++ #if !os(macOS) + private func getSymbolConfig() -> UIImage.SymbolConfiguration { + #if os(tvOS) + var config = UIImage.SymbolConfiguration(pointSize: 18.0, weight: weight, scale: scale) +@@ -95,4 +147,5 @@ class SymbolView: ExpoView { + + return config + } ++ #endif + } diff --git a/patches/expo-system-ui@55.0.22.patch b/patches/expo-system-ui@55.0.22.patch new file mode 100644 index 000000000..1dea01844 --- /dev/null +++ b/patches/expo-system-ui@55.0.22.patch @@ -0,0 +1,61 @@ +diff --git a/ios/ExpoSystemUI/ExpoSystemUIModule.swift b/ios/ExpoSystemUI/ExpoSystemUIModule.swift +index 79e7b61..703babf 100644 +--- a/ios/ExpoSystemUI/ExpoSystemUIModule.swift ++++ b/ios/ExpoSystemUI/ExpoSystemUIModule.swift +@@ -28,6 +28,36 @@ public class ExpoSystemUIModule: Module { + } + } + ++ #if os(macOS) ++ // The app delegate owns the main window, as on iOS. ++ static var mainWindow: NSWindow? { ++ guard let delegate = NSApp.delegate as? NSObject, delegate.responds(to: Selector(("window"))) else { ++ return nil ++ } ++ return delegate.value(forKey: "window") as? NSWindow ++ } ++ ++ static func getBackgroundColor() -> String? { ++ var color: String? ++ EXUtilities.performSynchronously { ++ if let backgroundColor = mainWindow?.backgroundColor.usingColorSpace(.sRGB)?.cgColor { ++ color = EXUtilities.hexString(with: backgroundColor) ++ } ++ } ++ return color ++ } ++ ++ static func setBackgroundColorAsync(color: Int?) { ++ EXUtilities.performSynchronously { ++ if let color { ++ UserDefaults.standard.set(color, forKey: colorKey) ++ } else { ++ UserDefaults.standard.removeObject(forKey: colorKey) ++ } ++ mainWindow?.backgroundColor = color.flatMap { EXUtilities.uiColor($0) } ?? .windowBackgroundColor ++ } ++ } ++ #else + static func getBackgroundColor() -> String? { + var color: String? + EXUtilities.performSynchronously { +@@ -70,4 +100,5 @@ public class ExpoSystemUIModule: Module { + } + } + } ++ #endif + } +diff --git a/ios/ExpoSystemUI.podspec b/ios/ExpoSystemUI.podspec +index 6cf5c56..0723ef6 100644 +--- a/ios/ExpoSystemUI.podspec ++++ b/ios/ExpoSystemUI.podspec +@@ -12,7 +12,8 @@ Pod::Spec.new do |s| + s.homepage = package['homepage'] + s.platforms = { + :ios => '15.1', +- :tvos => '15.1' ++ :tvos => '15.1', ++ :osx => '14.0' + } + s.swift_version = '5.9' + s.source = { git: 'https://github.com/expo/expo.git' } diff --git a/patches/expo-web-browser@55.0.20.patch b/patches/expo-web-browser@55.0.20.patch new file mode 100644 index 000000000..e0677e857 --- /dev/null +++ b/patches/expo-web-browser@55.0.20.patch @@ -0,0 +1,13 @@ +diff --git a/ios/WebBrowserSession.swift b/ios/WebBrowserSession.swift +index d3d2981..1ac7c6b 100644 +--- a/ios/WebBrowserSession.swift ++++ b/ios/WebBrowserSession.swift +@@ -126,6 +126,8 @@ internal class WebBrowserSession: NSObject, WKNavigationDelegate, WKUIDelegate, + } + + func dismiss(completion: ((String) -> Void)? = nil) { ++ // Closing reports "cancel" through windowWillClose, as for the user; a dismiss reports "dismiss", as on iOS. ++ window?.delegate = nil + window?.close() + let type = "dismiss" + finish(type: type) diff --git a/patches/licenses/expo-audio/LICENSE.provenance b/patches/licenses/expo-audio/LICENSE.provenance index b93c841e3..0bac3fd7b 100644 --- a/patches/licenses/expo-audio/LICENSE.provenance +++ b/patches/licenses/expo-audio/LICENSE.provenance @@ -1,4 +1,4 @@ -Upstream: expo-audio@1.1.1 -Source: https://github.com/expo/expo/blob/ce76892f5b356dc1bbfeabaf29dd8a50a19aa08a/LICENSE -Commit: ce76892f5b356dc1bbfeabaf29dd8a50a19aa08a +Upstream: expo-audio@55.0.18 +Source: https://github.com/expo/expo/blob/856b99321eeb04bd528b33f90c0e7fa2859a1fcb/LICENSE +Commit: 856b99321eeb04bd528b33f90c0e7fa2859a1fcb The accompanying LICENSE is copied verbatim from this upstream revision. diff --git a/patches/react-native-app-logs@0.3.1.patch b/patches/react-native-app-logs@0.3.1.patch new file mode 100644 index 000000000..c018abe27 --- /dev/null +++ b/patches/react-native-app-logs@0.3.1.patch @@ -0,0 +1,20 @@ +diff --git a/react-native-app-logs.podspec b/react-native-app-logs.podspec +index 9ce28e8..d815df0 100644 +--- a/react-native-app-logs.podspec ++++ b/react-native-app-logs.podspec +@@ -11,7 +11,7 @@ Pod::Spec.new do |s| + s.license = package["license"] + s.authors = package["author"] + +- s.platforms = { :ios => min_ios_version_supported } ++ s.platforms = { :ios => min_ios_version_supported, :osx => "14.0" } + s.source = { :git => "https://github.com/kirillzyusko/react-native-app-logs.git", :tag => "#{s.version}" } + + s.source_files = "ios/**/*.{h,m,mm,swift}" +diff --git a/src/bindings.macos.ts b/src/bindings.macos.ts +new file mode 100644 +index 0000000..2d2a485 +--- /dev/null ++++ b/src/bindings.macos.ts +@@ -0,0 +1 @@ ++export { default } from './bindings.ios'; diff --git a/patches/react-native-blurhash@2.1.3.patch b/patches/react-native-blurhash@2.1.3.patch new file mode 100644 index 000000000..5f122f1f8 --- /dev/null +++ b/patches/react-native-blurhash@2.1.3.patch @@ -0,0 +1,318 @@ +diff --git a/ios/BlurhashModule.mm b/ios/BlurhashModule.mm +index 189b790..a5b24bd 100644 +--- a/ios/BlurhashModule.mm ++++ b/ios/BlurhashModule.mm +@@ -1,7 +1,9 @@ + #import "BlurhashModule.h" + + #import <React/RCTUtils.h> ++#if !TARGET_OS_OSX + #import <UIKit/UIKit.h> ++#endif + #ifdef RCT_NEW_ARCH_ENABLED + #import <blurhash_codegen/blurhash_codegen.h> + #endif +diff --git a/ios/BlurhashModuleImpl.swift b/ios/BlurhashModuleImpl.swift +index dc5f62e..ca0f795 100644 +--- a/ios/BlurhashModuleImpl.swift ++++ b/ios/BlurhashModuleImpl.swift +@@ -7,7 +7,11 @@ + // + + import Foundation ++#if canImport(UIKit) + import UIKit ++#else ++import AppKit ++#endif + + @objc + public final class BlurhashModuleImpl: NSObject { +diff --git a/ios/BlurhashPlatform.swift b/ios/BlurhashPlatform.swift +new file mode 100644 +index 0000000..b9a3e49 +--- /dev/null ++++ b/ios/BlurhashPlatform.swift +@@ -0,0 +1,6 @@ ++#if os(macOS) ++import AppKit ++ ++// The decoder and encoder extend UIImage; on macOS that is NSImage. ++public typealias UIImage = NSImage ++#endif +diff --git a/ios/BlurhashView.swift b/ios/BlurhashView.swift +index 795e58e..45a3ace 100644 +--- a/ios/BlurhashView.swift ++++ b/ios/BlurhashView.swift +@@ -7,7 +7,13 @@ + // + + import Foundation ++#if os(macOS) ++import AppKit ++public typealias BlurhashPlatformView = NSView ++#else + import UIKit ++public typealias BlurhashPlatformView = UIView ++#endif + + @objc + public protocol BlurhashViewDelegate { +@@ -16,12 +22,17 @@ public protocol BlurhashViewDelegate { + func blurhashViewLoadDidError(_ message: String?) + } + +-public final class BlurhashView: UIView { ++public final class BlurhashView: BlurhashPlatformView { + @objc public var blurhash: String? + @objc public var decodeWidth: Int = 32 + @objc public var decodeHeight: Int = 32 + @objc public var decodePunch: Float = 1 + @objc public var decodeAsync: Bool = false ++#if os(macOS) ++ @objc public var contentsGravity: CALayerContentsGravity = .resizeAspectFill { ++ didSet { layer?.contentsGravity = contentsGravity } ++ } ++#else + @objc override public var contentMode: ContentMode { + get { + imageContainer.contentMode +@@ -30,10 +41,19 @@ public final class BlurhashView: UIView { + imageContainer.contentMode = newValue + } + } ++#endif + + @objc public weak var delegate: BlurhashViewDelegate? + + private var lastState: BlurhashCache? ++#if os(macOS) ++ override public init(frame: CGRect) { ++ super.init(frame: frame) ++ wantsLayer = true ++ layer?.masksToBounds = true ++ layer?.contentsGravity = contentsGravity ++ } ++#else + private let imageContainer: UIImageView + + override public init(frame: CGRect) { +@@ -44,6 +64,7 @@ public final class BlurhashView: UIView { + super.init(frame: frame) + addSubview(imageContainer) + } ++#endif + + @available(*, unavailable) + required init?(coder _: NSCoder) { +@@ -119,12 +140,15 @@ public final class BlurhashView: UIView { + } + + private final func setImageContainerImage(image: UIImage?) { ++#if os(macOS) ++ let show: () -> Void = { self.layer?.contents = image?.cgImage(forProposedRect: nil, context: nil, hints: nil) } ++#else ++ let show: () -> Void = { self.imageContainer.image = image } ++#endif + if Thread.isMainThread { +- imageContainer.image = image ++ show() + } else { +- DispatchQueue.main.async { +- self.imageContainer.image = image +- } ++ DispatchQueue.main.async(execute: show) + } + } + +diff --git a/ios/BlurhashViewComponentView.h b/ios/BlurhashViewComponentView.h +index 51c9a3a..93a78eb 100644 +--- a/ios/BlurhashViewComponentView.h ++++ b/ios/BlurhashViewComponentView.h +@@ -1,6 +1,8 @@ + #ifdef RCT_NEW_ARCH_ENABLED + ++#if !TARGET_OS_OSX + #import <UIKit/UIKit.h> ++#endif + + #import <React/RCTViewComponentView.h> + +diff --git a/ios/BlurhashViewComponentView.mm b/ios/BlurhashViewComponentView.mm +index e483bfb..ccc38ab 100644 +--- a/ios/BlurhashViewComponentView.mm ++++ b/ios/BlurhashViewComponentView.mm +@@ -18,6 +18,21 @@ + + using namespace facebook::react; + ++#if TARGET_OS_OSX ++static CALayerContentsGravity convertResizeMode(const BlurhashViewResizeMode &resizeMode) ++{ ++ switch (resizeMode) { ++ case BlurhashViewResizeMode::Contain: ++ return kCAGravityResizeAspect; ++ case BlurhashViewResizeMode::Cover: ++ return kCAGravityResizeAspectFill; ++ case BlurhashViewResizeMode::Stretch: ++ return kCAGravityResize; ++ case BlurhashViewResizeMode::Center: ++ return kCAGravityCenter; ++ } ++} ++#else + static UIViewContentMode convertResizeMode(const BlurhashViewResizeMode &resizeMode) + { + switch (resizeMode) { +@@ -31,6 +46,7 @@ static UIViewContentMode convertResizeMode(const BlurhashViewResizeMode &resizeM + return UIViewContentMode::UIViewContentModeCenter; + } + } ++#endif + + @interface BlurhashViewComponentView () <RCTBlurhashViewViewProtocol, BlurhashViewDelegate> + @end +@@ -81,7 +97,11 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & + _blurhashView.decodeAsync = newViewProps.decodeAsync; + } + if (oldViewProps.resizeMode != newViewProps.resizeMode) { ++#if TARGET_OS_OSX ++ _blurhashView.contentsGravity = convertResizeMode(newViewProps.resizeMode); ++#else + _blurhashView.contentMode = convertResizeMode(newViewProps.resizeMode); ++#endif + } + + [super updateProps:props oldProps:oldProps]; +diff --git a/ios/BlurhashViewManager.m b/ios/BlurhashViewManager.m +index 318c955..16292b7 100644 +--- a/ios/BlurhashViewManager.m ++++ b/ios/BlurhashViewManager.m +@@ -1,3 +1,5 @@ ++#include <TargetConditionals.h> ++#if !TARGET_OS_OSX + #import "BlurhashBridge.h" + #import <React/RCTViewManager.h> + +@@ -17,3 +19,4 @@ @interface RCT_EXTERN_REMAP_MODULE (BlurhashView, BlurhashViewManager, + RCT_EXPORT_VIEW_PROPERTY(onLoadError, RCTDirectEventBlock); + + @end ++#endif +diff --git a/ios/BlurhashViewManager.swift b/ios/BlurhashViewManager.swift +index c1abb62..6b5c6b1 100644 +--- a/ios/BlurhashViewManager.swift ++++ b/ios/BlurhashViewManager.swift +@@ -6,6 +6,7 @@ + // Copyright © 2020 Facebook. All rights reserved. + // + ++#if !os(macOS) + import Foundation + import UIKit + +@@ -151,3 +152,5 @@ final class BlurhashViewManager: RCTViewManager { + return true + } + } ++ ++#endif +diff --git a/ios/UIImage+BlurhashDecode.swift b/ios/UIImage+BlurhashDecode.swift +index 7076215..a79516c 100644 +--- a/ios/UIImage+BlurhashDecode.swift ++++ b/ios/UIImage+BlurhashDecode.swift +@@ -7,7 +7,11 @@ + // + + import Foundation ++#if canImport(UIKit) + import UIKit ++#else ++import AppKit ++#endif + + public extension UIImage { + convenience init?(blurHash: String, size: CGSize, punch: Float = 1) { +@@ -95,6 +99,10 @@ public extension UIImage { + shouldInterpolate: true, + intent: .defaultIntent) else { return nil } + ++#if os(macOS) ++ self.init(cgImage: cgImage, size: NSSize(width: width, height: height)) ++#else + self.init(cgImage: cgImage) ++#endif + } + } +diff --git a/ios/UIImage+BlurhashEncode.swift b/ios/UIImage+BlurhashEncode.swift +index bc68c95..c32c4af 100644 +--- a/ios/UIImage+BlurhashEncode.swift ++++ b/ios/UIImage+BlurhashEncode.swift +@@ -7,12 +7,22 @@ + // + + import Foundation ++#if canImport(UIKit) + import UIKit ++#else ++import AppKit ++#endif + + extension UIImage { + public func encodeBlurhash(numberOfComponents components: (Int, Int)) -> String? { ++#if os(macOS) ++ guard let source = cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil } ++ let pixelWidth = source.width ++ let pixelHeight = source.height ++#else + let pixelWidth = Int(round(size.width * scale)) + let pixelHeight = Int(round(size.height * scale)) ++#endif + + let context = CGContext( + data: nil, +@@ -23,12 +33,16 @@ extension UIImage { + space: CGColorSpace(name: CGColorSpace.sRGB)!, + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + )! ++#if os(macOS) ++ context.draw(source, in: CGRect(x: 0, y: 0, width: pixelWidth, height: pixelHeight)) ++#else + context.scaleBy(x: scale, y: -scale) + context.translateBy(x: 0, y: -size.height) + + UIGraphicsPushContext(context) + draw(at: .zero) + UIGraphicsPopContext() ++#endif + + guard let cgImage = context.makeImage(), + let dataProvider = cgImage.dataProvider, +diff --git a/package.json b/package.json +index dcc1bf5..6edaa33 100644 +--- a/package.json ++++ b/package.json +@@ -108,6 +108,11 @@ + "name": "blurhash_codegen", + "type": "all", + "jsSrcsDir": "./src/specs", ++ "ios": { ++ "componentProvider": { ++ "BlurhashView": "BlurhashViewComponentView" ++ } ++ }, + "android": { + "javaPackageName": "com.mrousavy.blurhash" + } +diff --git a/react-native-blurhash.podspec b/react-native-blurhash.podspec +index bb6ea68..ae7198c 100644 +--- a/react-native-blurhash.podspec ++++ b/react-native-blurhash.podspec +@@ -17,6 +17,7 @@ Pod::Spec.new do |s| + s.authors = { "Marc Rousavy" => "marcrousavy@hotmail.com" } + s.ios.deployment_target = '9.0' + s.tvos.deployment_target = '9.0' ++ s.osx.deployment_target = '14.0' + s.source = { :git => "https://github.com/mrousavy/react-native-blurhash.git", :tag => "#{s.version}" } + + s.source_files = "ios/**/*.{h,c,m,mm,swift}" diff --git a/patches/react-native-community-blur@4.4.1.patch b/patches/react-native-community-blur@4.4.1.patch new file mode 100644 index 000000000..c9494b6d2 --- /dev/null +++ b/patches/react-native-community-blur@4.4.1.patch @@ -0,0 +1,415 @@ +diff --git a/ios/BlurEffectWithAmount.h b/ios/BlurEffectWithAmount.h +index bf3992a..b15d6f0 100644 +--- a/ios/BlurEffectWithAmount.h ++++ b/ios/BlurEffectWithAmount.h +@@ -1,3 +1,5 @@ ++#include <TargetConditionals.h> ++#if !TARGET_OS_OSX + #import <UIKit/UIKit.h> + + @interface BlurEffectWithAmount : UIBlurEffect +@@ -5,3 +7,4 @@ + + + (instancetype)effectWithStyle:(UIBlurEffectStyle)style andBlurAmount:(NSNumber*)blurAmount; + @end ++#endif +diff --git a/ios/BlurEffectWithAmount.m b/ios/BlurEffectWithAmount.m +index e279400..70d41b6 100644 +--- a/ios/BlurEffectWithAmount.m ++++ b/ios/BlurEffectWithAmount.m +@@ -1,3 +1,5 @@ ++#include <TargetConditionals.h> ++#if !TARGET_OS_OSX + // + // We subclass UIBlurEffect so that we can set the BlurAmount. + // See: http://stackoverflow.com/a/30744777/304706 +@@ -63,3 +65,4 @@ - (id)copyWithZone:(NSZone*)zone + } + + @end ++#endif +diff --git a/ios/BlurView.h b/ios/BlurView.h +index e960937..5a7415b 100644 +--- a/ios/BlurView.h ++++ b/ios/BlurView.h +@@ -1,3 +1,5 @@ ++#include <TargetConditionals.h> ++#if !TARGET_OS_OSX + #import <UIKit/UIKit.h> + #import "BlurEffectWithAmount.h" + +@@ -24,3 +26,4 @@ + - (void)updateFallbackView; + - (BOOL)useReduceTransparencyFallback; + @end ++#endif +diff --git a/ios/BlurView.mm b/ios/BlurView.mm +index 3d35d8f..71410a5 100644 +--- a/ios/BlurView.mm ++++ b/ios/BlurView.mm +@@ -1,3 +1,5 @@ ++#include <TargetConditionals.h> ++#if !TARGET_OS_OSX + #import "BlurView.h" + #import "BlurEffectWithAmount.h" + +@@ -221,3 +223,4 @@ - (void)reduceTransparencyStatusDidChange:(__unused NSNotification *)notificatio + return BlurView.class; + } + #endif // RCT_NEW_ARCH_ENABLED ++#endif +diff --git a/ios/BlurViewManager.h b/ios/BlurViewManager.h +index 7aad0df..e33ca8d 100644 +--- a/ios/BlurViewManager.h ++++ b/ios/BlurViewManager.h +@@ -1,5 +1,8 @@ ++#include <TargetConditionals.h> ++#if !TARGET_OS_OSX + #import <React/RCTViewManager.h> + + @interface BlurViewManager : RCTViewManager + + @end ++#endif +diff --git a/ios/BlurViewManager.mm b/ios/BlurViewManager.mm +index de58e0c..0bc1e39 100644 +--- a/ios/BlurViewManager.mm ++++ b/ios/BlurViewManager.mm +@@ -1,3 +1,5 @@ ++#include <TargetConditionals.h> ++#if !TARGET_OS_OSX + #import "BlurViewManager.h" + #import "BlurView.h" + +@@ -15,3 +17,4 @@ - (UIView *)view + RCT_EXPORT_VIEW_PROPERTY(reducedTransparencyFallbackColor, UIColor); + + @end ++#endif +diff --git a/ios/VibrancyView.h b/ios/VibrancyView.h +index a5c0608..139c55c 100644 +--- a/ios/VibrancyView.h ++++ b/ios/VibrancyView.h +@@ -1,4 +1,7 @@ ++#include <TargetConditionals.h> ++#if !TARGET_OS_OSX + #import "BlurView.h" + + @interface VibrancyView : BlurView + @end ++#endif +diff --git a/ios/VibrancyView.mm b/ios/VibrancyView.mm +index 63660cd..0137596 100644 +--- a/ios/VibrancyView.mm ++++ b/ios/VibrancyView.mm +@@ -1,3 +1,5 @@ ++#include <TargetConditionals.h> ++#if !TARGET_OS_OSX + #import <React/RCTComponent.h> + #import "BlurView.h" + #import "VibrancyView.h" +@@ -118,3 +120,4 @@ - (void)updateFallbackView + return VibrancyView.class; + } + #endif // RCT_NEW_ARCH_ENABLED ++#endif +diff --git a/ios/VibrancyViewManager.h b/ios/VibrancyViewManager.h +index 2593794..0db60b2 100644 +--- a/ios/VibrancyViewManager.h ++++ b/ios/VibrancyViewManager.h +@@ -1,5 +1,8 @@ ++#include <TargetConditionals.h> ++#if !TARGET_OS_OSX + #import <React/RCTViewManager.h> + + @interface VibrancyViewManager : RCTViewManager + +-@end +\ No newline at end of file ++@end ++#endif +diff --git a/ios/VibrancyViewManager.mm b/ios/VibrancyViewManager.mm +index 17bb10e..a0ca18f 100644 +--- a/ios/VibrancyViewManager.mm ++++ b/ios/VibrancyViewManager.mm +@@ -1,3 +1,5 @@ ++#include <TargetConditionals.h> ++#if !TARGET_OS_OSX + #import "VibrancyViewManager.h" + #import "VibrancyView.h" + +@@ -15,3 +17,4 @@ - (UIView *)view + RCT_EXPORT_VIEW_PROPERTY(reducedTransparencyFallbackColor, UIColor); + + @end ++#endif +diff --git a/ios/macos/RNBlurMacOS.mm b/ios/macos/RNBlurMacOS.mm +new file mode 100644 +index 0000000..e365754 +--- /dev/null ++++ b/ios/macos/RNBlurMacOS.mm +@@ -0,0 +1,218 @@ ++#include <TargetConditionals.h> ++#if TARGET_OS_OSX ++ ++// AppKit BlurView and VibrancyView: NSVisualEffectView takes a material instead of a ++// UIBlurEffectStyle and has no blur radius, so blurAmount has no effect on macOS. ++#import <AppKit/AppKit.h> ++#import <React/RCTAssert.h> ++#import <React/RCTConversions.h> ++#import <React/RCTFabricComponentsPlugins.h> ++#import <React/RCTViewComponentView.h> ++#import <react/renderer/components/rnblurview/ComponentDescriptors.h> ++#import <react/renderer/components/rnblurview/Props.h> ++#import <react/renderer/components/rnblurview/RCTComponentViewHelpers.h> ++ ++using namespace facebook::react; ++ ++static NSVisualEffectMaterial RNBlurMaterial(NSString *type) ++{ ++ if ([type hasPrefix:@"ultraThinMaterial"]) return NSVisualEffectMaterialHUDWindow; ++ if ([type hasPrefix:@"thinMaterial"]) return NSVisualEffectMaterialPopover; ++ if ([type hasPrefix:@"thickMaterial"]) return NSVisualEffectMaterialWindowBackground; ++ if ([type hasPrefix:@"chromeMaterial"]) return NSVisualEffectMaterialTitlebar; ++ if ([type hasPrefix:@"material"] || [type isEqual:@"regular"] || [type isEqual:@"prominent"]) return NSVisualEffectMaterialContentBackground; ++ return NSVisualEffectMaterialFullScreenUI; ++} ++ ++static NSAppearance *RNBlurAppearance(NSString *type) ++{ ++ if ([type isEqual:@"dark"] || [type isEqual:@"extraDark"] || [type hasSuffix:@"Dark"]) return [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; ++ if ([type isEqual:@"light"] || [type isEqual:@"xlight"] || [type hasSuffix:@"Light"]) return [NSAppearance appearanceNamed:NSAppearanceNameAqua]; ++ return nil; // Adaptive materials follow the window. ++} ++ ++// Flipped like React's views, so children mounted inside get React's coordinates. ++@interface RNBlurEffectView : NSVisualEffectView ++@end ++ ++@implementation RNBlurEffectView ++- (BOOL)isFlipped ++{ ++ return YES; ++} ++@end ++ ++// Resolves its color in updateLayer, under the view's appearance, so dynamic colors follow Dark Mode. ++@interface RNBlurFallbackView : NSView ++@property (nonatomic, strong) NSColor *color; ++@end ++ ++@implementation RNBlurFallbackView ++- (BOOL)isFlipped ++{ ++ return YES; ++} ++ ++- (BOOL)wantsUpdateLayer ++{ ++ return YES; ++} ++ ++- (void)setColor:(NSColor *)color ++{ ++ _color = color; ++ self.needsDisplay = YES; ++} ++ ++- (void)updateLayer ++{ ++ self.layer.backgroundColor = _color.CGColor; ++} ++@end ++ ++@interface BlurView : RCTViewComponentView ++@property (nonatomic, readonly) NSVisualEffectView *effectView; ++@property (nonatomic, readonly) RNBlurFallbackView *fallbackView; ++@property (nonatomic, copy) NSString *blurType; ++@property (nonatomic, strong) NSColor *fallbackColor; ++@end ++ ++@interface BlurView () <RCTBlurViewViewProtocol> ++@end ++ ++@implementation BlurView ++ +++ (ComponentDescriptorProvider)componentDescriptorProvider ++{ ++ return concreteComponentDescriptorProvider<BlurViewComponentDescriptor>(); ++} ++ ++- (instancetype)initWithFrame:(CGRect)frame ++{ ++ if (self = [super initWithFrame:frame]) { ++ static const auto defaultProps = std::make_shared<const BlurViewProps>(); ++ _props = defaultProps; ++ _effectView = [[RNBlurEffectView alloc] initWithFrame:self.bounds]; ++ _effectView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; ++ _effectView.blendingMode = NSVisualEffectBlendingModeWithinWindow; ++ _effectView.state = NSVisualEffectStateFollowsWindowActiveState; ++ _fallbackView = [[RNBlurFallbackView alloc] initWithFrame:self.bounds]; ++ _fallbackView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; ++ _fallbackView.wantsLayer = YES; ++ self.blurType = @"dark"; ++ self.clipsToBounds = YES; ++ [self updateFallbackView]; ++ [NSWorkspace.sharedWorkspace.notificationCenter addObserver:self selector:@selector(updateFallbackView) name:NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification object:nil]; ++ } ++ return self; ++} ++ ++- (void)dealloc ++{ ++ [NSWorkspace.sharedWorkspace.notificationCenter removeObserver:self]; ++} ++ ++- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps ++{ ++ const auto &newViewProps = *std::static_pointer_cast<const BlurViewProps>(props); ++ self.blurType = [NSString stringWithUTF8String:toString(newViewProps.blurType).c_str()]; ++ self.fallbackColor = RCTUIColorFromSharedColor(newViewProps.reducedTransparencyFallbackColor); ++ [self updateFallbackView]; ++ [super updateProps:props oldProps:oldProps]; ++} ++ ++- (void)setBlurType:(NSString *)blurType ++{ ++ _blurType = [blurType copy]; ++ _effectView.material = RNBlurMaterial(blurType); ++ _effectView.appearance = RNBlurAppearance(blurType); ++} ++ ++- (void)updateFallbackView ++{ ++ BOOL fallback = NSWorkspace.sharedWorkspace.accessibilityDisplayShouldReduceTransparency && self.fallbackColor; ++ NSView *shown = fallback ? _fallbackView : _effectView, *hidden = fallback ? _effectView : _fallbackView; ++ _fallbackView.color = self.fallbackColor; ++ if (shown.superview) { ++ return; ++ } ++ // VibrancyView mounts its children inside the material, so they move with it. ++ for (NSView *child in [hidden.subviews copy]) { ++ [shown addSubview:child]; ++ } ++ // React may have moved the material into its clipping container; swap it in place. ++ NSView *parent = hidden.superview ?: self; ++ [hidden removeFromSuperview]; ++ shown.frame = parent.bounds; ++ [parent addSubview:shown positioned:NSWindowBelow relativeTo:nil]; ++} ++ ++@end ++ ++@interface VibrancyView : BlurView ++@end ++ ++@interface VibrancyView () <RCTVibrancyViewViewProtocol> ++@end ++ ++@implementation VibrancyView ++ +++ (ComponentDescriptorProvider)componentDescriptorProvider ++{ ++ return concreteComponentDescriptorProvider<VibrancyViewComponentDescriptor>(); ++} ++ ++- (instancetype)initWithFrame:(CGRect)frame ++{ ++ if (self = [super initWithFrame:frame]) { ++ static const auto defaultProps = std::make_shared<const VibrancyViewProps>(); ++ _props = defaultProps; ++ } ++ return self; ++} ++ ++// Like iOS, children mount inside the material (or the reduced-transparency fallback). ++- (NSView *)contentView ++{ ++ return self.effectView.superview ? self.effectView : self.fallbackView; ++} ++ ++- (void)mountChildComponentView:(RCTUIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index ++{ ++ NSView *contentView = self.contentView; ++ if (index < (NSInteger)contentView.subviews.count) { ++ [contentView addSubview:childComponentView positioned:NSWindowBelow relativeTo:contentView.subviews[index]]; ++ } else { ++ [contentView addSubview:childComponentView]; ++ } ++} ++ ++- (void)unmountChildComponentView:(RCTUIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index ++{ ++ RCTAssert(childComponentView.superview == self.contentView, @"Attempt to unmount a view which is mounted inside a different view."); ++ RCTAssert(self.contentView.subviews[index] == childComponentView, @"Attempt to unmount a view which has a different index."); ++ [childComponentView removeFromSuperview]; ++} ++ ++- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps ++{ ++ const auto &newViewProps = *std::static_pointer_cast<const VibrancyViewProps>(props); ++ self.blurType = [NSString stringWithUTF8String:toString(newViewProps.blurType).c_str()]; ++ self.fallbackColor = RCTUIColorFromSharedColor(newViewProps.reducedTransparencyFallbackColor); ++ [self updateFallbackView]; ++ [super updateProps:props oldProps:oldProps]; ++} ++ ++@end ++ ++Class<RCTComponentViewProtocol> BlurViewCls(void) ++{ ++ return BlurView.class; ++} ++ ++Class<RCTComponentViewProtocol> VibrancyViewCls(void) ++{ ++ return VibrancyView.class; ++} ++ ++#endif +diff --git a/package.json b/package.json +index 4e5c8a6..c8932f9 100644 +--- a/package.json ++++ b/package.json +@@ -150,6 +150,12 @@ + "name": "rnblurview", + "type": "components", + "jsSrcsDir": "./src/fabric", ++ "ios": { ++ "componentProvider": { ++ "BlurView": "BlurView", ++ "VibrancyView": "VibrancyView" ++ } ++ }, + "android": { + "javaPackageName": "com.reactnativecommunityblur" + } +diff --git a/react-native-blur.podspec b/react-native-blur.podspec +index 2913ba7..2d032c5 100644 +--- a/react-native-blur.podspec ++++ b/react-native-blur.podspec +@@ -11,7 +11,7 @@ Pod::Spec.new do |s| + s.license = package["license"] + s.authors = package["author"] + +- s.platforms = { :ios => "10.0", :tvos => "11.0", :visionos => "1.0" } ++ s.platforms = { :ios => "10.0", :tvos => "11.0", :visionos => "1.0", :osx => "14.0" } + s.source = { :git => "https://github.com/react-native-community/react-native-blur.git" } + + s.source_files = "ios/**/*.{h,m,mm}" +diff --git a/src/components/BlurView.macos.tsx b/src/components/BlurView.macos.tsx +new file mode 100644 +index 0000000..a5a3f06 +--- /dev/null ++++ b/src/components/BlurView.macos.tsx +@@ -0,0 +1,2 @@ ++export * from './BlurView.ios'; ++export { default } from './BlurView.ios'; +diff --git a/src/components/VibrancyView.macos.tsx b/src/components/VibrancyView.macos.tsx +new file mode 100644 +index 0000000..99d118b +--- /dev/null ++++ b/src/components/VibrancyView.macos.tsx +@@ -0,0 +1,2 @@ ++export * from './VibrancyView.ios'; ++export { default } from './VibrancyView.ios'; diff --git a/patches/react-native-community-datetimepicker@9.2.2.patch b/patches/react-native-community-datetimepicker@9.2.2.patch new file mode 100644 index 000000000..87092e6a8 --- /dev/null +++ b/patches/react-native-community-datetimepicker@9.2.2.patch @@ -0,0 +1,320 @@ +diff --git a/RNDateTimePicker.podspec b/RNDateTimePicker.podspec +index 5f53282..ebb61c9 100644 +--- a/RNDateTimePicker.podspec ++++ b/RNDateTimePicker.podspec +@@ -10,7 +10,7 @@ Pod::Spec.new do |s| + s.license = package['license'] + s.author = package['author'] + s.homepage = package['homepage'] +- s.platforms = { :ios => "11.0", :visionos => "1.0" } ++ s.platforms = { :ios => "11.0", :visionos => "1.0", :osx => "14.0" } + s.source = { :git => "https://github.com/react-native-community/datetimepicker", :tag => "v#{s.version}" } + s.source_files = "ios/**/*.{h,m,mm,cpp}" + s.requires_arc = true +diff --git a/ios/RNDateTimePicker.h b/ios/RNDateTimePicker.h +index 00dd45d..1308e42 100644 +--- a/ios/RNDateTimePicker.h ++++ b/ios/RNDateTimePicker.h +@@ -1,3 +1,5 @@ ++#include <TargetConditionals.h> ++#if !TARGET_OS_OSX + /** + * Copyright (c) Facebook, Inc. and its affiliates. + * +@@ -10,3 +12,4 @@ + @interface RNDateTimePicker : UIDatePicker + + @end ++#endif +diff --git a/ios/RNDateTimePicker.m b/ios/RNDateTimePicker.m +index 9c73c8e..7a913f2 100644 +--- a/ios/RNDateTimePicker.m ++++ b/ios/RNDateTimePicker.m +@@ -1,3 +1,5 @@ ++#include <TargetConditionals.h> ++#if !TARGET_OS_OSX + /** + * Copyright (c) Facebook, Inc. and its affiliates. + * +@@ -73,3 +75,4 @@ - (void)setDate:(NSDate *)date { + } + + @end ++#endif +diff --git a/ios/RNDateTimePickerManager.h b/ios/RNDateTimePickerManager.h +index a619db6..2925f0e 100644 +--- a/ios/RNDateTimePickerManager.h ++++ b/ios/RNDateTimePickerManager.h +@@ -1,3 +1,5 @@ ++#include <TargetConditionals.h> ++#if !TARGET_OS_OSX + /** + * Copyright (c) Facebook, Inc. and its affiliates. + * +@@ -17,3 +19,4 @@ + @interface RNDateTimePickerManager : RCTViewManager + + @end ++#endif +diff --git a/ios/RNDateTimePickerManager.m b/ios/RNDateTimePickerManager.m +index 0a69f5c..85943c5 100644 +--- a/ios/RNDateTimePickerManager.m ++++ b/ios/RNDateTimePickerManager.m +@@ -1,3 +1,5 @@ ++#include <TargetConditionals.h> ++#if !TARGET_OS_OSX + /** + * Copyright (c) Facebook, Inc. and its affiliates. + * +@@ -196,3 +198,4 @@ + (NSString*) datepickerStyleToString: (UIDatePickerStyle) style API_AVAILABLE( + } + + @end ++#endif +diff --git a/ios/RNDateTimePickerShadowView.h b/ios/RNDateTimePickerShadowView.h +index 0e25849..a8054d1 100644 +--- a/ios/RNDateTimePickerShadowView.h ++++ b/ios/RNDateTimePickerShadowView.h +@@ -1,3 +1,5 @@ ++#include <TargetConditionals.h> ++#if !TARGET_OS_OSX + #import <React/RCTShadowView.h> + #import <React/RCTLog.h> + #import "RNDateTimePicker.h" +@@ -13,3 +15,4 @@ + @property (nonatomic, assign) UIDatePickerStyle displayIOS API_AVAILABLE(ios(13.4)); + + @end ++#endif +diff --git a/ios/RNDateTimePickerShadowView.m b/ios/RNDateTimePickerShadowView.m +index 4ff3362..352c0af 100644 +--- a/ios/RNDateTimePickerShadowView.m ++++ b/ios/RNDateTimePickerShadowView.m +@@ -1,3 +1,5 @@ ++#include <TargetConditionals.h> ++#if !TARGET_OS_OSX + #import "RNDateTimePickerShadowView.h" + + @implementation RNDateTimePickerShadowView +@@ -79,3 +81,4 @@ static YGSize RNDateTimePickerShadowViewMeasure(YGNodeConstRef node, float width + } + + @end ++#endif +diff --git a/ios/fabric/RNDateTimePickerComponentView.mm b/ios/fabric/RNDateTimePickerComponentView.mm +index ec9d7cd..2ca07f1 100644 +--- a/ios/fabric/RNDateTimePickerComponentView.mm ++++ b/ios/fabric/RNDateTimePickerComponentView.mm +@@ -1,3 +1,5 @@ ++#include <TargetConditionals.h> ++#if !TARGET_OS_OSX + /** + * RNDateTimePickerComponentView is only be available when fabric is enabled. + */ +@@ -326,4 +328,4 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & + { + return RNDateTimePickerComponentView.class; + } +- ++#endif +diff --git a/ios/fabric/RNDateTimePickerComponentViewMacOS.mm b/ios/fabric/RNDateTimePickerComponentViewMacOS.mm +new file mode 100644 +index 0000000..7f8f91e +--- /dev/null ++++ b/ios/fabric/RNDateTimePickerComponentViewMacOS.mm +@@ -0,0 +1,179 @@ ++/** ++ * macOS: the Fabric picker on AppKit's NSDatePicker. NSDatePicker has no countdown mode, wheels, ++ * minute interval or accent color; countdown shows a time picker and spinner a stepper field. ++ */ ++#include <TargetConditionals.h> ++#if TARGET_OS_OSX ++ ++#import "RNDateTimePickerComponentView.h" ++#import <React/RCTConversions.h> ++#import <React/RCTFabricComponentsPlugins.h> ++ ++#import "cpp/react/renderer/components/RNDateTimePicker/ComponentDescriptors.h" ++#import <react/renderer/components/RNDateTimePickerCGen/EventEmitters.h> ++#import <react/renderer/components/RNDateTimePickerCGen/Props.h> ++#import <react/renderer/components/RNDateTimePickerCGen/RCTComponentViewHelpers.h> ++ ++using namespace facebook::react; ++ ++@interface RNDateTimePickerComponentView () <RCTRNDateTimePickerViewProtocol> ++@end ++ ++@implementation RNDateTimePickerComponentView { ++ NSDatePicker *_picker; ++ RNDateTimePickerShadowNode::ConcreteState::Shared _state; ++ CGSize _measuredSize; ++} ++ ++- (instancetype)initWithFrame:(CGRect)frame ++{ ++ if (self = [super initWithFrame:frame]) { ++ static const auto defaultProps = std::make_shared<const RNDateTimePickerProps>(); ++ _props = defaultProps; ++ ++ _picker = [NSDatePicker new]; ++ _picker.datePickerStyle = NSDatePickerStyleTextFieldAndStepper; ++ _picker.datePickerElements = NSDatePickerElementFlagYearMonthDay; ++ _picker.target = self; ++ _picker.action = @selector(onChange:); ++ ++ self.contentView = _picker; ++ } ++ return self; ++} ++ ++- (void)onChange:(NSDatePicker *)sender ++{ ++ if (!_eventEmitter) { ++ return; ++ } ++ RNDateTimePickerEventEmitter::OnChange event = { ++ // Sending time in milliseconds ++ .timestamp = [sender.dateValue timeIntervalSince1970] * 1000 ++ }; ++ std::dynamic_pointer_cast<const RNDateTimePickerEventEmitter>(_eventEmitter)->onChange(event); ++} ++ ++// Sizes the shadow node to the picker (see adopt in ComponentDescriptors.h). ++- (void)updateMeasurements ++{ ++ CGSize size = _picker.fittingSize; ++ if (_state == nullptr || CGSizeEqualToSize(size, _measuredSize)) { ++ return; ++ } ++ _measuredSize = size; ++ _state->updateState(RNDateTimePickerState{RCTSizeFromCGSize(size)}); ++} ++ +++ (ComponentDescriptorProvider)componentDescriptorProvider ++{ ++ return concreteComponentDescriptorProvider<RNDateTimePickerComponentDescriptor>(); ++} ++ +++ (BOOL)shouldBeRecycled ++{ ++ return NO; ++} ++ ++- (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState ++{ ++ _state = std::static_pointer_cast<const RNDateTimePickerShadowNode::ConcreteState>(state); ++ if (oldState == nullptr) { ++ _measuredSize = CGSizeZero; ++ [self updateMeasurements]; ++ } ++} ++ ++- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps ++{ ++ const auto &oldPickerProps = *std::static_pointer_cast<const RNDateTimePickerProps>(_props); ++ const auto &newPickerProps = *std::static_pointer_cast<const RNDateTimePickerProps>(props); ++ ++ BOOL limitsChanged = oldPickerProps.minimumDate != newPickerProps.minimumDate || oldPickerProps.maximumDate != newPickerProps.maximumDate; ++ if (limitsChanged) { ++ NSDate *minDate = newPickerProps.minimumDate ? [NSDate dateWithTimeIntervalSince1970:newPickerProps.minimumDate / 1000.0] : nil; ++ NSDate *maxDate = newPickerProps.maximumDate ? [NSDate dateWithTimeIntervalSince1970:newPickerProps.maximumDate / 1000.0] : nil; ++ // As on iOS, min > max leaves both unset and relies on the JavaScript warning. ++ BOOL valid = !minDate || !maxDate || [minDate compare:maxDate] != NSOrderedDescending; ++ _picker.minDate = nil; ++ _picker.maxDate = nil; ++ _picker.minDate = valid ? minDate : nil; ++ _picker.maxDate = valid ? maxDate : nil; ++ } ++ ++ // NSDatePicker clamps its date whenever the limits move, so set the date after them. ++ if (limitsChanged || oldPickerProps.date != newPickerProps.date) { ++ _picker.dateValue = [NSDate dateWithTimeIntervalSince1970:newPickerProps.date / 1000.0]; ++ } ++ ++ if (oldPickerProps.locale != newPickerProps.locale) { ++ _picker.locale = [[NSLocale alloc] initWithLocaleIdentifier:RCTNSStringFromString(newPickerProps.locale)]; ++ } ++ ++ if (oldPickerProps.mode != newPickerProps.mode) { ++ switch (newPickerProps.mode) { ++ case RNDateTimePickerMode::Time: ++ case RNDateTimePickerMode::Countdown: ++ _picker.datePickerElements = NSDatePickerElementFlagHourMinute; ++ break; ++ case RNDateTimePickerMode::Datetime: ++ _picker.datePickerElements = NSDatePickerElementFlagYearMonthDay | NSDatePickerElementFlagHourMinute; ++ break; ++ default: ++ _picker.datePickerElements = NSDatePickerElementFlagYearMonthDay; ++ } ++ } ++ ++ if (oldPickerProps.displayIOS != newPickerProps.displayIOS) { ++ _picker.datePickerStyle = newPickerProps.displayIOS == RNDateTimePickerDisplayIOS::Inline ++ ? NSDatePickerStyleClockAndCalendar ++ : NSDatePickerStyleTextFieldAndStepper; ++ } ++ ++ if (oldPickerProps.timeZoneOffsetInMinutes != newPickerProps.timeZoneOffsetInMinutes) { ++ // JS standard for time zones is minutes. ++ _picker.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:newPickerProps.timeZoneOffsetInMinutes * 60.0]; ++ } ++ ++ if (oldPickerProps.timeZoneName != newPickerProps.timeZoneName) { ++ NSString *timeZoneName = RCTNSStringFromString(newPickerProps.timeZoneName); ++ NSTimeZone *timeZone = timeZoneName.length ? [NSTimeZone timeZoneWithName:timeZoneName] : nil; ++ if (timeZoneName.length && !timeZone) { ++ RCTLogWarn(@"'%@' does not exist in NSTimeZone.knownTimeZoneNames. Falling back to localTimeZone=%@", timeZoneName, NSTimeZone.localTimeZone.name); ++ } ++ _picker.timeZone = timeZone ?: NSTimeZone.localTimeZone; ++ } ++ ++ if (oldPickerProps.textColor != newPickerProps.textColor) { ++ _picker.textColor = RCTUIColorFromSharedColor(newPickerProps.textColor) ?: NSColor.controlTextColor; ++ } ++ ++ if (oldPickerProps.themeVariant != newPickerProps.themeVariant) { ++ switch (newPickerProps.themeVariant) { ++ case RNDateTimePickerThemeVariant::Light: ++ _picker.appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; ++ break; ++ case RNDateTimePickerThemeVariant::Dark: ++ _picker.appearance = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; ++ break; ++ default: ++ _picker.appearance = nil; ++ } ++ } ++ ++ if (oldPickerProps.enabled != newPickerProps.enabled) { ++ _picker.enabled = newPickerProps.enabled; ++ } ++ ++ [super updateProps:props oldProps:oldProps]; ++ [self updateMeasurements]; ++} ++ ++@end ++ ++Class<RCTComponentViewProtocol> RNDateTimePickerCls(void) ++{ ++ return RNDateTimePickerComponentView.class; ++} ++ ++#endif +diff --git a/src/datetimepicker.macos.js b/src/datetimepicker.macos.js +new file mode 100644 +index 0000000..4290def +--- /dev/null ++++ b/src/datetimepicker.macos.js +@@ -0,0 +1,2 @@ ++// macOS uses the native picker, like iOS. ++export { default } from './datetimepicker.ios'; +diff --git a/src/picker.macos.js b/src/picker.macos.js +new file mode 100644 +index 0000000..157ac5c +--- /dev/null ++++ b/src/picker.macos.js +@@ -0,0 +1,2 @@ ++// macOS uses the native picker, like iOS. ++export { default } from './picker.ios'; diff --git a/patches/react-native-fast-image@10.9.9.patch b/patches/react-native-fast-image@10.9.9.patch new file mode 100644 index 000000000..f685b7760 --- /dev/null +++ b/patches/react-native-fast-image@10.9.9.patch @@ -0,0 +1,131 @@ +diff --git a/src/index.macos.tsx b/src/index.macos.tsx +new file mode 100644 +index 0000000..0d10daf +--- /dev/null ++++ b/src/index.macos.tsx +@@ -0,0 +1,125 @@ ++// FastImage on macOS: React Native's Image stands in for the native view, which is ++// UIKit only. Sources (uri, headers, require()), objectFit, tintColor, placeholder and ++// defaultSource while loading, and the load events work; Image's own caches replace ++// SDWebImage's, so cache options, thumbhash/blurhash placeholders, transitions and ++// several sizes of one image are not supported (the largest size loads). ++import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react' ++import { Image, StyleSheet, View, type ImageResizeMode } from 'react-native' ++import { cacheControl, priority, resizeMode } from './constants' ++import { splitBackgroundProps } from './backgroundProps' ++import type { ++ ClearCacheResult, ++ FastImageBackgroundProps, ++ FastImageProps, ++ FastImageRef, ++ FastImageStaticProperties, ++ ObjectFit, ++ PreloadResult, ++} from './index' ++ ++export type * from './index' ++ ++const fits: Record<ObjectFit, ImageResizeMode> = { ++ fill: 'stretch', ++ contain: 'contain', ++ cover: 'cover', ++ none: 'center', ++ 'scale-down': 'center', ++} ++const unsupported = (name: string) => ++ Promise.resolve({ ok: false as const, error: `FastImage.${name} is not supported on macOS` }) ++ ++type OneSource = Exclude<NonNullable<FastImageProps['source']>, readonly unknown[]> ++ ++function imageSource(source: FastImageProps['source']) { ++ const sources = (Array.isArray(source) ? source : [source]) as (OneSource | undefined)[] ++ const area = (one: OneSource | undefined) => typeof one === 'object' ? (one.width ?? 0) * (one.height ?? 0) : 0 ++ const selected = sources.reduce((best, next) => area(next) < area(best) ? best : next, sources[0]) ++ const largest = typeof selected === 'number' ? Image.resolveAssetSource(selected) : selected ++ // RN Image copies source dimensions into its layout; let the container set the size. ++ return typeof largest === 'object' ++ ? { uri: largest.uri, headers: 'headers' in largest ? largest.headers : undefined, scale: largest.scale } ++ : largest ++} ++ ++const FastImageComponent = forwardRef<FastImageRef, FastImageProps>(({ ++ source, objectFit = 'cover', tintColor, placeholder, defaultSource, ++ onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, ...props ++}, ref) => { ++ const view = useRef<View>(null) ++ const [attempt, setAttempt] = useState(0) ++ const [loaded, setLoaded] = useState(false) ++ useImperativeHandle(ref, () => Object.assign(view.current ?? {}, { reload: () => { setLoaded(false); setAttempt(value => value + 1) } }) as FastImageRef, []) ++ const fallback = typeof placeholder === 'number' ? placeholder ++ : typeof placeholder === 'string' ? { uri: placeholder } ++ : placeholder && 'uri' in placeholder && placeholder.uri ? { uri: placeholder.uri, headers: placeholder.headers } ++ : defaultSource ++ return <View ref={view} {...props} style={[styles.container, style]}> ++ {!loaded && fallback !== undefined && <Image source={imageSource(fallback) as never} resizeMode={fits[objectFit]} style={StyleSheet.absoluteFill} />} ++ <Image ++ key={attempt} ++ source={imageSource(source) as never} ++ resizeMode={fits[objectFit]} ++ tintColor={tintColor} ++ style={[StyleSheet.absoluteFill, tintColor === undefined ? null : { tintColor }]} ++ onLoadStart={() => { setLoaded(false); onLoadStart?.() }} ++ onProgress={event => onProgress?.({ nativeEvent: { loaded: event.nativeEvent.loaded, total: event.nativeEvent.total } } as never)} ++ onLoad={event => { ++ const loaded = (width: number, height: number) => { ++ setLoaded(true) ++ onLoad?.({ nativeEvent: { width, height } }) ++ onLoadEnd?.({ ok: true, width, height }) ++ } ++ // React Native macOS scales a 1x image's size by the screen's; getSize reads its pixels. ++ const { uri, width, height } = event.nativeEvent.source ++ if (uri) Image.getSize(uri).then(size => loaded(size.width, size.height), () => loaded(width, height)) ++ else loaded(width, height) ++ }} ++ onError={event => { ++ const error = String(event.nativeEvent.error) ++ onError?.({ nativeEvent: { error } }) ++ onLoadEnd?.({ ok: false, error }) ++ }} ++ /> ++ </View> ++}) ++FastImageComponent.displayName = 'FastImage' ++ ++const statics: FastImageStaticProperties = { ++ resizeMode, ++ priority, ++ cacheControl, ++ preload: sources => Promise.all(sources.map(async (source): Promise<PreloadResult> => { ++ if (!source?.uri) return { ok: false, uri: source?.uri, error: 'FastImage.preload needs a uri on macOS' } ++ try { ++ await Image.prefetch(source.uri) ++ const { width, height } = await Image.getSize(source.uri) ++ return { ok: true, uri: source.uri, width, height } ++ } catch (error) { ++ return { ok: false, uri: source.uri, error: String(error) } ++ } ++ })), ++ clearMemoryCache: () => unsupported('clearMemoryCache') as Promise<ClearCacheResult>, ++ clearDiskCache: () => unsupported('clearDiskCache') as Promise<ClearCacheResult>, ++ getCachePath: () => unsupported('getCachePath') as never, ++ writeToCache: () => unsupported('writeToCache') as never, ++ configureCache: () => unsupported('configureCache') as never, ++ setDefaults: () => {}, ++} ++ ++const FastImage = Object.assign(FastImageComponent, statics) ++ ++export const FastImageBackground = forwardRef<View, FastImageBackgroundProps>( ++ ({ style, imageStyle, imageRef, imageProps, children, ...props }, ref) => { ++ const [image, container] = splitBackgroundProps(props, imageProps) ++ return <View ref={ref} {...container} style={style}> ++ <FastImageComponent {...image} style={[StyleSheet.absoluteFill, imageStyle]} ref={imageRef} /> ++ {children} ++ </View> ++ }, ++) ++FastImageBackground.displayName = 'FastImageBackground' ++ ++const styles = StyleSheet.create({ container: { overflow: 'hidden' } }) ++ ++export default FastImage diff --git a/patches/react-native-graph@1.4.0.patch b/patches/react-native-graph@1.4.0.patch new file mode 100644 index 000000000..4e60ebd19 --- /dev/null +++ b/patches/react-native-graph@1.4.0.patch @@ -0,0 +1,157 @@ +diff --git a/lib/module/AnimatedLineGraph.js b/lib/module/AnimatedLineGraph.js +index f08d7f4..5621a8b 100644 +--- a/lib/module/AnimatedLineGraph.js ++++ b/lib/module/AnimatedLineGraph.js +@@ -77,14 +77,14 @@ export function AnimatedLineGraph({ + setHeight(Math.round(layout.height)); + }, []); + const straightLine = useMemo(() => { +- const path = Skia.Path.Make(); ++ const path = Skia.PathBuilder.Make(); + path.moveTo(0, height / 2); + for (let i = 0; i < width - 1; i += 2) { + const x = i; + const y = height / 2; + path.cubicTo(x, y, x, y, x, y); + } +- return path; ++ return path.build(); + }, [height, width]); + const paths = useSharedValue({}); + const gradientPaths = useSharedValue({}); +diff --git a/lib/module/CreateGraphPath.js b/lib/module/CreateGraphPath.js +index cad0ca6..28e70c7 100644 +--- a/lib/module/CreateGraphPath.js ++++ b/lib/module/CreateGraphPath.js +@@ -49,13 +49,14 @@ function createGraphPathBase({ + canvasWidth: width, + shouldFillGradient + }) { +- const path = Skia.Path.Make(); ++ // Skia 2 builds paths with PathBuilder; SkPath itself is immutable. ++ const builder = Skia.PathBuilder.Make(); + + // Canvas width substracted by the horizontal padding => Actual drawing width + const drawingWidth = width - 2 * horizontalPadding; + // Canvas height substracted by the vertical padding => Actual drawing height + const drawingHeight = height - 2 * verticalPadding; +- if (graphData[0] == null) return path; ++ if (graphData[0] == null) return builder.build(); + const points = []; + const startX = getXInRange(drawingWidth, graphData[0].date, range.x) + horizontalPadding; + const endX = getXInRange(drawingWidth, graphData[graphData.length - 1].date, range.x) + horizontalPadding; +@@ -91,7 +92,7 @@ function createGraphPathBase({ + const point = points[i]; + + // first point needs to start the path +- if (i === 0) path.moveTo(point.x, point.y); ++ if (i === 0) builder.moveTo(point.x, point.y); + const prev = points[i - 1]; + const prevPrev = points[i - 2]; + if (prev == null) continue; +@@ -103,15 +104,19 @@ function createGraphPathBase({ + const cp2y = (p0.y + 2 * p1.y) / 3; + const cp3x = (p0.x + 4 * p1.x + point.x) / 6; + const cp3y = (p0.y + 4 * p1.y + point.y) / 6; +- path.cubicTo(cp1x, cp1y, cp2x, cp2y, cp3x, cp3y); ++ builder.cubicTo(cp1x, cp1y, cp2x, cp2y, cp3x, cp3y); + if (i === points.length - 1) { +- path.cubicTo(point.x, point.y, point.x, point.y, point.x, point.y); ++ builder.cubicTo(point.x, point.y, point.x, point.y, point.x, point.y); + } + } ++ const path = builder.build(); + if (!shouldFillGradient) return path; +- const gradientPath = path.copy(); +- gradientPath.lineTo(endX, height + verticalPadding); +- gradientPath.lineTo(0 + horizontalPadding, height + verticalPadding); ++ ++ // build() leaves the builder intact, so the gradient continues the line path. ++ const gradientPath = builder ++ .lineTo(endX, height + verticalPadding) ++ .lineTo(0 + horizontalPadding, height + verticalPadding) ++ .build(); + return { + path: path, + gradientPath: gradientPath +diff --git a/src/AnimatedLineGraph.tsx b/src/AnimatedLineGraph.tsx +index 6b12417..f1b4b0a 100644 +--- a/src/AnimatedLineGraph.tsx ++++ b/src/AnimatedLineGraph.tsx +@@ -133,7 +133,7 @@ export function AnimatedLineGraph({ + ); + + const straightLine = useMemo(() => { +- const path = Skia.Path.Make(); ++ const path = Skia.PathBuilder.Make(); + path.moveTo(0, height / 2); + for (let i = 0; i < width - 1; i += 2) { + const x = i; +@@ -141,7 +141,7 @@ export function AnimatedLineGraph({ + path.cubicTo(x, y, x, y, x, y); + } + +- return path; ++ return path.build(); + }, [height, width]); + + const paths = useSharedValue<{ from?: SkPath; to?: SkPath }>({}); +diff --git a/src/CreateGraphPath.ts b/src/CreateGraphPath.ts +index 0c7f1c0..38c4065 100644 +--- a/src/CreateGraphPath.ts ++++ b/src/CreateGraphPath.ts +@@ -144,14 +144,15 @@ function createGraphPathBase({ + }: GraphPathConfigWithGradient | GraphPathConfigWithoutGradient): + | SkPath + | GraphPathWithGradient { +- const path = Skia.Path.Make(); ++ // Skia 2 builds paths with PathBuilder; SkPath itself is immutable. ++ const builder = Skia.PathBuilder.Make(); + + // Canvas width substracted by the horizontal padding => Actual drawing width + const drawingWidth = width - 2 * horizontalPadding; + // Canvas height substracted by the vertical padding => Actual drawing height + const drawingHeight = height - 2 * verticalPadding; + +- if (graphData[0] == null) return path; ++ if (graphData[0] == null) return builder.build(); + + const points: SkPoint[] = []; + +@@ -215,7 +216,7 @@ function createGraphPathBase({ + const point = points[i]!; + + // first point needs to start the path +- if (i === 0) path.moveTo(point.x, point.y); ++ if (i === 0) builder.moveTo(point.x, point.y); + + const prev = points[i - 1]; + const prevPrev = points[i - 2]; +@@ -231,19 +232,21 @@ function createGraphPathBase({ + const cp3x = (p0.x + 4 * p1.x + point.x) / 6; + const cp3y = (p0.y + 4 * p1.y + point.y) / 6; + +- path.cubicTo(cp1x, cp1y, cp2x, cp2y, cp3x, cp3y); ++ builder.cubicTo(cp1x, cp1y, cp2x, cp2y, cp3x, cp3y); + + if (i === points.length - 1) { +- path.cubicTo(point.x, point.y, point.x, point.y, point.x, point.y); ++ builder.cubicTo(point.x, point.y, point.x, point.y, point.x, point.y); + } + } + ++ const path = builder.build(); + if (!shouldFillGradient) return path; + +- const gradientPath = path.copy(); +- +- gradientPath.lineTo(endX, height + verticalPadding); +- gradientPath.lineTo(0 + horizontalPadding, height + verticalPadding); ++ // build() leaves the builder intact, so the gradient continues the line path. ++ const gradientPath = builder ++ .lineTo(endX, height + verticalPadding) ++ .lineTo(0 + horizontalPadding, height + verticalPadding) ++ .build(); + + return { path: path, gradientPath: gradientPath }; + } diff --git a/patches/react-native-keyboard-controller@1.22.9.patch b/patches/react-native-keyboard-controller@1.22.9.patch new file mode 100644 index 000000000..f2d90e38e --- /dev/null +++ b/patches/react-native-keyboard-controller@1.22.9.patch @@ -0,0 +1,16 @@ +diff --git a/src/bindings.macos.ts b/src/bindings.macos.ts +new file mode 100644 +index 0000000..30108ce +--- /dev/null ++++ b/src/bindings.macos.ts +@@ -0,0 +1,2 @@ ++// macOS has no software keyboard: use the no-op bindings that web uses instead of the native module. ++export * from "./bindings.ts"; +diff --git a/src/reanimated.macos.ts b/src/reanimated.macos.ts +new file mode 100644 +index 0000000..fd25177 +--- /dev/null ++++ b/src/reanimated.macos.ts +@@ -0,0 +1,2 @@ ++// macOS has no software keyboard: keyboard handlers are no-ops, as on web. ++export * from "./reanimated.ts"; diff --git a/patches/react-native-masked-view-masked-view@0.3.2.patch b/patches/react-native-masked-view-masked-view@0.3.2.patch new file mode 100644 index 000000000..0bc06e14e --- /dev/null +++ b/patches/react-native-masked-view-masked-view@0.3.2.patch @@ -0,0 +1,140 @@ +diff --git a/RNCMaskedView.podspec b/RNCMaskedView.podspec +index b29590c..6eefd94 100644 +--- a/RNCMaskedView.podspec ++++ b/RNCMaskedView.podspec +@@ -10,7 +10,7 @@ Pod::Spec.new do |s| + + s.authors = package['author'] + s.homepage = package['homepage'] +- s.platforms = { :ios => "9.0", :tvos => "9.0" } ++ s.platforms = { :ios => "9.0", :tvos => "9.0", :osx => "14.0" } + + s.source = { :git => "https://github.com/react-native-masked-view/masked-view.git", :tag => "v#{s.version}" } + s.source_files = "ios/**/*.{h,m}" +diff --git a/ios/RNCMaskedView.h b/ios/RNCMaskedView.h +index afaa6c3..9f80599 100644 +--- a/ios/RNCMaskedView.h ++++ b/ios/RNCMaskedView.h +@@ -5,7 +5,10 @@ + * LICENSE file in the root directory of this source tree. + */ + ++#include <TargetConditionals.h> ++#if !TARGET_OS_OSX + #import <UIKit/UIKit.h> ++#endif + + #import <React/RCTView.h> + +diff --git a/ios/RNCMaskedView.m b/ios/RNCMaskedView.m +index e8e51ac..dda4fa1 100644 +--- a/ios/RNCMaskedView.m ++++ b/ios/RNCMaskedView.m +@@ -9,6 +9,86 @@ + + #import <React/UIView+React.h> + ++#if TARGET_OS_OSX ++#import <QuartzCore/QuartzCore.h> ++ ++// AppKit has no maskView. The mask stays a live view inside an invisible host so it keeps drawing; ++// each frame it is rendered into the layer mask of a container holding the other children. ++@implementation RNCMaskedView { ++ RCTUIView *_maskHost; ++ RCTUIView *_contentView; ++ CALayer *_maskLayer; ++ CADisplayLink *_displayLink; ++} ++ ++- (instancetype)initWithFrame:(NSRect)frame ++{ ++ if (self = [super initWithFrame:frame]) { ++ _maskHost = [[RCTUIView alloc] initWithFrame:self.bounds]; ++ _maskHost.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; ++ _maskHost.alphaValue = 0; ++ _contentView = [[RCTUIView alloc] initWithFrame:self.bounds]; ++ _contentView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; ++ _contentView.wantsLayer = YES; ++ _maskLayer = [CALayer layer]; ++ _contentView.layer.mask = _maskLayer; ++ [self addSubview:_maskHost]; ++ [self addSubview:_contentView]; ++ } ++ return self; ++} ++ ++- (void)viewDidMoveToWindow ++{ ++ [super viewDidMoveToWindow]; ++ [_displayLink invalidate]; ++ _displayLink = nil; ++ if (self.window) { ++ _displayLink = [self displayLinkWithTarget:self selector:@selector(updateMask)]; ++ [_displayLink addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes]; ++ } ++} ++ ++- (void)updateMask ++{ ++ NSView *maskView = _maskHost.subviews.firstObject; ++ CGSize size = _contentView.bounds.size; ++ CGFloat scale = self.window.backingScaleFactor ?: 1; ++ CGImageRef image = NULL; ++ if (maskView.layer && size.width > 0 && size.height > 0) { ++ CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); ++ CGContextRef context = CGBitmapContextCreate(NULL, ceil(size.width * scale), ceil(size.height * scale), 8, 0, colorSpace, kCGImageAlphaPremultipliedLast); ++ CGColorSpaceRelease(colorSpace); ++ // The mask image is drawn unflipped; React's views are flipped. ++ CGContextTranslateCTM(context, 0, ceil(size.height * scale)); ++ CGContextScaleCTM(context, scale, -scale); ++ [maskView.layer renderInContext:context]; ++ image = CGBitmapContextCreateImage(context); ++ CGContextRelease(context); ++ } ++ [CATransaction begin]; ++ [CATransaction setDisableActions:YES]; ++ _maskLayer.frame = _contentView.layer.bounds; ++ _maskLayer.contents = (__bridge_transfer id)image; ++ [CATransaction commit]; ++} ++ ++- (void)didUpdateReactSubviews ++{ ++ // RNCMaskedView expects that the first subview rendered is the mask. ++ NSArray<NSView *> *subviews = self.reactSubviews; ++ if (subviews.firstObject) { ++ [_maskHost addSubview:subviews.firstObject]; ++ } ++ for (NSUInteger i = 1; i < subviews.count; i++) { ++ [_contentView addSubview:subviews[i]]; ++ } ++ [self updateMask]; ++} ++ ++@end ++ ++#else + @implementation RNCMaskedView + + - (void)didUpdateReactSubviews +@@ -32,3 +112,4 @@ - (void)displayLayer:(CALayer *)layer + } + + @end ++#endif +diff --git a/ios/RNCMaskedViewManager.m b/ios/RNCMaskedViewManager.m +index 339ca26..977cbbd 100644 +--- a/ios/RNCMaskedViewManager.m ++++ b/ios/RNCMaskedViewManager.m +@@ -13,7 +13,11 @@ @implementation RNCMaskedViewManager + + RCT_EXPORT_MODULE() + ++#if TARGET_OS_OSX ++- (RCTPlatformView *)view ++#else + - (UIView *)view ++#endif + { + return [RNCMaskedView new]; + } diff --git a/patches/react-native-mmkv@4.3.2.patch b/patches/react-native-mmkv@4.3.2.patch new file mode 100644 index 000000000..d7bb12dfa --- /dev/null +++ b/patches/react-native-mmkv@4.3.2.patch @@ -0,0 +1,40 @@ +diff --git a/ios/HybridMMKVPlatformContext.swift b/ios/HybridMMKVPlatformContext.swift +index 0d19d4a..0644cbc 100644 +--- a/ios/HybridMMKVPlatformContext.swift ++++ b/ios/HybridMMKVPlatformContext.swift +@@ -12,6 +12,8 @@ class HybridMMKVPlatformContext: HybridMMKVPlatformContextSpec { + static var directory: FileManager.SearchPathDirectory { + #if os(tvOS) + return .cachesDirectory ++#elseif os(macOS) ++ return .applicationSupportDirectory + #else + return .documentDirectory + #endif +@@ -25,10 +27,26 @@ class HybridMMKVPlatformContext: HybridMMKVPlatformContextSpec { + } + + // append /mmkv to it ++#if os(macOS) ++ // Application Support is shared by every Mac app, so scope MMKV like Spark's other app data. ++ let basePath = documentsPath.appendingPathComponent(Self.namespace, isDirectory: true).appendingPathComponent("mmkv", conformingTo: .directory) ++#else + let basePath = documentsPath.appendingPathComponent("mmkv", conformingTo: .directory) ++#endif + return basePath.path + } + ++#if os(macOS) ++ private static var namespace: String { ++ // Spark's host exports SparkNamespace(); look it up at runtime so MMKV keeps no link dependency on Spark. ++ typealias Namespace = @convention(c) () -> Unmanaged<NSString> ++ if let symbol = dlsym(UnsafeMutableRawPointer(bitPattern: -2), "SparkNamespace") { ++ return unsafeBitCast(symbol, to: Namespace.self)().takeUnretainedValue() as String ++ } ++ return Bundle.main.bundleIdentifier ?? ProcessInfo.processInfo.processName ++ } ++#endif ++ + func getAppGroupDirectory() throws -> String? { + // Read `AppGroupIdentifier` from `Info.plist` + guard let appGroupID = Bundle.main.object(forInfoDictionaryKey: "AppGroupIdentifier") as? String else { diff --git a/patches/react-native-nitro-fetch@1.9.0.patch b/patches/react-native-nitro-fetch@1.9.0.patch new file mode 100644 index 000000000..687b3709c --- /dev/null +++ b/patches/react-native-nitro-fetch@1.9.0.patch @@ -0,0 +1,130 @@ +diff --git a/NitroFetch.podspec b/NitroFetch.podspec +index 8b08083..d08a41d 100644 +--- a/NitroFetch.podspec ++++ b/NitroFetch.podspec +@@ -10,7 +10,7 @@ Pod::Spec.new do |s| + s.license = package["license"] + s.authors = package["author"] + +- s.platforms = { :ios => min_ios_version_supported, :tvos => min_ios_version_supported } ++ s.platforms = { :ios => min_ios_version_supported, :tvos => min_ios_version_supported, :osx => "14.0" } + s.source = { :git => "https://github.com/margelo/react-native-nitro-fetch.git", :tag => "v#{s.version}" } + + +diff --git a/ios/HybridNativeStorage.swift b/ios/HybridNativeStorage.swift +index 68e5246..51bcf3b 100644 +--- a/ios/HybridNativeStorage.swift ++++ b/ios/HybridNativeStorage.swift +@@ -10,7 +10,11 @@ import Security + /// Keep `keychainService` and `encPrefix` in sync with `NitroWSSecureAtRest.swift` in nitro-websockets. + internal enum NitroFetchSecureAtRest { + static let encPrefix = "nfc1:" ++#if os(macOS) ++ private static let keychainService = "com.margelo.nitrofetch.aesgcm.v1.\(AppNamespace.value)" ++#else + private static let keychainService = "com.margelo.nitrofetch.aesgcm.v1" ++#endif + private static let keychainAccount = "master" + + private static func loadOrCreateSymmetricKey() throws -> SymmetricKey { +@@ -119,7 +123,11 @@ internal enum NitroFetchSecureAtRest { + } + + final class HybridNativeStorage: HybridNativeStorageSpec { ++#if os(macOS) ++ private static let suiteName = "nitro_fetch_storage.\(AppNamespace.value)" ++#else + private static let suiteName = "nitro_fetch_storage" ++#endif + + private let userDefaults: UserDefaults + +diff --git a/ios/NitroAutoPrefetcher.swift b/ios/NitroAutoPrefetcher.swift +index 18ffea9..8b69d1f 100644 +--- a/ios/NitroAutoPrefetcher.swift ++++ b/ios/NitroAutoPrefetcher.swift +@@ -6,7 +6,11 @@ public final class NitroAutoPrefetcher: NSObject { + // Serializes queue reads/writes and keeps Keychain work off the launch thread. + private static let workQueue = DispatchQueue(label: "com.margelo.nitrofetch.autoprefetch") + private static let queueKey = "nitrofetch_autoprefetch_queue" ++#if os(macOS) ++ private static let suiteName = "nitro_fetch_storage.\(AppNamespace.value)" ++#else + private static let suiteName = "nitro_fetch_storage" ++#endif + private static let tokenRefreshKey = "nitro_token_refresh_fetch" + private static let tokenCacheKey = "nitro_token_refresh_fetch_cache" + +diff --git a/ios/NitroBootstrap.mm b/ios/NitroBootstrap.mm +index 8e23106..41ad6fc 100644 +--- a/ios/NitroBootstrap.mm ++++ b/ios/NitroBootstrap.mm +@@ -1,6 +1,8 @@ + #import <Foundation/Foundation.h> + #if __has_include(<UIKit/UIKit.h>) + #import <UIKit/UIKit.h> ++#elif __has_include(<AppKit/AppKit.h>) ++#import <AppKit/AppKit.h> + #endif + + @interface NitroAutoPrefetcher : NSObject +@@ -22,6 +24,15 @@ + (void)load { + [NitroAutoPrefetcher prefetchOnStart]; + }); + } ++#elif __has_include(<AppKit/AppKit.h>) ++ [[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationDidFinishLaunchingNotification ++ object:nil queue:nil ++ usingBlock:^(__unused NSNotification *note) { ++ [NitroAutoPrefetcher prefetchOnStart]; ++ }]; ++ dispatch_async(dispatch_get_main_queue(), ^{ ++ [NitroAutoPrefetcher prefetchOnStart]; ++ }); + #endif + } + @end +diff --git a/ios/NitroURLSession.swift b/ios/NitroURLSession.swift +index 871df60..0505ac4 100644 +--- a/ios/NitroURLSession.swift ++++ b/ios/NitroURLSession.swift +@@ -18,12 +18,39 @@ private final class NitroPinningURLSessionDelegate: NSObject, URLSessionDelegate + } + + enum NitroURLCache { ++#if os(macOS) ++ // A relative diskPath resolves against the working directory on macOS, so keep the cache in ++ // the app's Caches folder, scoped like Spark's other app data. ++ static let shared = URLCache( ++ memoryCapacity: 32 * 1024 * 1024, ++ diskCapacity: 100 * 1024 * 1024, ++ directory: FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] ++ .appendingPathComponent(AppNamespace.value, isDirectory: true) ++ .appendingPathComponent("nitrofetch_urlcache", isDirectory: true) ++ ) ++#else + static let shared = URLCache( + memoryCapacity: 32 * 1024 * 1024, + diskCapacity: 100 * 1024 * 1024, + diskPath: "nitrofetch_urlcache" + ) ++#endif ++} ++ ++#if os(macOS) ++/// Mac apps share ~/Library, user defaults suites and Keychain services, so Nitro Fetch's stores are ++/// scoped like Spark's other app data. ++enum AppNamespace { ++ static let value: String = { ++ // Spark's host exports SparkNamespace(); look it up at runtime so Nitro Fetch keeps no link dependency on Spark. ++ typealias Namespace = @convention(c) () -> Unmanaged<NSString> ++ if let symbol = dlsym(UnsafeMutableRawPointer(bitPattern: -2), "SparkNamespace") { ++ return unsafeBitCast(symbol, to: Namespace.self)().takeUnretainedValue() as String ++ } ++ return Bundle.main.bundleIdentifier ?? ProcessInfo.processInfo.processName ++ }() + } ++#endif + + enum NitroURLSession { + static let shared = make(configuration: .default) diff --git a/patches/react-native-nitro-zxing@0.3.0.patch b/patches/react-native-nitro-zxing@0.3.0.patch new file mode 100644 index 000000000..8ada7e431 --- /dev/null +++ b/patches/react-native-nitro-zxing@0.3.0.patch @@ -0,0 +1,13 @@ +diff --git a/VisionCameraZxing.podspec b/VisionCameraZxing.podspec +index 301b92f..c4fda73 100644 +--- a/VisionCameraZxing.podspec ++++ b/VisionCameraZxing.podspec +@@ -20,7 +20,7 @@ Pod::Spec.new do |s| + s.license = package["license"] + s.authors = package["author"] + +- s.platforms = { :ios => min_ios_version_supported, :visionos => 1.0 } ++ s.platforms = { :ios => min_ios_version_supported, :visionos => 1.0, :osx => "14.0" } + s.source = { :git => "https://github.com/margelo/react-native-vision-camera-zxing.git", :tag => "v#{s.version}" } + + s.source_files = [ diff --git a/patches/react-native-quick-base64@3.0.1.patch b/patches/react-native-quick-base64@3.0.1.patch new file mode 100644 index 000000000..3819d1535 --- /dev/null +++ b/patches/react-native-quick-base64@3.0.1.patch @@ -0,0 +1,13 @@ +diff --git a/react-native-quick-base64.podspec b/react-native-quick-base64.podspec +index eb43697..f02ee48 100644 +--- a/react-native-quick-base64.podspec ++++ b/react-native-quick-base64.podspec +@@ -10,7 +10,7 @@ Pod::Spec.new do |s| + s.license = package["license"] + s.authors = package["author"] + +- s.platforms = { :ios => min_ios_version_supported } ++ s.platforms = { :ios => min_ios_version_supported, :osx => "14.0" } + s.source = { :git => "https://github.com/craftzdog/react-native-quick-base64.git", :tag => "#{s.version}" } + + s.source_files = "ios/**/*.{h,m,mm}", "cpp/**/*.{hpp,cpp,c,h}", "ios/generated/*.{h,cpp,mm}" diff --git a/patches/react-native-quick-crypto@1.1.9.patch b/patches/react-native-quick-crypto@1.1.9.patch new file mode 100644 index 000000000..c53f72d98 --- /dev/null +++ b/patches/react-native-quick-crypto@1.1.9.patch @@ -0,0 +1,31 @@ +diff --git a/QuickCrypto.podspec b/QuickCrypto.podspec +index 8e026e5..0c45a92 100644 +--- a/QuickCrypto.podspec ++++ b/QuickCrypto.podspec +@@ -198,7 +198,10 @@ Pod::Spec.new do |s| + # Disable x86 SIMD intrinsics on iOS simulator — the .c implementation files are already + # excluded above, but blake3_dispatch.c still references the symbols unless these macros + # are defined. Mirrors the Android CMakeLists.txt approach (line 18-22). +- "GCC_PREPROCESSOR_DEFINITIONS[sdk=iphonesimulator*][arch=x86_64]" => "$(inherited) BLAKE3_NO_AVX512 BLAKE3_NO_AVX2 BLAKE3_NO_SSE41 BLAKE3_NO_SSE2 NCRYPTO_EVP_ONLY=1" ++ "GCC_PREPROCESSOR_DEFINITIONS[sdk=iphonesimulator*][arch=x86_64]" => "$(inherited) BLAKE3_NO_AVX512 BLAKE3_NO_AVX2 BLAKE3_NO_SSE41 BLAKE3_NO_SSE2 NCRYPTO_EVP_ONLY=1", ++ # Intel Macs need the same: no NEON source and no x86 SIMD symbols. ++ "EXCLUDED_SOURCE_FILE_NAMES[sdk=macosx*][arch=x86_64]" => "deps/blake3/c/blake3_neon.c", ++ "GCC_PREPROCESSOR_DEFINITIONS[sdk=macosx*][arch=x86_64]" => "$(inherited) BLAKE3_NO_AVX512 BLAKE3_NO_AVX2 BLAKE3_NO_SSE41 BLAKE3_NO_SSE2 NCRYPTO_EVP_ONLY=1" + } + + # Every translation unit calls the renamed OpenSSL symbols. Missing a rename +diff --git a/deps/fastpbkdf2/fastpbkdf2.c b/deps/fastpbkdf2/fastpbkdf2.c +index ee2d2f0..b04ec59 100644 +--- a/deps/fastpbkdf2/fastpbkdf2.c ++++ b/deps/fastpbkdf2/fastpbkdf2.c +@@ -17,8 +17,8 @@ + #include <assert.h> + #include <string.h> + #if defined(__GNUC__) +- #if TARGET_OS_MACCATALYST +- #include <machine/endian.h> // Mac Catalyst ++ #if TARGET_OS_MACCATALYST || TARGET_OS_OSX ++ #include <machine/endian.h> // Mac Catalyst, macOS + #else + #include <endian.h> // iOS + #endif diff --git a/patches/react-native-release-profiler@0.4.5.patch b/patches/react-native-release-profiler@0.4.5.patch new file mode 100644 index 000000000..2cc0823b5 --- /dev/null +++ b/patches/react-native-release-profiler@0.4.5.patch @@ -0,0 +1,37 @@ +diff --git a/ios/ReleaseProfiler.mm b/ios/ReleaseProfiler.mm +index b38beb6..50e668d 100644 +--- a/ios/ReleaseProfiler.mm ++++ b/ios/ReleaseProfiler.mm +@@ -1,5 +1,6 @@ + #import <React/RCTBridgeModule.h> + #import <hermes/hermes.h> ++#include <dlfcn.h> + + #ifdef __has_include + #if __has_include(<cxxreact/ReactNativeVersion.h>) +@@ -51,6 +52,12 @@ + (BOOL)requiresMainQueueSetup + withRejecter:(RCTPromiseRejectBlock)reject) { + NSFileManager* sharedFM = [NSFileManager defaultManager]; + NSURL *appDir=[[sharedFM URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] objectAtIndex:0]; ++#if TARGET_OS_OSX ++ // ~/Library/Caches is shared by every Mac app; use the app's own folder, scoped like Spark's other app data. ++ NSString *(*sparkNamespace)(void) = (NSString *(*)(void))dlsym(RTLD_DEFAULT, "SparkNamespace"); ++ appDir = [appDir URLByAppendingPathComponent:sparkNamespace ? sparkNamespace() : NSBundle.mainBundle.bundleIdentifier isDirectory:YES]; ++ [sharedFM createDirectoryAtURL:appDir withIntermediateDirectories:YES attributes:nil error:nil]; ++#endif + NSString * tempFilename = [NSString stringWithFormat:@"profile-%@%@", [[NSProcessInfo processInfo] globallyUniqueString], @".cpuprofile"]; + NSURL *fileURL=[appDir URLByAppendingPathComponent:tempFilename]; + std::string finalPath = std::string([fileURL.path UTF8String]); +diff --git a/react-native-release-profiler.podspec b/react-native-release-profiler.podspec +index 135d951..b5efe5e 100644 +--- a/react-native-release-profiler.podspec ++++ b/react-native-release-profiler.podspec +@@ -11,7 +11,7 @@ Pod::Spec.new do |s| + s.license = package["license"] + s.authors = package["author"] + +- s.platforms = { :ios => "11.0" } ++ s.platforms = { :ios => "11.0", :osx => "14.0" } + s.source = { :git => "https://github.com/Szymon20000/react-native-release-profiler.git", :tag => "#{s.version}" } + + s.source_files = "ios/**/*.{h,m,mm,swift}" diff --git a/patches/react-native-screen-corner-radius@0.2.3.patch b/patches/react-native-screen-corner-radius@0.2.3.patch new file mode 100644 index 000000000..79977ef7b --- /dev/null +++ b/patches/react-native-screen-corner-radius@0.2.3.patch @@ -0,0 +1,13 @@ +diff --git a/src/index.ts b/src/index.ts +index 60f89f5..369c39a 100644 +--- a/src/index.ts ++++ b/src/index.ts +@@ -4,7 +4,7 @@ import { NativeModules } from 'react-native'; + * The current device's screen's corner radius. May also be `0` if the corner radius couldn't be detected. + */ + export const ScreenCornerRadius: number = +- NativeModules.ScreenCornerRadius.cornerRadius ?? 0; ++ NativeModules.ScreenCornerRadius?.cornerRadius ?? 0; + + /** + * `true` if the screen has rounded corners, `false` otherwise. diff --git a/patches/react-native-screens@4.23.0.patch b/patches/react-native-screens@4.23.0.patch new file mode 100644 index 000000000..8e1fc4ab7 --- /dev/null +++ b/patches/react-native-screens@4.23.0.patch @@ -0,0 +1,160 @@ +diff --git a/src/components/DebugContainer.macos.tsx b/src/components/DebugContainer.macos.tsx +new file mode 100644 +index 0000000..7526909 +--- /dev/null ++++ b/src/components/DebugContainer.macos.tsx +@@ -0,0 +1,3 @@ ++// macOS has no native screens: use the web implementation's plain views. ++export * from './DebugContainer.web'; ++export { default } from './DebugContainer.web'; +diff --git a/src/components/FullWindowOverlay.macos.tsx b/src/components/FullWindowOverlay.macos.tsx +new file mode 100644 +index 0000000..43c70ba +--- /dev/null ++++ b/src/components/FullWindowOverlay.macos.tsx +@@ -0,0 +1,3 @@ ++// macOS has no native screens: use the web implementation's plain views. ++export * from './FullWindowOverlay.web'; ++export { default } from './FullWindowOverlay.web'; +diff --git a/src/components/Screen.macos.tsx b/src/components/Screen.macos.tsx +new file mode 100644 +index 0000000..2cd1f2a +--- /dev/null ++++ b/src/components/Screen.macos.tsx +@@ -0,0 +1,3 @@ ++// macOS has no native screens: use the web implementation's plain views. ++export * from './Screen.web'; ++export { default } from './Screen.web'; +diff --git a/src/components/ScreenContainer.macos.tsx b/src/components/ScreenContainer.macos.tsx +new file mode 100644 +index 0000000..710309d +--- /dev/null ++++ b/src/components/ScreenContainer.macos.tsx +@@ -0,0 +1,3 @@ ++// macOS has no native screens: use the web implementation's plain views. ++export * from './ScreenContainer.web'; ++export { default } from './ScreenContainer.web'; +diff --git a/src/components/ScreenContentWrapper.macos.tsx b/src/components/ScreenContentWrapper.macos.tsx +new file mode 100644 +index 0000000..ff03c98 +--- /dev/null ++++ b/src/components/ScreenContentWrapper.macos.tsx +@@ -0,0 +1,3 @@ ++// macOS has no native screens: use the web implementation's plain views. ++export * from './ScreenContentWrapper.web'; ++export { default } from './ScreenContentWrapper.web'; +diff --git a/src/components/ScreenFooter.macos.tsx b/src/components/ScreenFooter.macos.tsx +new file mode 100644 +index 0000000..8a6e94e +--- /dev/null ++++ b/src/components/ScreenFooter.macos.tsx +@@ -0,0 +1,3 @@ ++// macOS has no native screens: use the web implementation's plain views. ++export * from './ScreenFooter.web'; ++export { default } from './ScreenFooter.web'; +diff --git a/src/components/ScreenStack.macos.tsx b/src/components/ScreenStack.macos.tsx +new file mode 100644 +index 0000000..48b1e73 +--- /dev/null ++++ b/src/components/ScreenStack.macos.tsx +@@ -0,0 +1,3 @@ ++// macOS has no native screens: use the web implementation's plain views. ++export * from './ScreenStack.web'; ++export { default } from './ScreenStack.web'; +diff --git a/src/components/ScreenStackHeaderConfig.macos.tsx b/src/components/ScreenStackHeaderConfig.macos.tsx +new file mode 100644 +index 0000000..b613332 +--- /dev/null ++++ b/src/components/ScreenStackHeaderConfig.macos.tsx +@@ -0,0 +1,2 @@ ++// macOS has no native screens: use the web implementation's plain views. ++export * from './ScreenStackHeaderConfig.web'; +diff --git a/src/components/SearchBar.macos.tsx b/src/components/SearchBar.macos.tsx +new file mode 100644 +index 0000000..939c989 +--- /dev/null ++++ b/src/components/SearchBar.macos.tsx +@@ -0,0 +1,3 @@ ++// macOS has no native screens: use the web implementation's plain views. ++export * from './SearchBar.web'; ++export { default } from './SearchBar.web'; +diff --git a/src/components/gamma/split/SplitHost.macos.tsx b/src/components/gamma/split/SplitHost.macos.tsx +new file mode 100644 +index 0000000..7f7f91a +--- /dev/null ++++ b/src/components/gamma/split/SplitHost.macos.tsx +@@ -0,0 +1,3 @@ ++// macOS has no native screens: use the web implementation's plain views. ++export * from './SplitHost.web'; ++export { default } from './SplitHost.web'; +diff --git a/src/components/gamma/split/SplitScreen.macos.tsx b/src/components/gamma/split/SplitScreen.macos.tsx +new file mode 100644 +index 0000000..049aa9f +--- /dev/null ++++ b/src/components/gamma/split/SplitScreen.macos.tsx +@@ -0,0 +1,3 @@ ++// macOS has no native screens: use the web implementation's plain views. ++export * from './SplitScreen.web'; ++export { default } from './SplitScreen.web'; +diff --git a/src/components/gamma/stack/StackHost.macos.tsx b/src/components/gamma/stack/StackHost.macos.tsx +new file mode 100644 +index 0000000..ea6c2a9 +--- /dev/null ++++ b/src/components/gamma/stack/StackHost.macos.tsx +@@ -0,0 +1,3 @@ ++// macOS has no native screens: use the web implementation's plain views. ++export * from './StackHost.web'; ++export { default } from './StackHost.web'; +diff --git a/src/components/gamma/stack/StackScreen.macos.tsx b/src/components/gamma/stack/StackScreen.macos.tsx +new file mode 100644 +index 0000000..f6b6910 +--- /dev/null ++++ b/src/components/gamma/stack/StackScreen.macos.tsx +@@ -0,0 +1,3 @@ ++// macOS has no native screens: use the web implementation's plain views. ++export * from './StackScreen.web'; ++export { default } from './StackScreen.web'; +diff --git a/src/components/safe-area/SafeAreaView.macos.tsx b/src/components/safe-area/SafeAreaView.macos.tsx +new file mode 100644 +index 0000000..e96a644 +--- /dev/null ++++ b/src/components/safe-area/SafeAreaView.macos.tsx +@@ -0,0 +1,3 @@ ++// macOS has no native screens: use the web implementation's plain views. ++export * from './SafeAreaView.web'; ++export { default } from './SafeAreaView.web'; +diff --git a/src/components/tabs/TabsHost.macos.tsx b/src/components/tabs/TabsHost.macos.tsx +new file mode 100644 +index 0000000..fa5d559 +--- /dev/null ++++ b/src/components/tabs/TabsHost.macos.tsx +@@ -0,0 +1,3 @@ ++// macOS has no native screens: use the web implementation's plain views. ++export * from './TabsHost.web'; ++export { default } from './TabsHost.web'; +diff --git a/src/components/tabs/TabsScreen.macos.tsx b/src/components/tabs/TabsScreen.macos.tsx +new file mode 100644 +index 0000000..8d72134 +--- /dev/null ++++ b/src/components/tabs/TabsScreen.macos.tsx +@@ -0,0 +1,3 @@ ++// macOS has no native screens: use the web implementation's plain views. ++export * from './TabsScreen.web'; ++export { default } from './TabsScreen.web'; +diff --git a/src/fabric/NativeScreensModule.macos.ts b/src/fabric/NativeScreensModule.macos.ts +new file mode 100644 +index 0000000..f242de2 +--- /dev/null ++++ b/src/fabric/NativeScreensModule.macos.ts +@@ -0,0 +1,3 @@ ++// macOS has no native screens: use the web implementation's plain views. ++export * from './NativeScreensModule.web'; ++export { default } from './NativeScreensModule.web'; +diff --git a/src/gesture-handler/fabricUtils.macos.ts b/src/gesture-handler/fabricUtils.macos.ts +new file mode 100644 +index 0000000..56afb7a +--- /dev/null ++++ b/src/gesture-handler/fabricUtils.macos.ts +@@ -0,0 +1,2 @@ ++// macOS has no native screens: use the web implementation's plain views. ++export * from './fabricUtils.web'; diff --git a/patches/react-native-svg@15.15.3.patch b/patches/react-native-svg@15.15.3.patch new file mode 100644 index 000000000..b1b871e3a --- /dev/null +++ b/patches/react-native-svg@15.15.3.patch @@ -0,0 +1,31 @@ +diff --git a/apple/Elements/RNSVGSvgView.mm b/apple/Elements/RNSVGSvgView.mm +index d03d5c0..a3e679f 100644 +--- a/apple/Elements/RNSVGSvgView.mm ++++ b/apple/Elements/RNSVGSvgView.mm +@@ -315,6 +315,15 @@ - (void)drawToContext:(CGContextRef)context withRect:(CGRect)rect + } + } + ++#if TARGET_OS_OSX ++// React Native macOS component views draw through updateLayer, so AppKit never calls drawRect:, ++// which is where the SVG renders. ++- (BOOL)wantsUpdateLayer ++{ ++ return NO; ++} ++#endif ++ + - (void)drawRect:(CGRect)rect + { + RNSVGPlatformView *parent = self.superview; +diff --git a/apple/RNSVGUIKit.macos.mm b/apple/RNSVGUIKit.macos.mm +index aa9f94b..f3bbf76 100644 +--- a/apple/RNSVGUIKit.macos.mm ++++ b/apple/RNSVGUIKit.macos.mm +@@ -1,5 +1,6 @@ + #import "RNSVGUIKit.h" + #import <objc/runtime.h> ++#import <React/RCTAssert.h> + + #if TARGET_OS_OSX + diff --git a/patches/react-native-unistyles@3.6.0.patch b/patches/react-native-unistyles@3.6.0.patch new file mode 100644 index 000000000..d840f8ba2 --- /dev/null +++ b/patches/react-native-unistyles@3.6.0.patch @@ -0,0 +1,203 @@ +diff --git a/Unistyles.podspec b/Unistyles.podspec +index adf83bf..ba3ba7f 100644 +--- a/Unistyles.podspec ++++ b/Unistyles.podspec +@@ -11,7 +11,7 @@ Pod::Spec.new do |s| + s.license = package["license"] + s.authors = package["author"] + +- s.platforms = { :ios => min_ios_version_supported } ++ s.platforms = { :ios => min_ios_version_supported, :osx => "14.0" } + s.source = { :git => package["repository"], :tag => "#{s.version}" } + + s.source_files = [ +diff --git a/ios/Extensions.swift b/ios/Extensions.swift +index 9b38a32..a707de7 100644 +--- a/ios/Extensions.swift ++++ b/ios/Extensions.swift +@@ -1,5 +1,7 @@ + import Foundation + ++#if os(iOS) ++ + extension UIColor { + // int is universal way of color representation + // iOS API expects UIColor, so we need to translate it +@@ -11,3 +13,4 @@ extension UIColor { + return UIColor(red: red, green: green, blue: blue, alpha: 1.0) + } + } ++#endif +diff --git a/ios/NativePlatform+keyboard.swift b/ios/NativePlatform+keyboard.swift +index 0237e76..b95f651 100644 +--- a/ios/NativePlatform+keyboard.swift ++++ b/ios/NativePlatform+keyboard.swift +@@ -1,3 +1,4 @@ ++#if os(iOS) + import Foundation + + struct KeyboardAnimation { +@@ -103,3 +104,5 @@ extension NativeIOSPlatform { + } + } + } ++ ++#endif +diff --git a/ios/NativePlatform+macos.swift b/ios/NativePlatform+macos.swift +new file mode 100644 +index 0000000..ac219cf +--- /dev/null ++++ b/ios/NativePlatform+macos.swift +@@ -0,0 +1,124 @@ ++#if os(macOS) ++ ++import AppKit ++import NitroModules ++ ++// AppKit counterpart of NativeIOSPlatform: a desktop window has no safe-area, status bar, ++// navigation bar or software keyboard, so those report zero; the window is the "screen". ++class NativeMacOSPlatform: HybridNativePlatformSpec { ++ var miniRuntime: UnistylesNativeMiniRuntime? ++ var dependencyListeners: Array<(Array<UnistyleDependency>, UnistylesNativeMiniRuntime) -> Void> = [] ++ var observers: [NSObjectProtocol] = [] ++ var appearanceObservation: NSKeyValueObservation? ++ ++ override init() { ++ super.init() ++ self.miniRuntime = onMain { self.buildMiniRuntime() } ++ let center = NotificationCenter.default ++ for name in [NSWindow.didResizeNotification, NSWindow.didBecomeMainNotification, NSWindow.didChangeBackingPropertiesNotification] { ++ observers.append(center.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in self?.onNativePlatformChange() }) ++ } ++ appearanceObservation = NSApplication.shared.observe(\.effectiveAppearance) { [weak self] _, _ in ++ DispatchQueue.main.async { self?.onNativePlatformChange() } ++ } ++ } ++ ++ deinit { ++ observers.forEach { NotificationCenter.default.removeObserver($0) } ++ } ++ ++ func onMain<T>(_ body: () -> T) -> T { ++ Thread.isMainThread ? body() : DispatchQueue.main.sync(execute: body) ++ } ++ ++ var mainWindow: NSWindow? { NSApplication.shared.mainWindow ?? NSApplication.shared.keyWindow ?? NSApplication.shared.windows.first { $0.isVisible } } ++ ++ func buildMiniRuntime() -> UnistylesNativeMiniRuntime { ++ let screen = getScreenDimensions() ++ return UnistylesNativeMiniRuntime( ++ colorScheme: getColorScheme(), ++ screen: screen, ++ contentSizeCategory: getContentSizeCategory(), ++ insets: getInsets(), ++ pixelRatio: getPixelRatio(), ++ fontScale: getFontScale(), ++ rtl: getPrefersRtlDirection(), ++ statusBar: getStatusBarDimensions(), ++ navigationBar: getNavigationBarDimensions(), ++ isPortrait: screen.height >= screen.width, ++ isLandscape: screen.width > screen.height ++ ) ++ } ++ ++ func getMiniRuntime() -> UnistylesNativeMiniRuntime { miniRuntime! } ++ ++ func getColorScheme() -> ColorScheme { ++ onMain { NSApplication.shared.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua ? .dark : .light } ++ } ++ ++ func getFontScale() -> Double { 1 } ++ ++ func getContentSizeCategory() -> String { "unspecified" } ++ ++ func getScreenDimensions() -> Dimensions { ++ onMain { ++ let size = mainWindow?.contentView?.bounds.size ?? NSScreen.main?.frame.size ?? .zero ++ return Dimensions(width: size.width, height: size.height) ++ } ++ } ++ ++ func getOrientation() -> Orientation { ++ let screen = getScreenDimensions() ++ return screen.width > screen.height ? .landscape : .portrait ++ } ++ ++ func getInsets() -> Insets { Insets(top: 0, bottom: 0, left: 0, right: 0, ime: 0) } ++ ++ func getPrefersRtlDirection() -> Bool { ++ onMain { UserDefaults.standard.bool(forKey: "RCTI18nUtil_forceRTL") || NSApplication.shared.userInterfaceLayoutDirection == .rightToLeft } ++ } ++ ++ func getStatusBarDimensions() -> Dimensions { Dimensions(width: 0, height: 0) } ++ ++ func getNavigationBarDimensions() -> Dimensions { Dimensions(width: 0, height: 0) } ++ ++ func getPixelRatio() -> Double { ++ onMain { Double(mainWindow?.backingScaleFactor ?? NSScreen.main?.backingScaleFactor ?? 1) } ++ } ++ ++ func setRootViewBackgroundColor(color: Double) throws { ++ let value = Int(color) ++ DispatchQueue.main.async { ++ self.mainWindow?.backgroundColor = NSColor(red: CGFloat((value >> 16) & 0xFF) / 255, green: CGFloat((value >> 8) & 0xFF) / 255, blue: CGFloat(value & 0xFF) / 255, alpha: 1) ++ } ++ } ++ ++ func setStatusBarHidden(isHidden: Bool) throws {} ++ ++ func setNavigationBarHidden(isHidden: Bool) throws {} ++ ++ func setImmersiveMode(isEnabled: Bool) throws {} ++ ++ func registerPlatformListener(callback: @escaping (Array<UnistyleDependency>, UnistylesNativeMiniRuntime) -> Void) throws { ++ dependencyListeners.append(callback) ++ } ++ ++ // Desktop windows have no software keyboard, so the IME inset never changes. ++ func registerImeListener(callback: @escaping (UnistylesNativeMiniRuntime) -> Void) throws {} ++ ++ func unregisterPlatformListeners() throws { ++ dependencyListeners.removeAll() ++ } ++ ++ func onNativePlatformChange() { ++ guard let current = miniRuntime else { return } ++ let next = buildMiniRuntime() ++ let changed = UnistylesNativeMiniRuntime.diff(lhs: current, rhs: next) ++ if !changed.isEmpty { ++ miniRuntime = next ++ dependencyListeners.forEach { $0(changed, next) } ++ } ++ } ++} ++ ++#endif +diff --git a/ios/NativePlatform.swift b/ios/NativePlatform.swift +index d8b86a1..90288a2 100644 +--- a/ios/NativePlatform.swift ++++ b/ios/NativePlatform.swift +@@ -8,6 +8,8 @@ public class NativePlatform { + public static func create() -> HybridNativePlatformSpec_cxx { + #if os(iOS) + let nativePlatform = NativeIOSPlatform() ++ #elseif os(macOS) ++ let nativePlatform = NativeMacOSPlatform() + #else + throw PlatformError.UnsupportedPlatform + #endif +diff --git a/ios/NativePlatformListener+ios.swift b/ios/NativePlatformListener+ios.swift +index ac7db8f..8968117 100644 +--- a/ios/NativePlatformListener+ios.swift ++++ b/ios/NativePlatformListener+ios.swift +@@ -1,3 +1,4 @@ ++#if os(iOS) + import Foundation + import Combine + +@@ -65,3 +66,5 @@ extension NativeIOSPlatform { + } + } + } ++ ++#endif diff --git a/patches/react-native-worklets-core@1.6.3.patch b/patches/react-native-worklets-core@1.6.3.patch new file mode 100644 index 000000000..42b7a018f --- /dev/null +++ b/patches/react-native-worklets-core@1.6.3.patch @@ -0,0 +1,13 @@ +diff --git a/react-native-worklets-core.podspec b/react-native-worklets-core.podspec +index f5b58d7..c9bcfea 100644 +--- a/react-native-worklets-core.podspec ++++ b/react-native-worklets-core.podspec +@@ -11,7 +11,7 @@ Pod::Spec.new do |s| + s.license = package["license"] + s.authors = package["author"] + +- s.platforms = { :ios => "11.0" } ++ s.platforms = { :ios => "11.0", :osx => "14.0" } + s.source = { :git => "https://github.com/margelo/react-native-worklets-core.git", :tag => "#{s.version}" } + + s.source_files = "ios/**/*.{h,m,mm}", "cpp/**/*.{h,cpp}" diff --git a/patches/react-navigation-native-stack@7.21.0.patch b/patches/react-navigation-native-stack@7.21.0.patch new file mode 100644 index 000000000..6a7a584eb --- /dev/null +++ b/patches/react-navigation-native-stack@7.21.0.patch @@ -0,0 +1,16 @@ +diff --git a/lib/module/views/NativeStackView.macos.js b/lib/module/views/NativeStackView.macos.js +new file mode 100644 +index 0000000..c42b5d6 +--- /dev/null ++++ b/lib/module/views/NativeStackView.macos.js +@@ -0,0 +1,2 @@ ++// macOS has no native stack: use the JavaScript stack view, which draws headers with @react-navigation/elements. ++export * from "./NativeStackView.js"; +diff --git a/src/views/NativeStackView.macos.tsx b/src/views/NativeStackView.macos.tsx +new file mode 100644 +index 0000000..82511c8 +--- /dev/null ++++ b/src/views/NativeStackView.macos.tsx +@@ -0,0 +1,2 @@ ++// macOS has no native stack: use the JavaScript stack view, which draws headers with @react-navigation/elements. ++export * from './NativeStackView.tsx'; diff --git a/patches/sentry-react-native@8.32.0.patch b/patches/sentry-react-native@8.32.0.patch new file mode 100644 index 000000000..9c11feead --- /dev/null +++ b/patches/sentry-react-native@8.32.0.patch @@ -0,0 +1,76 @@ +diff --git a/ios/Replay/RNSentryReplayMask.h b/ios/Replay/RNSentryReplayMask.h +index 3903d41..61dd169 100644 +--- a/ios/Replay/RNSentryReplayMask.h ++++ b/ios/Replay/RNSentryReplayMask.h +@@ -1,6 +1,8 @@ + #import <Sentry/SentryDefines.h> + +-#if SENTRY_HAS_UIKIT ++// macOS has no replay, but codegen's component provider still names this view: without the class, its ++// registry gets a nil entry and the app aborts at launch. On macOS it is a plain container. ++#if SENTRY_HAS_UIKIT || TARGET_OS_OSX + + # import <React/RCTViewManager.h> + +diff --git a/ios/Replay/RNSentryReplayMask.mm b/ios/Replay/RNSentryReplayMask.mm +index 3dad47a..de9bd40 100644 +--- a/ios/Replay/RNSentryReplayMask.mm ++++ b/ios/Replay/RNSentryReplayMask.mm +@@ -1,6 +1,6 @@ + #import <Sentry/SentryDefines.h> + +-#if SENTRY_HAS_UIKIT ++#if SENTRY_HAS_UIKIT || TARGET_OS_OSX + + # import "RNSentryReplayMask.h" + +@@ -16,7 +16,11 @@ @implementation RNSentryReplayMaskManager + + RCT_EXPORT_MODULE(RNSentryReplayMask) + ++# if TARGET_OS_OSX ++- (RCTPlatformView *)view ++# else + - (UIView *)view ++# endif + { + return [RNSentryReplayMask new]; + } +diff --git a/ios/Replay/RNSentryReplayUnmask.h b/ios/Replay/RNSentryReplayUnmask.h +index c1dc8a3..d28497e 100644 +--- a/ios/Replay/RNSentryReplayUnmask.h ++++ b/ios/Replay/RNSentryReplayUnmask.h +@@ -1,6 +1,8 @@ + #import <Sentry/SentryDefines.h> + +-#if SENTRY_HAS_UIKIT ++// macOS has no replay, but codegen's component provider still names this view: without the class, its ++// registry gets a nil entry and the app aborts at launch. On macOS it is a plain container. ++#if SENTRY_HAS_UIKIT || TARGET_OS_OSX + + # import <React/RCTViewManager.h> + +diff --git a/ios/Replay/RNSentryReplayUnmask.mm b/ios/Replay/RNSentryReplayUnmask.mm +index 0fc0248..c445a78 100644 +--- a/ios/Replay/RNSentryReplayUnmask.mm ++++ b/ios/Replay/RNSentryReplayUnmask.mm +@@ -1,6 +1,6 @@ + #import <Sentry/SentryDefines.h> + +-#if SENTRY_HAS_UIKIT ++#if SENTRY_HAS_UIKIT || TARGET_OS_OSX + + # import "RNSentryReplayUnmask.h" + +@@ -16,7 +16,11 @@ @implementation RNSentryReplayUnmaskManager + + RCT_EXPORT_MODULE(RNSentryReplayUnmask) + ++# if TARGET_OS_OSX ++- (RCTPlatformView *)view ++# else + - (UIView *)view ++# endif + { + return [RNSentryReplayUnmask new]; + } diff --git a/patches/workspace/expo-audio@1.1.1.patch b/patches/workspace/expo-audio@55.0.18.patch similarity index 62% rename from patches/workspace/expo-audio@1.1.1.patch rename to patches/workspace/expo-audio@55.0.18.patch index 8bc4c57ab..0829f36c7 100644 --- a/patches/workspace/expo-audio@1.1.1.patch +++ b/patches/workspace/expo-audio@55.0.18.patch @@ -1,22 +1,24 @@ --- a/android/src/main/java/expo/modules/audio/service/AudioControlsService.kt +++ b/android/src/main/java/expo/modules/audio/service/AudioControlsService.kt -@@ -31,6 +31,7 @@ +@@ -30,8 +30,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers + import kotlinx.coroutines.Job import kotlinx.coroutines.cancel +-import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + import java.lang.ref.WeakReference import java.net.URL - @OptIn(UnstableApi::class) -@@ -43,6 +44,7 @@ - private val scope = CoroutineScope(Dispatchers.IO) +@@ -46,6 +46,7 @@ class AudioControlsService : MediaSessionService() { private var currentArtworkUrl: URL? = null private var currentArtwork: Bitmap? = null + private var artworkLoadJob: Job? = null + private var artworkRequest = 0 private val notificationId: Int get() = currentPlayer?.hashCode() ?: CHANNEL_ID.hashCode() -@@ -207,16 +209,11 @@ +@@ -281,16 +282,11 @@ class AudioControlsService : MediaSessionService() { currentPlayer?.isActiveForLockScreen = false hideNotification() @@ -34,15 +36,15 @@ player?.isActiveForLockScreen = true if (player != null) { -@@ -228,6 +225,7 @@ +@@ -308,6 +304,7 @@ class AudioControlsService : MediaSessionService() { - addSession(session) - mediaSession = session -+ updateArtwork(metadata?.artworkUrl) + addSession(session) + mediaSession = session ++ updateArtwork(metadata?.artworkUrl) - // Set initial custom layout - updateSessionCustomLayout(player.ref.isPlaying) -@@ -259,12 +257,7 @@ + updateSessionCustomLayout(player.ref.isPlaying) + +@@ -328,12 +325,7 @@ class AudioControlsService : MediaSessionService() { return } currentMetadata = metadata @@ -56,51 +58,76 @@ } private fun clearSessionInternal() { -@@ -275,6 +268,7 @@ - playbackListener = null +@@ -341,6 +333,7 @@ class AudioControlsService : MediaSessionService() { + removePlayerListener() currentPlayer = null currentMetadata = null + clearArtwork() mediaSession?.release() mediaSession = null - stopForeground(STOP_FOREGROUND_REMOVE) -@@ -298,19 +292,36 @@ - return super.onBind(intent) ?: binder + currentPlayer?.assignBasicMediaSession() +@@ -369,16 +362,8 @@ class AudioControlsService : MediaSessionService() { + currentMetadata = metadata + currentOptions = options + +- // Reload artwork if metadata has changed +- metadata?.artworkUrl?.let { +- loadArtworkFromUrl(it) { bitmap -> +- currentArtwork = bitmap +- postOrStartForegroundNotification(startInForeground = false) +- } +- } +- + updateSessionCustomLayout(player.ref.isPlaying) +- postOrStartForegroundNotification(startInForeground = false) ++ updateArtwork(metadata?.artworkUrl) + } else { + setActivePlayerInternal(player, metadata, options) + } +@@ -388,26 +373,38 @@ class AudioControlsService : MediaSessionService() { + clearSessionInternal() } - private fun loadArtworkFromUrl(url: URL, callback: (Bitmap?) -> Unit) { + private fun clearArtwork() { + artworkRequest++ ++ artworkLoadJob?.cancel() ++ artworkLoadJob = null + currentArtworkUrl = null + currentArtwork = null + } + + private fun updateArtwork(url: URL?) { if (url != currentArtworkUrl) { ++ clearArtwork() currentArtworkUrl = url -- scope.launch { +- artworkLoadJob?.cancel() +- +- artworkLoadJob = scope.launch { - try { - val inputStream = url.openConnection().getInputStream() - val bitmap = BitmapFactory.decodeStream(inputStream) -- callback(bitmap) -- } catch (e: Exception) { -- callback(null) -+ currentArtwork = null -+ val request = ++artworkRequest +- +- if (isActive) { +- callback(bitmap) ++ val request = artworkRequest + if (url != null) { -+ scope.launch { ++ artworkLoadJob = scope.launch { + val bitmap = try { + url.openConnection().getInputStream().use { BitmapFactory.decodeStream(it) } + } catch (e: Exception) { + null -+ } + } +- } catch (e: Exception) { +- if (isActive) { +- callback(null) + withContext(Dispatchers.Main) { + // A replaced/cleared session must never receive an earlier download. + if (request == artworkRequest && currentPlayer != null) { + currentArtwork = bitmap + postOrStartForegroundNotification(startInForeground = false) + } -+ } + } } } } @@ -109,14 +136,16 @@ } private fun hideNotification() { -@@ -322,6 +333,7 @@ +@@ -419,8 +416,7 @@ class AudioControlsService : MediaSessionService() { override fun onDestroy() { super.onDestroy() - instance = null + +- artworkLoadJob?.cancel() +- artworkLoadJob = null + clearArtwork() - try { - scope.cancel() - } catch (e: Exception) { + scope.cancel() + mediaSession?.release() + mediaSession = null --- a/package.json +++ b/package.json @@ -11,8 +11,6 @@ diff --git a/patches/workspace/upstream.json b/patches/workspace/upstream.json index d4f9e1ba9..8d59f7348 100644 --- a/patches/workspace/upstream.json +++ b/patches/workspace/upstream.json @@ -21,8 +21,8 @@ "integrity": "sha512-of6A8BXTnIziF/+CaDQmIY0saC+5TpegJhs4CCkRXIhspWluSt3AcI/hYBwgHimJxH8BIsXpQ6JJT0EFZfq25g==" }, "expo-audio": { - "version": "1.1.1", - "url": "https://registry.npmjs.org/expo-audio/-/expo-audio-1.1.1.tgz", - "integrity": "sha512-CPCpJ+0AEHdzWROc0f00Zh6e+irLSl2ALos/LPvxEeIcJw1APfBa4DuHPkL4CQCWsVe7EnUjFpdwpqsEUWcP0g==" + "version": "55.0.18", + "url": "https://registry.npmjs.org/expo-audio/-/expo-audio-55.0.18.tgz", + "integrity": "sha512-izDr80hhHXkfIZn0TXdCheUhWEVRV205a6j0gdhgJ9se36C9Yo+REyaqFrEI1La4p95vPXvaH7UkAx/EGRKNHQ==" } } diff --git a/scripts/e2e/app-state.ts b/scripts/e2e/app-state.ts index 4bcfd715f..5824282d7 100644 --- a/scripts/e2e/app-state.ts +++ b/scripts/e2e/app-state.ts @@ -84,7 +84,7 @@ function macHost(): StateHost { deleteDefaults(domain) { const result = text("defaults", ["delete", domain]); if (result.status === 0) return true; - if (result.stderr.includes(`Domain '${domain}' not found`)) return false; + if (result.stderr.includes(`Domain '${domain}' not found`) || result.stderr.includes(`Domain (${domain}) not found.`)) return false; throw new Error(`defaults delete ${domain} failed (${result.status}): ${result.stderr.trim()}`); }, }; diff --git a/scripts/e2e/cli.ts b/scripts/e2e/cli.ts index e0b28fef1..cdc3064c8 100644 --- a/scripts/e2e/cli.ts +++ b/scripts/e2e/cli.ts @@ -58,6 +58,6 @@ if (mode === "schema") { ? await gateCommand(args, path.join(repo, "e2e"), { ...hostEnv(), signal: controller.signal }) : await runCommand(args, { backends: BACKENDS, stdout: line => console.log(line), stderr: line => console.error(line), signal: controller.signal }); } else { - console.error("usage: bun run e2e:validate [paths…] | bun run e2e:lint [paths…] | bun run e2e:coverage [paths…] [--json] | bun run e2e:schema | bun run e2e:run [flows…] [--backend …] [--agent] [--report dir] [--app path] | bun run gate [--target id] [--dry-run] | bun run e2e:golden approve <path> [--to <golden id>]"); + console.error("usage: bun run e2e:validate [paths…] | bun run e2e:lint [paths…] | bun run e2e:coverage [paths…] [--json] | bun run e2e:schema | bun run e2e:run [flows…] [--backend …] [--pointer] [--agent] [--report dir] [--app path] | bun run gate [--target id] [--dry-run] | bun run e2e:golden approve <path> [--to <golden id>]"); process.exitCode = 2; } diff --git a/scripts/e2e/driver.ts b/scripts/e2e/driver.ts index c9d86be5b..c124c8865 100644 --- a/scripts/e2e/driver.ts +++ b/scripts/e2e/driver.ts @@ -9,6 +9,7 @@ import { spawnProcess, type ManagedProcess, type ProcessLog } from "../../packag export type DriverWindow = "main" | "key" | `id:${string}` | `title:${string}`; export type Frame = { x: number; y: number; width: number; height: number }; export type Capture = { file: string; width: number; height: number; scale: number }; +export type PointerEvents = { pid: number; down: string; up: string }; export type LaunchOptions = { executable: string; args?: string[]; env?: Record<string, string>; log?: ProcessLog; startTimeoutMs?: number }; type Reply = { id?: unknown; ok: boolean; error?: { code: string; message: string }; [key: string]: unknown }; @@ -111,6 +112,12 @@ export class Driver { } async ping() { await this.request({ command: "ping" }, 10000); } + /** Shows a window and encodes OS mouse events at captured-frame points once mounted UI updates settle; a zero timeout does not wait. */ + async pointerEvents(window: DriverWindow, point: { x: number; y: number }, timeoutMs = 5000): Promise<PointerEvents> { + const reply = await this.request({ command: "pointerEvents", window, ...point, timeoutMs }, timeoutMs + 10000); + return { pid: reply.pid as number, down: reply.down as string, up: reply.up as string }; + } + async restorePointer() { await this.request({ command: "restorePointer" }, 10000); } /** Delivers a deep link through the app's URL handling once its first render has settled. */ async navigate(url: string, timeoutMs = 30000) { await this.request({ command: "navigate", url, timeoutMs }, timeoutMs + 10000); } /** Opens an existing file (absolute path) through the app's open-file handling once its first render has settled. */ diff --git a/scripts/e2e/kitchen-sink.ts b/scripts/e2e/kitchen-sink.ts index 726570979..aead9a468 100644 --- a/scripts/e2e/kitchen-sink.ts +++ b/scripts/e2e/kitchen-sink.ts @@ -1,7 +1,7 @@ // The Kitchen Sink under test, launched in driver mode (docs/test-driver.md). Shared by `ks:capture` and the // runner's driver backend: which app to launch, the JavaScript a dev build needs, and the launch itself. import { execFileSync, spawnSync } from "node:child_process"; -import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs"; import { createServer, type Server } from "node:http"; import type { AddressInfo } from "node:net"; import os from "node:os"; @@ -37,6 +37,8 @@ type Resolved = { app: string; executable: string; embedded: boolean } & AppIden export class KitchenSinkApp { private resolved?: Resolved; private bundle?: Buffer; + /** Asset files by URL path (`/assets/…`), as a dev server serves fonts and images. */ + private assets = new Map<string, Buffer>(); private server?: Server; /** `app` is a `.app` bundle; without it, the last dev build (`cd examples/kitchen-sink && bun run rebuild:macos`). It is resolved by `open()`. */ @@ -70,18 +72,27 @@ export class KitchenSinkApp { const file = path.join(work, "index.bundle"); prepareConfig(KITCHEN_SINK); await run(KITCHEN_SINK, [binary(KITCHEN_SINK, "expo"), "export:embed", "--platform", "macos", "--entry-file", path.resolve(KITCHEN_SINK, entryFile(KITCHEN_SINK)), - "--bundle-output", file, "--dev", "false", "--minify", "false", "--max-workers", "2"], { env: { CI: "1" }, capture: !label, label }); + "--bundle-output", file, "--assets-dest", work, "--dev", "false", "--minify", "false", "--max-workers", "2"], { env: { CI: "1" }, capture: !label, label }); this.bundle = readFileSync(file); + const assets = path.join(work, "assets"); + if (existsSync(assets)) for (const name of readdirSync(assets, { recursive: true, withFileTypes: true })) + if (name.isFile()) this.assets.set("/" + path.relative(work, path.join(name.parentPath, name.name)).split(path.sep).join("/"), readFileSync(path.join(name.parentPath, name.name))); } finally { rmSync(work, { recursive: true, force: true }); } } const bundle = this.bundle; const server = createServer((request, response) => { - if (new URL(request.url ?? "/", "http://127.0.0.1").pathname === "/index.bundle") { + const pathname = new URL(request.url ?? "/", "http://127.0.0.1").pathname; + if (pathname === "/index.bundle") { response.writeHead(200, { "Content-Type": "application/javascript" }).end(bundle); return; } + const asset = this.assets.get(decodeURIComponent(pathname)); + if (asset) { + response.writeHead(200).end(asset); + return; + } console.error(`Kitchen Sink: the app requested ${request.url}, which is not served`); response.writeHead(404).end(); }); diff --git a/scripts/e2e/macos-pointer.m b/scripts/e2e/macos-pointer.m new file mode 100644 index 000000000..184ee1f52 --- /dev/null +++ b/scripts/e2e/macos-pointer.m @@ -0,0 +1,41 @@ +#import <ApplicationServices/ApplicationServices.h> +#import <Foundation/Foundation.h> +#include <unistd.h> +#include <errno.h> +#include <limits.h> + +int main(int argc, const char **argv) { @autoreleasepool { + BOOL check = argc == 2 && strcmp(argv[1], "check") == 0; + if (!check && argc != 4) { + fprintf(stderr, "usage: macos-pointer check | <pid> <base64-down> <base64-up>\n"); + return 64; + } + char *end = NULL; + errno = 0; + long value = check ? 0 : strtol(argv[1], &end, 10); + if (!check && (errno || *end || value <= 0 || value > INT_MAX)) { + fprintf(stderr, "E_INVALID_ARGUMENT: Expected a positive process ID\n"); + return 64; + } + if (!CGPreflightPostEventAccess()) { + fprintf(stderr, "E_PERMISSION_DENIED: Real pointer input needs Accessibility permission for the e2e runner in System Settings > Privacy & Security > Accessibility. No permission prompt was opened.\n"); + return 77; + } + if (check) return 0; + pid_t pid = (pid_t)value; + NSData *downData = [[NSData alloc] initWithBase64EncodedString:@(argv[2]) options:0]; + NSData *upData = [[NSData alloc] initWithBase64EncodedString:@(argv[3]) options:0]; + CGEventRef down = downData ? CGEventCreateFromData(NULL, (__bridge CFDataRef)downData) : NULL; + CGEventRef up = upData ? CGEventCreateFromData(NULL, (__bridge CFDataRef)upData) : NULL; + if (!down || !up) { + if (down) CFRelease(down); + if (up) CFRelease(up); + fprintf(stderr, "E_EVENT: Could not decode mouse events\n"); + return 1; + } + CGEventPostToPid(pid, down); + usleep(50000); + CGEventPostToPid(pid, up); + CFRelease(down); CFRelease(up); + return 0; +} } diff --git a/scripts/e2e/runner/backend.ts b/scripts/e2e/runner/backend.ts index 0a9a58ffc..cb4ea3734 100644 --- a/scripts/e2e/runner/backend.ts +++ b/scripts/e2e/runner/backend.ts @@ -42,8 +42,8 @@ export type CommandContext = { outputDir: string; /** Waits until a selector matches exactly one element of this backend, then returns it. */ resolve(selector: unknown): Promise<Target>; - /** `resolve` for the command's own selector keys (tapOn, assertFocused, …). */ - target(): Promise<Target>; + /** `resolve` for the command's own selector keys (tapOn, assertFocused, …), waiting up to `timeoutMs` (default: the step's). */ + target(timeoutMs?: number): Promise<Target>; /** Waits until a window selector (`key`, `main`, `{ title, index, type, id }`) matches exactly one window of this backend. */ window(selector: unknown): Promise<WindowInfo>; /** Runs `check` until it stops throwing a retryable Failure ("assertion", "not-found", "ambiguous"), or the timeout passes. Never use it for actions. */ diff --git a/scripts/e2e/runner/driver-backend.ts b/scripts/e2e/runner/driver-backend.ts index 78fec7904..1b1e5eb54 100644 --- a/scripts/e2e/runner/driver-backend.ts +++ b/scripts/e2e/runner/driver-backend.ts @@ -4,12 +4,14 @@ import { copyFileSync, existsSync, writeFileSync } from "node:fs"; import path from "node:path"; import { processLog, type ProcessLog } from "../../../packages/cli/src/process.ts"; -import { DriverError, type Capture as DriverCapture, type DriverWindow, type Frame } from "../driver.ts"; +import { DriverError, type Capture as DriverCapture, type DriverWindow, type Frame, type PointerEvents } from "../driver.ts"; import { Failure, type Artifact, type Atom, type AtomKey, type Backend, type Capture, type CommandHandler, type Element, type QueryScope, type Session } from "./backend.ts"; /** What the backend uses of a driver session (`Driver` in scripts/e2e/driver.ts). Tests pass a double. */ export type DriverClient = { readonly running: boolean; + restorePointer(): Promise<void>; + pointerEvents(window: DriverWindow, point: { x: number; y: number }, timeoutMs?: number): Promise<PointerEvents>; navigate(url: string): Promise<void>; openFile(file: string): Promise<void>; waitFor(testID: string, timeoutMs?: number): Promise<{ window: string; frame: Frame }>; @@ -54,6 +56,11 @@ export class DriverBackend implements Backend { constructor(private readonly app: DriverApp) {} + /** Used only by the opt-in OS pointer backend; ordinary driver runs stay hidden. */ + pointerEvents(window: DriverWindow, point: { x: number; y: number }, timeoutMs?: number) { return this.client().pointerEvents(window, point, timeoutMs); } + + async restorePointer() { if (this.driver?.running) await this.driver.restorePointer(); } + async start(session: Session) { this.session = session; this.logs = []; diff --git a/scripts/e2e/runner/executor.ts b/scripts/e2e/runner/executor.ts index b48e3f73d..507fe9518 100644 --- a/scripts/e2e/runner/executor.ts +++ b/scripts/e2e/runner/executor.ts @@ -250,7 +250,7 @@ async function executeFlow(flow: FlowAst, absolute: string, matrix: Matrix, base const context: CommandContext = { command: { ...command, args }, timeoutMs, platform, matrix, outputDir, resolve: async value => (await find(backend, value, "one", timeoutMs))!, - target: () => context.resolve(selectorArgs(args)), + target: async (wait = timeoutMs) => (await find(backend, selectorArgs(args), "one", wait))!, window: value => eventually(async () => { const windows = await finderFor(backend).windows(), found = pickWindows(parseWindow(value, platform), windows); if (found.length === 1) return found[0]!; diff --git a/scripts/e2e/runner/pointer-backend.ts b/scripts/e2e/runner/pointer-backend.ts new file mode 100644 index 000000000..6a5d2525a --- /dev/null +++ b/scripts/e2e/runner/pointer-backend.ts @@ -0,0 +1,60 @@ +import { execFile } from "node:child_process"; +import path from "node:path"; +import { promisify } from "node:util"; +import { DriverError } from "../driver.ts"; +import { Failure, type Backend, type CommandHandler, type Session } from "./backend.ts"; +import type { DriverBackend } from "./driver-backend.ts"; + +const execute = promisify(execFile); +/** The longest single settle wait, well under the driver's reply limit. */ +const SETTLE_SLICE_MS = 5000; + +/** OS mouse input with in-process testID lookup. Requires Accessibility input-posting access. */ +export class PointerBackend implements Backend { + readonly name = "pointer"; + readonly kind = "black-box"; + readonly platform = "macos"; + readonly matrix: ReadonlySet<string> = new Set(); + readonly queryKeys: ReadonlySet<"id"> = new Set(["id"]); + private helper?: Promise<string>; + readonly commands: Readonly<Record<string, CommandHandler>> = { + tapOn: async (args, context) => { + if ((args.modifiers as string[] | undefined)?.length) throw new Failure("unsupported", "pointer tapOn does not support modifiers"); + // Gesture recognizers attach after their views mount, so a tap waits for mounts to settle. A screen that keeps + // mounting (a spinner) is waited on in slices that re-resolve the target, then tapped once the timeout is spent. + // Every wait shares the step's timeout. + const until = Date.now() + context.timeoutMs; + for (;;) { + const target = await context.target(Math.max(0, until - Date.now())), settleMs = Math.max(0, Math.min(until - Date.now(), SETTLE_SLICE_MS)); + try { + const { pid, down, up } = await this.driver.pointerEvents(`id:${target.element.window}`, target.point, settleMs); + return await this.post([String(pid), down, up]); + } catch (error) { + if (!(error instanceof DriverError && error.code === "E_NOT_SETTLED")) throw error; + } + } + }, + }; + + constructor(private readonly driver: DriverBackend) {} + + async start(session: Session) { + if (process.platform !== "darwin") throw new Failure("unsupported", "pointer input is available on macOS only"); + if (!this.helper) { + const helper = path.join(session.dir, "macos-pointer"); + this.helper = execute("clang", ["-fobjc-arc", "-framework", "Foundation", "-framework", "ApplicationServices", + path.resolve(import.meta.dirname, "../macos-pointer.m"), "-o", helper], { timeout: 60000 }).then(() => helper); + } + await this.post(["check"]); + } + async stop() { await this.driver.restorePointer(); } + query: DriverBackend["query"] = (atom, scope) => this.driver.query(atom, scope); + + private async post(args: string[]) { + try { await execute((await this.helper)!, args, { timeout: 10000 }); } + catch (error) { + const failure = error as Error & { stderr?: string }; + throw new Failure("error", failure.stderr?.trim() || failure.message); + } + } +} diff --git a/scripts/e2e/runner/run.ts b/scripts/e2e/runner/run.ts index 4bfc436d9..7ac748790 100644 --- a/scripts/e2e/runner/run.ts +++ b/scripts/e2e/runner/run.ts @@ -5,6 +5,7 @@ import { collect } from "../files.ts"; import { KitchenSinkApp } from "../kitchen-sink.ts"; import type { Artifact, Backend, BackendFactory } from "./backend.ts"; import { DriverBackend } from "./driver-backend.ts"; +import { PointerBackend } from "./pointer-backend.ts"; import { realClock, runFlows, type Clock, type FailureReport, type FlowResult, type RunEvent, type RunOptions } from "./executor.ts"; import { buildReport, failureText, writeReports, type RunReport } from "./report.ts"; @@ -12,7 +13,7 @@ const repo = path.resolve(import.meta.dirname, "../../.."); /** * Production backends by `--backend` name. `driver` is the Spark in-app driver (docs/e2e-flows.md#the-driver-backend); - * the macOS black-box backend (AX + CGEvent) lands with #50. + * `--pointer` attaches OS mouse input to that driver's session; a full AX backend is separate (#50). */ export const BACKENDS: Readonly<Record<string, BackendFactory>> = { driver: ({ app }) => new DriverBackend(new KitchenSinkApp(app)), @@ -20,10 +21,10 @@ export const BACKENDS: Readonly<Record<string, BackendFactory>> = { export const NO_BACKENDS = "no backends are registered"; export type RunIO = { backends: Readonly<Record<string, BackendFactory>>; stdout(line: string): void; stderr(line: string): void; clock?: Clock; signal?: AbortSignal }; -const USAGE = "usage: bun run e2e:run [flow|dir…] [--backend name[,name…]] [--agent] [--report dir] [--app path]"; +const USAGE = "usage: bun run e2e:run [flow|dir…] [--backend name[,name…]] [--pointer] [--agent] [--report dir] [--app path]"; function parse(args: string[]) { - const options = { targets: [] as string[], backends: [] as string[], agent: false, report: undefined as string | undefined, app: undefined as string | undefined }; + const options = { targets: [] as string[], backends: [] as string[], pointer: false, agent: false, report: undefined as string | undefined, app: undefined as string | undefined }; for (let index = 0; index < args.length; index++) { const [flag, inline] = args[index]!.startsWith("--") ? args[index]!.split(/=(.*)/s, 2) as [string, string | undefined] : [undefined, undefined]; const value = () => { @@ -33,6 +34,7 @@ function parse(args: string[]) { }; if (flag === undefined) options.targets.push(args[index]!); else if (flag === "--agent" && inline === undefined) options.agent = true; + else if (flag === "--pointer" && inline === undefined) options.pointer = true; else if (flag === "--backend") options.backends.push(...value().split(",").filter(Boolean)); else if (flag === "--report") options.report = value(); else if (flag === "--app") options.app = value(); @@ -80,6 +82,11 @@ export async function runCommand(args: string[], io: RunIO): Promise<number> { const unknown = names.filter(name => !Object.hasOwn(io.backends, name)); if (unknown.length) { io.stderr(`unknown backend ${unknown.join(", ")} (available: ${Object.keys(io.backends).join(", ") || "none"})`); return 2; } const backends: Backend[] = names.map(name => io.backends[name]!({ app: options.app && path.resolve(options.app) })); + if (options.pointer) { + const driver = backends.find(backend => backend instanceof DriverBackend); + if (!(driver instanceof DriverBackend)) { io.stderr("--pointer requires the driver backend"); return 2; } + backends.push(new PointerBackend(driver)); + } const platforms = new Set(backends.map(backend => backend.platform)); if (platforms.size > 1) { io.stderr(`backends target different platforms: ${[...platforms].join(", ")}`); return 2; } diff --git a/scripts/install-workspace-adapters.ts b/scripts/install-workspace-adapters.ts index d277300dd..c4b608f90 100644 --- a/scripts/install-workspace-adapters.ts +++ b/scripts/install-workspace-adapters.ts @@ -19,10 +19,11 @@ export function installWorkspaceAdapters(root: string) { const file = path.join(root, "node_modules/react-native-windows/react-native.config.js"); if (existsSync(file)) { const source = readFileSync(file, "utf8"); - const guard = 'if (process.platform !== "win32") { module.exports = {}; return; }\n'; + const guard = 'if (process.platform !== "win32") { module.exports = {}; } else {\n'; if (!source.startsWith(guard)) { + const upstream = source.replace(/^if \(process\.platform !== "win32"\) \{ module\.exports = \{\}; return; \}\n/, ""); const temporary = `${file}.spark-${process.pid}.tmp`; - writeFileSync(temporary, guard + source); + writeFileSync(temporary, guard + upstream + "\n}\n"); renameSync(temporary, file); } } diff --git a/scripts/test-add-desktop.ts b/scripts/test-add-desktop.ts index eb71b7941..cb83a5faa 100644 --- a/scripts/test-add-desktop.ts +++ b/scripts/test-add-desktop.ts @@ -14,8 +14,8 @@ if (existsSync(root)) throw new Error("Choose a fresh fixture directory"); mkdirSync(path.join(root, "src"), { recursive: true }); writeJson(path.join(root, "package.json"), { name: "existing-expo", version: "1.0.0", private: true, main: "src/bootstrap.ts", packageManager: `npm@${(await run(framework, ["npm", "--version"], { capture: true })).trim()}`, scripts: { start: "expo start", ios: "expo run:ios", android: "expo run:android", web: "expo start --web", macos: "echo existing script" }, - dependencies: { expo: "54.0.37", react: "19.1.4", "react-native": "0.81.6", "react-dom": "19.1.4", "react-native-web": "0.21.0" }, - devDependencies: { typescript: "5.9.3", "@types/react": "19.1.10" }, + dependencies: { expo: "55.0.32", react: "19.2.0", "react-native": "0.83.10", "react-dom": "19.2.0", "react-native-web": "0.21.0" }, + devDependencies: { typescript: "5.9.3", "@types/react": "19.2.18" }, }); writeJson(path.join(root, "app.json"), { expo: { name: "ExistingApp", slug: "existing-app", version: "1.2.3", platforms: ["ios", "android", "web"], ios: { bundleIdentifier: "org.example.existing" }, android: { package: "org.example.existing" }, extra: { original: true } } }); writeFileSync(path.join(root, "app.config.ts"), 'import type { ConfigContext } from "expo/config";\nexport default ({ config }: ConfigContext) => ({ ...config, extra: { ...config.extra, environment: process.env.APP_ENV ?? "local" }, plugins: ["./plugin.cjs"] });\n'); diff --git a/scripts/test-api-adapters.ts b/scripts/test-api-adapters.ts index e7af4ac78..1bc47f589 100644 --- a/scripts/test-api-adapters.ts +++ b/scripts/test-api-adapters.ts @@ -62,7 +62,7 @@ try { const executable = path.join(application, "Contents/MacOS", executableName); writeJson(path.join(root, ".spark/session.json"), { compatible: true, target: "test", port }); const log = processLog(path.join(directory, "metro.log")); - metro = spawnProcess([binary(root, "expo"), "start", "--localhost", "--port", String(port), "--max-workers", "2"], { cwd: root, env: { ...process.env, CI: "1" }, stdout: log, stderr: log }); + metro = spawnProcess([binary(root, "expo"), "start", "--localhost", "--port", String(port), "--max-workers", "2"], { cwd: root, env: { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ""} --dns-result-order=ipv4first`, CI: "1" }, stdout: log, stderr: log }); await waitFor(async () => fetch(`http://127.0.0.1:${port}/status`, { signal: AbortSignal.timeout(1000) }).then(r => r.ok ? true : undefined, () => undefined), "Metro", 60000); const bundleURL = `http://127.0.0.1:${port}/index.bundle?platform=macos&dev=true&minify=false`; const summary: Record<string, unknown> = {}; diff --git a/scripts/test-api-platforms.ts b/scripts/test-api-platforms.ts index d2e86ef81..63d4bc73f 100644 --- a/scripts/test-api-platforms.ts +++ b/scripts/test-api-platforms.ts @@ -16,8 +16,8 @@ const archives = { "@legendapp/spark": localArchive(path.join(framework, "artifa mkdirSync(root, { recursive: true }); writeFileSync(path.join(root, ".spark-api-probe"), "managed\n"); writeJson(path.join(root, "package.json"), { name: "spark-api-platform-probe", private: true, version: "1.0.0", main: "index.ts", - dependencies: { ...archives, "@expo/ui": "0.2.0-beta.9", expo: "54.0.37", "expo-clipboard": "8.0.8", "expo-secure-store": "15.0.8", "expo-linking": "8.0.12", react: "19.1.4", "react-native": "0.81.6", "react-dom": "19.1.4", "react-native-web": "0.21.0" }, - overrides: { ...archives, expo: "54.0.37", "expo-constants": "18.0.14", "expo-modules-core": "3.0.30" }, + dependencies: { ...archives, "@expo/ui": "55.0.17", expo: "55.0.32", "expo-clipboard": "55.0.17", "expo-secure-store": "55.0.18", "expo-linking": "55.0.17", react: "19.2.0", "react-native": "0.83.10", "react-dom": "19.2.0", "react-native-web": "0.21.0" }, + overrides: { ...archives, expo: "55.0.32", "expo-constants": "55.0.17", "expo-modules-core": "55.0.27" }, }); writeJson(path.join(root, "app.json"), { expo: { name: "API Platform Probe", slug: "spark-api-probe", platforms: ["ios", "android", "web"] } }); writeFileSync(path.join(root, "metro.config.js"), 'const {getDefaultConfig} = require("expo/metro-config"); module.exports = getDefaultConfig(__dirname);\n'); diff --git a/scripts/test-audio.ts b/scripts/test-audio.ts index a5729e4db..3b07d899c 100644 --- a/scripts/test-audio.ts +++ b/scripts/test-audio.ts @@ -28,7 +28,7 @@ rmSync(appPath, { recursive: true, force: true }); cpSync(source, appPath, { rec const port = await availablePort(); const report = path.join(directory, "report.json"); rmSync(report, { force: true }); const metroLog = processLog(path.join(directory, "metro.log")); -const metro = spawnProcess([binary(root, "expo"), "start", "--localhost", "--port", String(port), "--max-workers", "2"], { cwd: root, env: { ...process.env, CI: "1", SPARK_PLATFORM: "macos" }, stdout: metroLog, stderr: metroLog }); +const metro = spawnProcess([binary(root, "expo"), "start", "--localhost", "--port", String(port), "--max-workers", "2"], { cwd: root, env: { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ""} --dns-result-order=ipv4first`, CI: "1", SPARK_PLATFORM: "macos" }, stdout: metroLog, stderr: metroLog }); let app: ReturnType<typeof spawnProcess> | undefined; try { const deadline = Date.now() + 60000; diff --git a/scripts/test-auth.ts b/scripts/test-auth.ts index 9fe56bdd9..cb5fd79be 100644 --- a/scripts/test-auth.ts +++ b/scripts/test-auth.ts @@ -25,7 +25,7 @@ rmSync(appPath, { recursive: true, force: true }); cpSync(source, appPath, { rec const port = await availablePort(); const report = path.join(directory, "report.json"); rmSync(report, { force: true }); const metroLog = processLog(path.join(directory, "metro.log")); -const metro = spawnProcess([binary(root, "expo"), "start", "--localhost", "--port", String(port), "--max-workers", "2"], { cwd: root, env: { ...process.env, CI: "1", SPARK_PLATFORM: "macos" }, stdout: metroLog, stderr: metroLog }); +const metro = spawnProcess([binary(root, "expo"), "start", "--localhost", "--port", String(port), "--max-workers", "2"], { cwd: root, env: { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ""} --dns-result-order=ipv4first`, CI: "1", SPARK_PLATFORM: "macos" }, stdout: metroLog, stderr: metroLog }); const provider = await serveTestHTTP({ hostname: "127.0.0.1", port: 0, fetch(request) { const input = new URL(request.url), redirect = new URL(input.searchParams.get("redirect_uri")!); redirect.searchParams.set("state", input.searchParams.get("state")!); redirect.searchParams.set("code", "local-test"); diff --git a/scripts/test-camera.ts b/scripts/test-camera.ts index 9086c3ee2..1109b1a90 100644 --- a/scripts/test-camera.ts +++ b/scripts/test-camera.ts @@ -39,7 +39,7 @@ try { if (mode === "dev") { writeJson(path.join(root, ".spark/session.json"), { compatible: true, target: "camera-proof", port }); const log = processLog(path.join(directory, "metro.log")); - metro = spawnProcess([binary(root, "expo"), "start", "--localhost", "--port", String(port), "--max-workers", "2"], { cwd: root, env: { ...process.env, CI: "1" }, stdout: log, stderr: log }); + metro = spawnProcess([binary(root, "expo"), "start", "--localhost", "--port", String(port), "--max-workers", "2"], { cwd: root, env: { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ""} --dns-result-order=ipv4first`, CI: "1" }, stdout: log, stderr: log }); const deadline = Date.now() + 120000; let ready = false; while (!ready && Date.now() < deadline) { ready = await fetch(`http://127.0.0.1:${port}/status`, { signal: AbortSignal.timeout(1000) }).then(r => r.ok, () => false); diff --git a/scripts/test-customize-app.ts b/scripts/test-customize-app.ts index 4a08e6e64..1b0b6f003 100644 --- a/scripts/test-customize-app.ts +++ b/scripts/test-customize-app.ts @@ -30,7 +30,7 @@ if (args.includes('--help')) { // Preserve generated package/config fields on reruns so the native build cache remains usable. if (!existsSync(path.join(root, 'package.json'))) writeJson(path.join(root, 'package.json'), { name: 'spark-customize-demo', version: `${version}.0.0`, private: true, main: 'index.js', - dependencies: { '@legendapp/spark': VERSION, expo: '54.0.37', react: '19.1.4', 'react-native': '0.81.6', 'react-native-macos': '0.81.7', 'expo-desktop-metro-config': '54.81.3' }, + dependencies: { '@legendapp/spark': VERSION, expo: '55.0.32', react: '19.2.0', 'react-native': '0.83.10', 'react-native-macos': '0.83.0', 'expo-desktop-metro-config': '55.83.3' }, }); if (!existsSync(path.join(root, 'desktop.config.json'))) writeJson(path.join(root, 'desktop.config.json'), { name: 'Spark Customize Demo', version: `${version}.0.0`, projectId: 'be3cc70e-63b8-42d1-95c6-4f527c80eb15', platforms: ['macos'], diff --git a/scripts/test-desktop-foundations.ts b/scripts/test-desktop-foundations.ts index 858e79a23..07ca78eaf 100644 --- a/scripts/test-desktop-foundations.ts +++ b/scripts/test-desktop-foundations.ts @@ -24,7 +24,7 @@ rmSync(appPath, { recursive: true, force: true }); cpSync(source, appPath, { rec const port = await availablePort(); const report = path.join(directory, "report.json"); rmSync(report, { force: true }); const metroLog = processLog(path.join(directory, "metro.log")); -const metro = spawnProcess([binary(root, "expo"), "start", "--localhost", "--port", String(port), "--max-workers", "2"], { cwd: root, env: { ...process.env, CI: "1", SPARK_PLATFORM: "macos" }, stdout: metroLog, stderr: metroLog }); +const metro = spawnProcess([binary(root, "expo"), "start", "--localhost", "--port", String(port), "--max-workers", "2"], { cwd: root, env: { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ""} --dns-result-order=ipv4first`, CI: "1", SPARK_PLATFORM: "macos" }, stdout: metroLog, stderr: metroLog }); let app: ReturnType<typeof spawnProcess> | undefined; try { const deadline = Date.now() + 60000; diff --git a/scripts/test-expansion.ts b/scripts/test-expansion.ts index 059e02553..51d76890b 100644 --- a/scripts/test-expansion.ts +++ b/scripts/test-expansion.ts @@ -24,7 +24,7 @@ try { macos: previous.macos, window: { title: "Configured main", size: { width: 930, height: 620 }, minSize: { width: 400, height: 100 }, maxSize: { width: 1400, height: 20000 }, resizable: false, titleBarStyle: "overlay", restoreBounds: false } }); prepareConfig(root); const log = processLog(path.join(directory, "metro.log")); - metro = spawnProcess([binary(root, "expo"), "start", "--localhost", "--port", String(port), "--max-workers", "2"], { cwd: root, env: { ...process.env, CI: "1" }, stdout: log, stderr: log }); + metro = spawnProcess([binary(root, "expo"), "start", "--localhost", "--port", String(port), "--max-workers", "2"], { cwd: root, env: { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ""} --dns-result-order=ipv4first`, CI: "1" }, stdout: log, stderr: log }); let ready = false; for (let attempt = 0; attempt < 120; attempt++) { if (await fetch(`http://127.0.0.1:${port}/status`, { signal: AbortSignal.timeout(1000) }).then(r => r.ok, () => false)) { ready = true; break; } diff --git a/scripts/test-fabric-reload.ts b/scripts/test-fabric-reload.ts index 853b88137..ba520f717 100644 --- a/scripts/test-fabric-reload.ts +++ b/scripts/test-fabric-reload.ts @@ -20,7 +20,7 @@ const report=${JSON.stringify(report)}; function App({windowId}){return windowId&&windowId!=='main'?<Text>Child</Text>:<Checks/>;} function Checks(){useEffect(()=>{const timer=setTimeout(()=>{void(async()=>{try{const n=await files.exists(report+'.count')?Number(await files.readText(report+'.count')):0;for(let i=0;i<10;i++){await windows.openWindow({id:'reload-race',component:'main',props:{windowId:'reload-race'}});await windows.closeWindow('reload-race');}if(n<3){await files.writeText(report+'.count',String(n+1));DevSettings.reload();}else await files.writeText(report,JSON.stringify({passed:true,reloads:n,cycles:40}));}catch(error){await files.writeText(report,JSON.stringify({passed:false,error:String(error)}));}})();},500);return()=>clearTimeout(timer)},[]);return <Text>Fabric reload acceptance</Text>};registerRootComponent(App);`); const port=await availablePort();const log=processLog(path.join(framework,'.spark/fabric-reload-metro.log')); -const metro=spawnProcess(nodeCommand(root,'expo','expo',['start','--localhost','--port',String(port),'--max-workers','2']),{cwd:root,env:{...process.env,CI:'1',SPARK_PLATFORM:'macos'},stdout:log,stderr:log});let app; +const metro=spawnProcess(nodeCommand(root,'expo','expo',['start','--localhost','--port',String(port),'--max-workers','2']),{cwd:root,env:{...process.env,NODE_OPTIONS:`${process.env.NODE_OPTIONS ?? ''} --dns-result-order=ipv4first`,CI:'1',SPARK_PLATFORM:'macos'},stdout:log,stderr:log});let app; try{const end=Date.now()+60000;while(!await fetch(`http://127.0.0.1:${port}/status`).then(r=>r.ok,()=>false)){if(Date.now()>end)throw Error('Metro timeout');await sleep(200);} const output=processLog(path.join(framework,'.spark/fabric-reload-app.log'));app=spawnProcess([executable,'-RCT_jsLocation',`127.0.0.1:${port}`],{cwd:root,env:{...process.env,...projectEnvironment(root),SPARK_BUNDLE_URL:`http://127.0.0.1:${port}/FabricReloadAcceptance.bundle?platform=macos&dev=true&minify=false`},stdout:output,stderr:output});const deadline=Date.now()+90000;while(!existsSync(report)){if(Date.now()>deadline||app.exitCode!==null||app.signalCode!==null)throw Error('Reload probe did not report');await sleep(200);}console.log(readFileSync(report,'utf8'));if(!JSON.parse(readFileSync(report,'utf8')).passed)throw Error('Reload failed'); }finally{app?.kill();if(app)await app.exited;metro.kill();await metro.exited;rmSync(entry,{force:true});} diff --git a/scripts/test-file-streams.ts b/scripts/test-file-streams.ts index f7ef33294..307a7b0ea 100644 --- a/scripts/test-file-streams.ts +++ b/scripts/test-file-streams.ts @@ -17,7 +17,7 @@ rmSync(appPath, { recursive: true, force: true }); cpSync(source, appPath, { rec const port = await availablePort(); const report = path.join(directory, "report.json"); rmSync(report, { force: true }); const metroLog = processLog(path.join(directory, "metro.log")); -const metro = spawnProcess([binary(root, "expo"), "start", "--localhost", "--port", String(port), "--max-workers", "2"], { cwd: root, env: { ...process.env, CI: "1", SPARK_PLATFORM: "macos" }, stdout: metroLog, stderr: metroLog }); +const metro = spawnProcess([binary(root, "expo"), "start", "--localhost", "--port", String(port), "--max-workers", "2"], { cwd: root, env: { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ""} --dns-result-order=ipv4first`, CI: "1", SPARK_PLATFORM: "macos" }, stdout: metroLog, stderr: metroLog }); let app: ReturnType<typeof spawnProcess> | undefined; try { const deadline = Date.now() + 60000; diff --git a/scripts/test-integrations.ts b/scripts/test-integrations.ts index 3ac535038..06b11b4ee 100644 --- a/scripts/test-integrations.ts +++ b/scripts/test-integrations.ts @@ -32,7 +32,7 @@ export default function App() { return <Text>Native desktop integration checks</Text>; }`); const log = processLog(path.join(root, ".spark/integration-metro.log")); - metro = spawnProcess([binary(root, "expo"), "start", "--localhost", "--port", String(port), "--max-workers", "2"], { cwd: root, env: { ...globalThis.process.env, CI: "1" }, stdout: log, stderr: log }); + metro = spawnProcess([binary(root, "expo"), "start", "--localhost", "--port", String(port), "--max-workers", "2"], { cwd: root, env: { ...globalThis.process.env, NODE_OPTIONS: `${globalThis.process.env.NODE_OPTIONS ?? ""} --dns-result-order=ipv4first`, CI: "1" }, stdout: log, stderr: log }); let ready = false; const readyBy = Date.now() + 60000; while (Date.now() < readyBy) { diff --git a/scripts/test-keyboard-events.ts b/scripts/test-keyboard-events.ts index b136db78b..2e48b56c0 100644 --- a/scripts/test-keyboard-events.ts +++ b/scripts/test-keyboard-events.ts @@ -37,7 +37,7 @@ const bundlePath = path.relative(metroRoot, entry).replaceAll(path.sep, "/").rep const port = await availablePort(); const metroLog = processLog(path.join(directory, "metro.log")); const metro = spawnProcess(nodeCommand(root, "expo", "expo", ["start", "--localhost", "--port", String(port), "--max-workers", "2"]), { - cwd: root, env: { ...process.env, CI: "1", SPARK_PLATFORM: "macos" }, stdout: metroLog, stderr: metroLog, + cwd: root, env: { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ""} --dns-result-order=ipv4first`, CI: "1", SPARK_PLATFORM: "macos" }, stdout: metroLog, stderr: metroLog, }); let app: ReturnType<typeof spawnProcess> | undefined; try { diff --git a/scripts/test-native.ts b/scripts/test-native.ts index 0db0aeac6..546fa0e7d 100644 --- a/scripts/test-native.ts +++ b/scripts/test-native.ts @@ -37,7 +37,7 @@ const port = await availablePort(); const reportDir = path.join(root, ".spark/test-results"); mkdirSync(reportDir, { recursive: true }); writeJson(path.join(root, ".spark/session.json"), { compatible: true, target: "test", port }); const metroLog = processLog(path.join(reportDir, "metro.log")); -const startMetro = () => { writeFileSync(path.join(reportDir, "metro.log"), ""); return spawnProcess([binary(root, "expo"), "start", "--localhost", "--port", String(port), "--max-workers", "2"], { cwd: root, env: { ...process.env, CI: "1" }, stdout: metroLog, stderr: metroLog }); }; +const startMetro = () => { writeFileSync(path.join(reportDir, "metro.log"), ""); return spawnProcess([binary(root, "expo"), "start", "--localhost", "--port", String(port), "--max-workers", "2"], { cwd: root, env: { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ""} --dns-result-order=ipv4first`, CI: "1" }, stdout: metroLog, stderr: metroLog }); }; let metro = startMetro(); let appProcess: ReturnType<typeof spawnProcess> | undefined; async function waitFor<T>(read: () => Promise<T | undefined>, timeout: number, description: string): Promise<T> { diff --git a/scripts/test-platforms.ts b/scripts/test-platforms.ts index b068f8dab..ee22e19eb 100644 --- a/scripts/test-platforms.ts +++ b/scripts/test-platforms.ts @@ -126,7 +126,7 @@ try { report.versions = installedVersions(root); if (platform === "macos") { const manifest = readFileSync(path.join(root, "package.json"), "utf8"); - try { await run(root, nodeCommand(root, "expo-desktop", "expo-desktop", ["prebuild", "--platform", "macos", "--template", "expo-desktop-template-bare-minimum@54.81.6", "--no-install"]), { env: { CI: "1" }, capture: true }); } + try { await run(root, nodeCommand(root, "expo-desktop", "expo-desktop", ["prebuild", "--platform", "macos", "--template", "expo-desktop-template-bare-minimum@55.83.10", "--no-install"]), { env: { CI: "1" }, capture: true }); } finally { writeFileSync(path.join(root, "package.json"), manifest); } } else if (platform !== "web") await run(root, [process.execPath, "node_modules/@legendapp/spark/bin/spark.cjs", "prebuild", "--platform", platform], { capture: true }); record(report, { id: "build.project", status: "passed" }); stage = "build.bundle"; checkpoint(); @@ -137,7 +137,7 @@ try { writeJson(stateFile(root, "session.json"), { compatible: true, target: "test", port }); mkdirSync(path.dirname(stateFile(root, "contract-metro.log")), { recursive: true }); metro = spawnProcess(nodeCommand(root, "expo", "expo", ["start", "--localhost", "--port", String(port), "--max-workers", "2"]), { - cwd: root, env: { ...process.env, CI: "1" }, stdout: processLog(stateFile(root, "contract-metro.log")), stderr: processLog(stateFile(root, "contract-metro-errors.log")), + cwd: root, env: { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ""} --dns-result-order=ipv4first`, CI: "1" }, stdout: processLog(stateFile(root, "contract-metro.log")), stderr: processLog(stateFile(root, "contract-metro-errors.log")), }); stage = "runtime.launch"; await until(() => fetch(`http://127.0.0.1:${port}/status`, { signal: AbortSignal.timeout(1000) }).then(r => r.ok, () => false), "Metro", 60_000); diff --git a/scripts/test-runtimes.ts b/scripts/test-runtimes.ts index 7f81e37bf..68c3a2dd4 100644 --- a/scripts/test-runtimes.ts +++ b/scripts/test-runtimes.ts @@ -43,7 +43,7 @@ try { if (mode === "dev") { writeJson(path.join(root, ".spark/session.json"), { compatible: true, target: "test", port }); const log = processLog(path.join(directory, "metro.log")); - metro = spawnProcess([binary(root, "expo"), "start", "--localhost", "--port", String(port), "--max-workers", "2"], { cwd: root, env: { ...process.env, CI: "1" }, stdout: log, stderr: log }); + metro = spawnProcess([binary(root, "expo"), "start", "--localhost", "--port", String(port), "--max-workers", "2"], { cwd: root, env: { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ""} --dns-result-order=ipv4first`, CI: "1" }, stdout: log, stderr: log }); let ready = false; for (let i = 0; i < 120; i++) { if (await fetch(`http://127.0.0.1:${port}/status`, { signal: AbortSignal.timeout(1000) }).then(r => r.ok, () => false)) { ready = true; break; } diff --git a/scripts/test-sidecars.ts b/scripts/test-sidecars.ts index fa7d2c4cb..8fa80121c 100644 --- a/scripts/test-sidecars.ts +++ b/scripts/test-sidecars.ts @@ -23,7 +23,7 @@ await run(directory, ["codesign", "--force", "--deep", "--sign", "-", appPath]); const port = await availablePort(); const report = path.join(directory, "report.json"); const metroLog = processLog(path.join(directory, "metro.log")); -const metro = spawnProcess([binary(root, "expo"), "start", "--localhost", "--port", String(port), "--max-workers", "2"], { cwd: root, env: { ...process.env, CI: "1", SPARK_PLATFORM: "macos" }, stdout: metroLog, stderr: metroLog }); +const metro = spawnProcess([binary(root, "expo"), "start", "--localhost", "--port", String(port), "--max-workers", "2"], { cwd: root, env: { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ""} --dns-result-order=ipv4first`, CI: "1", SPARK_PLATFORM: "macos" }, stdout: metroLog, stderr: metroLog }); let app: ReturnType<typeof spawnProcess> | undefined; try { const deadline = Date.now() + 60000; diff --git a/scripts/test-ui.ts b/scripts/test-ui.ts index 90d011d4b..bfd068c61 100644 --- a/scripts/test-ui.ts +++ b/scripts/test-ui.ts @@ -33,7 +33,7 @@ try { writeJson(path.join(root, ".spark/session.json"), { compatible: true, target: "test", port }); const log = processLog(path.join(root, ".spark/ui-metro.log")); metro = spawnProcess([binary(root, "expo"), "start", "--localhost", "--port", String(port), "--max-workers", "2"], { - cwd: root, env: { ...process.env, CI: "1" }, stdout: log, stderr: log, + cwd: root, env: { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ""} --dns-result-order=ipv4first`, CI: "1" }, stdout: log, stderr: log, }); await waitFor(async () => fetch(`http://127.0.0.1:${port}/status`, { signal: AbortSignal.timeout(1000) }).then(r => r.ok, () => false), "Metro"); const executable = (await run(root, ["/usr/libexec/PlistBuddy", "-c", "Print :CFBundleExecutable", path.join(product.app, "Contents/Info.plist")], { capture: true })).trim(); diff --git a/scripts/test-updates.ts b/scripts/test-updates.ts index 21c619dc9..14ca7d428 100644 --- a/scripts/test-updates.ts +++ b/scripts/test-updates.ts @@ -66,7 +66,7 @@ export default function App() { const currentPackage = readFileSync(path.join(root, "package.json"), "utf8"); writeFileSync(configFile, originalConfig); prepareConfig(root); try { - await run(root, [binary(root, "expo-desktop"), "prebuild", "--platform", "macos", "--template", "expo-desktop-template-bare-minimum@54.81.6", "--no-install"], { env: { CI: "1" }, capture: true }); + await run(root, [binary(root, "expo-desktop"), "prebuild", "--platform", "macos", "--template", "expo-desktop-template-bare-minimum@55.83.10", "--no-install"], { env: { CI: "1" }, capture: true }); const regenerated = JSON.parse(await run(root, ["plutil", "-convert", "json", "-o", "-", path.join(root, "macos", `${info.CFBundleExecutable}-macOS`, "Info.plist")], { capture: true })); if (regenerated.SUFeedURL || regenerated.SUPublicEDKey || regenerated.SURequireSignedFeed || regenerated.LSUIElement || regenerated.SparkMenuBarOnly) throw new Error("Removed update/menu-bar configuration survived CNG"); console.log("PASS: prebuild removes stale update feed/key and resets menu-bar-only activation"); diff --git a/scripts/test-windows-features.ts b/scripts/test-windows-features.ts index 4a69e5e46..b56e94cf3 100644 --- a/scripts/test-windows-features.ts +++ b/scripts/test-windows-features.ts @@ -143,7 +143,7 @@ try { coverage.runtime = product.runtime; record(coverage, { id: "build.native", status: "passed" }); coverageStage = "runtime.launch"; writeJson(stateFile(root, "session.json"), { compatible: true, target: "test", port }); const log = processLog(stateFile(root, "features-metro.log")); - metro = spawnProcess(nodeCommand(root, "expo", "expo", ["start", "--localhost", "--port", String(port), "--max-workers", "2"]), { cwd: root, env: { ...process.env, CI: "1" }, stdout: log, stderr: log }); + metro = spawnProcess(nodeCommand(root, "expo", "expo", ["start", "--localhost", "--port", String(port), "--max-workers", "2"]), { cwd: root, env: { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ""} --dns-result-order=ipv4first`, CI: "1" }, stdout: log, stderr: log }); for (let i = 0; i < 120; i++) { if (await fetch(`http://127.0.0.1:${port}/status`).then(r => r.ok, () => false)) break; await sleep(500); diff --git a/tests/accessibility-state-native.test.ts b/tests/accessibility-state-native.test.ts index 1b79326d7..715c714ce 100644 --- a/tests/accessibility-state-native.test.ts +++ b/tests/accessibility-state-native.test.ts @@ -24,7 +24,7 @@ test("accessibility state patch install checks the version and preserves package const dependency = path.join(root, "node_modules/react-native-macos"), cache = path.join(root, "cache.mm"); const installed = path.join(dependency, file); mkdirSync(path.dirname(installed), { recursive: true }); - writeFileSync(path.join(dependency, "package.json"), JSON.stringify({ name: "react-native-macos", version: "0.81.7" })); + writeFileSync(path.join(dependency, "package.json"), JSON.stringify({ name: "react-native-macos", version: "0.83.0" })); writeFileSync(cache, pristine); linkSync(cache, installed); installAccessibilityStatePatch(root); expect(readFileSync(cache, "utf8")).toBe(pristine); @@ -32,7 +32,7 @@ test("accessibility state patch install checks the version and preserves package const inode = statSync(installed).ino; installAccessibilityStatePatch(root); expect(statSync(installed).ino).toBe(inode); writeFileSync(path.join(dependency, "package.json"), JSON.stringify({ name: "react-native-macos", version: "0.82.0" })); - expect(() => installAccessibilityStatePatch(root)).toThrow("requires react-native-macos@0.81.7"); + expect(() => installAccessibilityStatePatch(root)).toThrow("requires react-native-macos@0.83.0"); } finally { rmSync(root, { recursive: true, force: true }); } }); diff --git a/tests/audio-desktop.test.ts b/tests/audio-desktop.test.ts index 89ed2e51e..1dd51b25f 100644 --- a/tests/audio-desktop.test.ts +++ b/tests/audio-desktop.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, expect, test, vi } from "vitest"; const { call, listeners } = vi.hoisted(() => ({ call: vi.fn(), listeners: new Set<(value: unknown) => void>() })); vi.mock("react-native", () => ({ NativeEventEmitter: class { addListener(_name: string, listener: (value: unknown) => void) { listeners.add(listener); return { remove() { listeners.delete(listener); } }; } } })); vi.mock("../packages/audio/src/NativeSparkAudio", () => ({ default: { call } })); -import { createAudioPlayer } from "../packages/audio/src/desktop"; +import { createAudioPlayer } from "../packages/audio/src/index.macos"; beforeEach(() => { listeners.clear(); call.mockReset(); call.mockImplementation(async (method: string) => method === "awaitReady" ? "true" : "null"); }); afterEach(() => vi.useRealTimers()); test("desktop player validates transport readiness and awaits failed-load cleanup", async () => { diff --git a/tests/audio-pack.test.ts b/tests/audio-pack.test.ts index 56b51b101..57d931082 100644 --- a/tests/audio-pack.test.ts +++ b/tests/audio-pack.test.ts @@ -12,7 +12,7 @@ test("distributed audio archives apply the exact workspace delta and verify upst const root = mkdtempSync(path.join(os.tmpdir(), "spark-audio-package-")); try { const actual = path.resolve(import.meta.dirname, ".."); - const patch = readFileSync(path.join(actual, "patches/workspace/expo-audio@1.1.1.patch"), "utf8"); + const patch = readFileSync(path.join(actual, "patches/workspace/expo-audio@55.0.18.patch"), "utf8"); const stage = path.join(root, "upstream"), cache = path.join(root, ".spark/vendor/audio"), output = path.join(root, "output"); mkdirSync(cache, { recursive: true }); mkdirSync(path.join(root, "patches/workspace"), { recursive: true }); const expected = new Map<string, string>(); @@ -24,8 +24,8 @@ test("distributed audio archives apply the exact workspace delta and verify upst const target = path.join(stage, relative); mkdirSync(path.dirname(target), { recursive: true }); writeFileSync(target, original); } const archive = path.join(cache, "upstream.tgz"); await packArchive(stage, archive); - writeFileSync(path.join(root, "patches/workspace/upstream.json"), JSON.stringify({ "expo-audio": { version: "1.1.1", integrity: `sha512-${createHash("sha512").update(readFileSync(archive)).digest("base64")}` } })); - writeFileSync(path.join(root, "patches/workspace/expo-audio@1.1.1.patch"), patch); + writeFileSync(path.join(root, "patches/workspace/upstream.json"), JSON.stringify({ "expo-audio": { version: "55.0.18", integrity: `sha512-${createHash("sha512").update(readFileSync(archive)).digest("base64")}` } })); + writeFileSync(path.join(root, "patches/workspace/expo-audio@55.0.18.patch"), patch); cpSync(path.join(actual, "patches/licenses"), path.join(root, "patches/licenses"), { recursive: true }); const manifest = await packAudio(root, output); const packed = path.join(output, manifest["expo-audio"]); expect(execFileSync("tar", ["-tzf", packed], { encoding: "utf8" })).not.toContain("package/.spark/"); diff --git a/tests/auth-desktop.test.ts b/tests/auth-desktop.test.ts index 73d8a788a..3cbf9bc5a 100644 --- a/tests/auth-desktop.test.ts +++ b/tests/auth-desktop.test.ts @@ -2,7 +2,7 @@ import { expect, test, vi } from "vitest"; const mocks = vi.hoisted(() => ({ call: vi.fn() })); vi.mock("react-native", () => ({ Platform: { OS: "macos" }, TurboModuleRegistry: { get: () => ({ call: mocks.call }) } })); vi.mock("@legendapp/spark-desktop-app/src/events", () => ({ onDesktopEvent: () => ({ remove() {} }) })); -import { createAuthSession, getRandomBytesAsync, digestStringAsync, maybeCompleteAuthSession } from "../packages/auth-session/src/index"; +import { createAuthSession, getRandomBytesAsync, digestStringAsync, maybeCompleteAuthSession } from "../packages/auth-session/src/index.macos"; test("desktop crypto rejects malformed, noncanonical and wrong-size native results", async () => { for (const value of [null, "AA==", "not base64!!", "AB=="]) { mocks.call.mockResolvedValue(JSON.stringify(value)); diff --git a/tests/autolinking.test.ts b/tests/autolinking.test.ts index 5827154d2..71b9e5a22 100644 --- a/tests/autolinking.test.ts +++ b/tests/autolinking.test.ts @@ -1,6 +1,6 @@ import { expect, test } from "vitest"; import { createRequire } from "node:module"; -import { readFileSync, writeFileSync, mkdirSync, mkdtempSync, linkSync, symlinkSync, rmSync, statSync } from "node:fs"; +import { copyFileSync, readFileSync, writeFileSync, mkdirSync, mkdtempSync, linkSync, symlinkSync, rmSync, statSync } from "node:fs"; import os from "node:os"; import path from "node:path"; const require = createRequire(import.meta.url); @@ -28,9 +28,10 @@ test("autolinking resolves symlinked podspec files without modifying the package const dependency = path.join(root, "node_modules/expo-modules-autolinking"); mkdirSync(path.join(dependency, "build"), { recursive: true }); const manifest = path.join(dependency, "package.json"); - writeFileSync(manifest, JSON.stringify({ name: "expo-modules-autolinking", version: "3.0.27" })); + writeFileSync(manifest, JSON.stringify({ name: "expo-modules-autolinking", version: "55.0.28" })); const cache = path.join(root, "cache.js"), installed = path.join(dependency, "build/utils.js"); writeFileSync(cache, source); linkSync(cache, installed); + copyFileSync(require.resolve("expo-modules-autolinking/build/memoize.js"), path.join(dependency, "build/memoize.js")); installAutolinkingPatch(root); expect(readFileSync(cache, "utf8")).toBe(source); expect(readFileSync(installed, "utf8")).toBe(patchAutolinkingSource(source)); @@ -49,25 +50,25 @@ test("autolinking resolves symlinked podspec files without modifying the package .toEqual([path.join(fixture, "a.podspec"), path.join(fixture, "b.podspec")]); expect(() => patchAutolinkingSource("changed upstream")).toThrow("source changed"); writeFileSync(manifest, JSON.stringify({ name: "expo-modules-autolinking", version: "4.0.0" })); - expect(() => installAutolinkingPatch(root)).toThrow("requires expo-modules-autolinking@3.0.27"); + expect(() => installAutolinkingPatch(root)).toThrow("requires expo-modules-autolinking@55.0.28"); } finally { rmSync(root, { recursive: true, force: true }); } }); test("the macOS Podfile passes the native selection's exclusions to Expo's React Native autolinker", async () => { const template = readFileSync(require.resolve("expo-desktop-template-bare-minimum/macos/Podfile"), "utf8"); const podfile = excludeNativeModules(template, ["@legendapp/spark-codex", "react-native-nitro-modules"]); - expect(podfile).toContain('use_native_modules!(config_command + ["--exclude","@legendapp/spark-codex","--exclude","react-native-nitro-modules"])'); - expect(excludeNativeModules(podfile, [])).toContain("use_native_modules!(config_command + [])"); + expect(podfile).toContain('use_native_modules!(config_command + ["--source-dir", __dir__] + ["--exclude","@legendapp/spark-codex","--exclude","react-native-nitro-modules"])'); + expect(excludeNativeModules(podfile, [])).toContain('use_native_modules!(config_command + ["--source-dir", __dir__] + [])'); + expect(excludeNativeModules(podfile, ["@legendapp/spark-codex", "react-native-nitro-modules"])).toBe(podfile); expect(() => excludeNativeModules("target 'App' do\nend\n", [])).toThrow("use_native_modules!(config_command)"); - // An app-level `ios: null` cannot unlink Codex, whose own config declares iOS for the desktop autolinker. + // Codex's own config declares iOS for the desktop autolinker; the exclusion list unlinks it. const { resolveReactNativeModule } = require("expo-modules-autolinking/build/reactNativeConfig/reactNativeConfig.js"); const codex = path.dirname(require.resolve("../packages/codex/package.json")); const resolution = { name: "@legendapp/spark-codex", version: "0.0.1-next.3", path: codex, originPath: codex, duplicates: null, depth: 0 }; - const disabled = { dependencies: { "@legendapp/spark-codex": { platforms: { ios: null } } } }; const previous = process.env.SPARK_DESKTOP_AUTOLINK; process.env.SPARK_DESKTOP_AUTOLINK = "macos"; try { - expect(await resolveReactNativeModule(resolution, disabled, "ios", new Set())).not.toBeNull(); - expect(await resolveReactNativeModule(resolution, disabled, "ios", new Set(["@legendapp/spark-codex"]))).toBeNull(); + expect(await resolveReactNativeModule(resolution, {}, "ios", new Set())).not.toBeNull(); + expect(await resolveReactNativeModule(resolution, {}, "ios", new Set(["@legendapp/spark-codex"]))).toBeNull(); } finally { if (previous === undefined) delete process.env.SPARK_DESKTOP_AUTOLINK; else process.env.SPARK_DESKTOP_AUTOLINK = previous; diff --git a/tests/customize.integration.test.ts b/tests/customize.integration.test.ts index fbe8b6614..6616eee76 100644 --- a/tests/customize.integration.test.ts +++ b/tests/customize.integration.test.ts @@ -17,7 +17,7 @@ it.skipIf(process.env.SPARK_TEST_CUSTOMIZE !== '1' || process.platform !== 'darw const app = path.join(root, 'Test.app'), sourcePath = layout === 'monorepo' ? 'apps/demo/index.js' : 'index.js'; mkdirSync(root, { recursive: true }); symlinkSync(path.resolve('node_modules'), path.join(root, 'node_modules')); - writeJson(path.join(root, 'package.json'), { name: 'customize-test', private: true, main: 'index.js', dependencies: { '@legendapp/spark': VERSION, expo: '54.0.37', react: '19.1.4', 'react-native': '0.81.6', 'react-native-macos': '0.81.7', 'expo-desktop-metro-config': '54.81.3' } }); + writeJson(path.join(root, 'package.json'), { name: 'customize-test', private: true, main: 'index.js', dependencies: { '@legendapp/spark': VERSION, expo: '55.0.32', react: '19.2.0', 'react-native': '0.83.10', 'react-native-macos': '0.83.0', 'expo-desktop-metro-config': '55.83.3' } }); const files = ['package.json', 'desktop.config.json', 'metro.config.js', 'index.js', 'pixel.png']; writeJson(path.join(root, 'desktop.config.json'), { name: 'Customize Test', version: '1.0.0', projectId: 'test.spark.customize', macos: { bundleIdentifier: 'test.spark.customize' }, platforms: ['macos'], customize: { repository: 'test/app', files: layout === 'monorepo' ? ['apps/demo/index.js', 'apps/demo/pixel.png', 'packages/shared'] : files, ...(layout === 'monorepo' ? { sourceRoot: '../..' } : {}) } }); writeFileSync(path.join(root, 'metro.config.js'), 'const { makeMetroConfig } = require("expo-desktop-metro-config"); const { withDesktop } = require("@legendapp/spark/metro"); module.exports=withDesktop(makeMetroConfig(__dirname), {runtimes:false});\n'); diff --git a/tests/customize.test.ts b/tests/customize.test.ts index 19923d0a2..c5e547b54 100644 --- a/tests/customize.test.ts +++ b/tests/customize.test.ts @@ -220,7 +220,9 @@ describe('native opt-in configuration', () => { const result = await config.mods.macos.appDelegate({ ...config, modResults: { contents: '' }, modRequest: { projectRoot: root, platform: 'macos', modName: 'appDelegate', introspect: false } }); expect(result.modResults.contents.includes('@interface SparkCustomizeController')).toBe(enabled); expect(result.modResults.contents.includes('return SparkCustomizeBundleURL();')).toBe(enabled); - expect(!!config.mods.macos.xcodeproj).toBe(enabled); + const project = require('xcode').project(path.join(path.dirname(require.resolve('expo-desktop-template-bare-minimum/package.json')), 'macos/HelloWorld.xcodeproj/project.pbxproj')).parseSync(); + const xcodeproj = await config.mods.macos.xcodeproj({ ...config, modResults: project, modRequest: { projectRoot: root, platform: 'macos', modName: 'xcodeproj', introspect: false } }); + expect(xcodeproj.modResults.writeSync().includes('Security.framework')).toBe(enabled); } }); it('rejects sandboxed customize app distribution', () => { diff --git a/tests/desktop-library-checks.test.ts b/tests/desktop-library-checks.test.ts new file mode 100644 index 000000000..dfda80e8d --- /dev/null +++ b/tests/desktop-library-checks.test.ts @@ -0,0 +1,117 @@ +import { createRequire } from "node:module"; +import React, { act } from "react"; +import { afterEach, beforeEach, expect, test, vi } from "vitest"; +const { create } = createRequire(import.meta.url)("react-test-renderer"); + +const mocks = vi.hoisted(() => ({ renderPicker: true, permission: "undetermined", heading: vi.fn() })); +vi.mock("react-native", () => ({ + ScrollView: "ScrollView", Text: "Text", View: "View", Pressable: "Pressable", + TurboModuleRegistry: { get: () => ({}) }, UIManager: { hasViewManagerConfig: () => true }, +})); +vi.mock("@react-native-community/datetimepicker", async () => { + const { createElement } = await import("react"); + return { default: (props: object) => mocks.renderPicker ? createElement("NativePicker", props) : null }; +}); +vi.mock("expo-location", () => ({ + getForegroundPermissionsAsync: async () => ({ status: mocks.permission }), + hasServicesEnabledAsync: async () => true, + getProviderStatusAsync: async () => ({ locationServicesEnabled: true }), + getHeadingAsync: () => mocks.heading(), +})); +vi.mock("react-native-gesture-handler", () => { + const gesture = (kind: string) => ({ + kind, callback: undefined as any, + runOnJS() { return this; }, + onEnd(callback: unknown) { this.callback = callback; return this; }, + }); + return { + GestureDetector: "GestureDetector", GestureHandlerRootView: "GestureHandlerRootView", RectButton: "RectButton", + Gesture: { Tap: () => gesture("tap"), Pan: () => gesture("pan"), Race: (...children: unknown[]) => ({ children }) }, + }; +}); + +import { DateTimePickerDemo } from "../examples/kitchen-sink/screens/native-controls/DateTimePickerDemo"; +import { ExpoLocationDemo } from "../examples/kitchen-sink/screens/system-integration/ExpoLocationDemo"; +import { GestureHandlerDemo } from "../examples/kitchen-sink/screens/component-library/GestureHandlerDemo"; + +let rendered: any; +beforeEach(() => { + (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; + mocks.renderPicker = true; + mocks.permission = "undetermined"; + mocks.heading.mockReset(); + const original = console.error; + vi.spyOn(console, "error").mockImplementation((...args) => { if (!String(args[0]).startsWith("react-test-renderer is deprecated")) original(...args); }); +}); +afterEach(async () => { + if (rendered) await act(async () => { rendered.unmount(); rendered = undefined; }); + vi.restoreAllMocks(); +}); +async function mount(component: React.ComponentType) { + await act(async () => { rendered = create(React.createElement(component)); }); +} +const host = (type: string) => rendered.root.findAll((node: any) => node.type === type); +const ids = () => rendered.root.findAll((node: any) => typeof node.type === "string" && node.props.testID).map((node: any) => node.props.testID); +const layout = (width: number, height: number) => ({ nativeEvent: { layout: { width, height, x: 0, y: 0 } } }); + +test("each datetimepicker earns its own marker only after a nonzero native frame", async () => { + await mount(DateTimePickerDemo); + expect(ids()).toEqual([]); + expect(host("View").every((node: any) => node.props.onLayout === undefined && node.props.testID === undefined)).toBe(true); + for (const [index, name] of ["date", "time", "inline"].entries()) { + for (const [width, height] of [[0, 20], [20, 0], [-1, 20]]) { + await act(async () => host("NativePicker")[index].props.onLayout(layout(width!, height!))); + expect(ids()).not.toContain(`native-controls-datetimepicker-${name}-laid-out`); + } + await act(async () => host("NativePicker")[index].props.onLayout(layout(100, 30))); + expect(ids()).toEqual(["date", "time", "inline"].slice(0, index + 1).map(name => `native-controls-datetimepicker-${name}-laid-out`)); + } +}); + +test("datetimepicker wrappers cannot pass when the native pickers render nothing", async () => { + mocks.renderPicker = false; + await mount(DateTimePickerDemo); + expect(host("View")).toHaveLength(3); + expect(host("NativePicker")).toHaveLength(0); + expect(ids()).toEqual([]); +}); + +for (const permission of ["granted", "denied", "undetermined"]) { + test(`expo-location accepts the exact macOS heading rejection with ${permission} permission`, async () => { + mocks.permission = permission; + mocks.heading.mockRejectedValue(permission === "granted" ? "Heading updates not available" : { code: "ERR_DENIED_FOREGROUND_LOCATION_PERMISSION" }); + await mount(ExpoLocationDemo); + expect(mocks.heading).toHaveBeenCalledTimes(1); + expect(ids()).toEqual(["system-integration-expo-location-passed"]); + }); + + test.each([ + ["a different native code", { code: "ERR_HEADING_UNAVAILABLE" }, false], + ["an error without a code", new Error("unavailable"), false], + ["an arbitrary rejection string", "unavailable", false], + ["a resolved heading", { trueHeading: 0 }, true], + ])(`expo-location fails for %s with ${permission} permission`, async (_description, value, resolves) => { + mocks.permission = permission; + if (resolves) mocks.heading.mockResolvedValue(value); + else mocks.heading.mockRejectedValue(value); + await mount(ExpoLocationDemo); + expect(ids()).toEqual(["system-integration-expo-location-failed"]); + }); +} + +test("gesture-handler passes only after the detector's successful tap callback", async () => { + await mount(GestureHandlerDemo); + const marker = "component-library-gesture-handler-tap-fired"; + expect(ids()).not.toContain(marker); + const target = host("View").find((node: any) => node.props.testID === "component-library-gesture-handler-target"); + expect(target.props.onLayout).toBeUndefined(); + await act(async () => host("RectButton")[0].props.onPress()); + expect(ids()).not.toContain(marker); + const detector = host("GestureDetector")[0]; + expect(detector.findAll((node: any) => node === target)).toHaveLength(1); + const tap = () => host("GestureDetector")[0].props.gesture.children.find((gesture: any) => gesture.kind === "tap"); + await act(async () => tap().callback({}, false)); + expect(ids()).not.toContain(marker); + await act(async () => tap().callback({}, true)); + expect(ids()).toContain(marker); +}); diff --git a/tests/e2e-app-state.test.ts b/tests/e2e-app-state.test.ts index c4c4e49dc..848d0c98a 100644 --- a/tests/e2e-app-state.test.ts +++ b/tests/e2e-app-state.test.ts @@ -1,9 +1,13 @@ +import { spawnSync } from "node:child_process"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, symlinkSync, writeFileSync } from "node:fs"; +import os from "node:os"; import { tmpdir } from "node:os"; import path from "node:path"; -import { describe, expect, test } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import { clearAppState, sparkNamespace, statePaths, type AppIdentity, type StateHost } from "../scripts/e2e/app-state.ts"; +vi.mock("node:child_process", () => ({ spawnSync: vi.fn() })); + const BUNDLE_ID = "so.legend.spark.examples.kitchensink"; const KITCHEN_SINK: AppIdentity = { bundleId: BUNDLE_ID, projectId: "c0bdf12f-0f35-4b71-8363-b83097388d7b", sandboxed: false }; const NS = "spark.desktop.f24fabcdca7ee623a41d0258b883fdc21422e5c283c01f3ece499645ed451090"; @@ -95,3 +99,26 @@ test("clearState refuses relative, empty or root home/temp directories", () => { expect(() => clearAppState(identity, identity.bundleId, host)).toThrow(/not an absolute, non-root path/); } }); + +test.each([ + [`Domain '${BUNDLE_ID}' not found`, false], + [`2026-10-28 defaults[123:456]\nDomain (${BUNDLE_ID}) not found.\nDefaults have not been changed.\n`, false], + ["Domain (com.example.other) not found.", true], + [`Could not write domain (${BUNDLE_ID}): Permission denied`, true], +])("defaults failure %s only clears an absent app domain", (stderr, fails) => { + const { host: fake } = host(); + vi.spyOn(os, "homedir").mockReturnValue(fake.home); + const result = { pid: 123, output: [], signal: null, status: 0, stdout: "", stderr: "" }; + vi.mocked(spawnSync) + .mockReturnValueOnce({ ...result, stdout: fake.temp }) + .mockReturnValueOnce(result) + .mockReturnValueOnce({ ...result, status: 1, stderr }); + try { + if (fails) expect(() => clearAppState(KITCHEN_SINK, BUNDLE_ID)).toThrow(`defaults delete ${BUNDLE_ID} failed (1): ${stderr.trim()}`); + else expect(clearAppState(KITCHEN_SINK, BUNDLE_ID)).toEqual([]); + expect(spawnSync).toHaveBeenLastCalledWith("defaults", ["delete", BUNDLE_ID], { encoding: "utf8" }); + } finally { + vi.restoreAllMocks(); + vi.mocked(spawnSync).mockReset(); + } +}); diff --git a/tests/e2e-driver-fake.ts b/tests/e2e-driver-fake.ts index a8f097eb3..469065b4a 100644 --- a/tests/e2e-driver-fake.ts +++ b/tests/e2e-driver-fake.ts @@ -15,7 +15,12 @@ export class FakeDriver implements DriverClient { private readonly directory = mkdtempSync(path.join(tmpdir(), "fake-driver-")); constructor(private readonly log: string[], private readonly views: Record<string, number>) {} + async restorePointer() { this.log.push("restorePointer"); } async navigate(url: string) { this.log.push(`navigate ${url}`); } + async pointerEvents(window: DriverWindow, point: { x: number; y: number }, _timeoutMs?: number) { + this.log.push(`pointerEvents ${window} ${point.x},${point.y}`); + return { pid: 123, down: "down-event", up: "up-event" }; + } async openFile(file: string) { this.log.push(`openFile ${file}`); } async waitFor(testID: string, timeoutMs?: number) { this.log.push(`waitFor ${testID} ${timeoutMs}`); diff --git a/tests/e2e-pointer-backend.test.ts b/tests/e2e-pointer-backend.test.ts new file mode 100644 index 000000000..044ba4067 --- /dev/null +++ b/tests/e2e-pointer-backend.test.ts @@ -0,0 +1,127 @@ +import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { expect, test, vi } from "vitest"; +import { DriverError } from "../scripts/e2e/driver.ts"; +import { DriverBackend } from "../scripts/e2e/runner/driver-backend.ts"; +import { PointerBackend } from "../scripts/e2e/runner/pointer-backend.ts"; +import { runFlows } from "../scripts/e2e/runner/executor.ts"; +import { runCommand } from "../scripts/e2e/runner/run.ts"; +import { FakeApp } from "./e2e-driver-fake.ts"; + +const native = vi.hoisted(() => ({ calls: [] as string[][], denied: false })); +vi.mock("node:child_process", async importOriginal => ({ + ...await importOriginal<typeof import("node:child_process")>(), + execFile: (file: string, args: string[], _options: unknown, callback: (error: unknown, stdout: string) => void) => { + native.calls.push([file, ...args]); + callback(native.denied && args[0] === "check" ? Object.assign(new Error("denied"), { stderr: "E_PERMISSION_DENIED: Accessibility permission is required" }) : null, ""); + }, +})); + +async function run(command: string, flows = 1, app = new FakeApp({ target: 1 })) { + native.calls = []; + const dir = mkdtempSync(path.join(tmpdir(), "e2e-pointer-")); + mkdirSync(path.join(dir, "flows")); + const file = path.join(dir, "flows/test.yaml"); + writeFileSync(file, `appId: so.legend.spark.examples.kitchensink\nname: Pointer\nintent: A real click.\n---\n- launchApp\n${command}\n`); + const driver = new DriverBackend(app); + const results = await runFlows(Array.from({ length: flows }, () => file), { backends: [driver, new PointerBackend(driver)], runDir: path.join(dir, "run") }); + return { result: results[0]!, results, app }; +} + +test.skipIf(process.platform !== "darwin")("tapOn resolves an element on the OS backend, translates its center once, and posts once", async () => { + native.denied = false; + const { result, app } = await run("- tapOn: { id: target }"); + expect(result.status, JSON.stringify(result.failure)).toBe("passed"); + expect(app.log.filter(line => line.startsWith("pointerEvents"))).toEqual(["pointerEvents id:spark.main 60,40"]); + expect(native.calls.slice(1).map(call => call.slice(1))).toEqual([["check"], ["123", "down-event", "up-event"]]); + expect(app.log.slice(-4)).toEqual(["restorePointer", "quit", "close", "app close"]); +}); + +test.skipIf(process.platform !== "darwin")("missing permission fails at flow start before launching or showing the app", async () => { + native.denied = true; + const { result, app } = await run("- tapOn: { id: target }"); + expect(result.failure).toMatchObject({ kind: "error", message: "E_PERMISSION_DENIED: Accessibility permission is required" }); + expect(app.log.some(line => line.startsWith("pointerEvents"))).toBe(false); + expect(native.calls).toHaveLength(2); + expect(app.log).toEqual(["open", "app close"]); +}); + +test.skipIf(process.platform !== "darwin")("unsupported modifiers fail before posting input", async () => { + native.denied = false; + const { result } = await run("- tapOn: { id: target, modifiers: [shift] }"); + expect(result.failure).toMatchObject({ kind: "unsupported", message: "pointer tapOn does not support modifiers" }); + expect(native.calls.slice(1).map(call => call.slice(1))).toEqual([["check"]]); +}); + +test.skipIf(process.platform !== "darwin")("each flow preflights once and shares the compiled helper, including explicit points", async () => { + native.denied = false; + const { results, app } = await run("- tapOn: { point: '25%,75%', within: { id: target } }\n- tapOn: { id: target }", 2); + expect(results.map(result => result.status), JSON.stringify(results.map(result => result.failure), null, 2)).toEqual(["passed", "passed"]); + expect(native.calls.filter(call => call[0] === "clang")).toHaveLength(1); + expect(native.calls.filter(call => call[1] === "check")).toHaveLength(2); + expect(app.log.filter(line => line.startsWith("pointerEvents"))).toEqual([ + "pointerEvents id:spark.main 35,50", "pointerEvents id:spark.main 60,40", + "pointerEvents id:spark.main 35,50", "pointerEvents id:spark.main 60,40", + ]); + expect(app.log.filter(line => line === "restorePointer")).toHaveLength(2); +}); + +test.skipIf(process.platform !== "darwin")("pointer state is restored after a tap when a later assertion fails", async () => { + native.denied = false; + const { result, app } = await run("- tapOn: { id: target }\n- assertNotVisible: { id: target, timeout: 20ms }"); + expect(result.failure?.kind).toBe("assertion"); + expect(app.log.slice(-4)).toEqual(["restorePointer", "quit", "close", "app close"]); +}); + +/** An app whose UI keeps mounting for its first `unsettled` pointer requests, which wait out their timeout on a fake Date. */ +function mountingApp(unsettled: number, onUnsettled = () => {}) { + const views = { target: 1 }, app = new FakeApp(views), timeouts: number[] = [], launched = app.launch.bind(app); + app.launch = async options => { + const driver = await launched(options), pointerEvents = driver.pointerEvents.bind(driver); + driver.pointerEvents = async (window, point, timeoutMs) => { + timeouts.push(timeoutMs!); + if (timeouts.length > unsettled || !timeoutMs) return pointerEvents(window, point); + vi.advanceTimersByTime(timeoutMs); + onUnsettled(); + throw new DriverError("E_NOT_SETTLED", "The UI kept mounting changes until the timeout"); + }; + return driver; + }; + return { app, timeouts, views }; +} + +test.skipIf(process.platform !== "darwin")("a screen that keeps mounting is tapped once the tap's timeout is spent", async () => { + native.denied = false; + const { app, timeouts } = mountingApp(Infinity); + vi.useFakeTimers({ toFake: ["Date"] }); + const { result } = await run("- tapOn: { id: target, timeout: 300ms }", 1, app).finally(() => vi.useRealTimers()); + expect(result.status, JSON.stringify(result.failure)).toBe("passed"); + expect(timeouts).toEqual([300, 0]); + expect(native.calls.slice(1).map(call => call.slice(1))).toEqual([["check"], ["123", "down-event", "up-event"]]); +}); + +test.skipIf(process.platform !== "darwin")("long tap timeouts wait in slices under the driver's reply limit and re-resolve the target", async () => { + native.denied = false; + const { app, timeouts } = mountingApp(1); + vi.useFakeTimers({ toFake: ["Date"] }); + const { result } = await run("- tapOn: { id: target, timeout: 60s }", 1, app).finally(() => vi.useRealTimers()); + expect(result.status, JSON.stringify(result.failure)).toBe("passed"); + expect(timeouts).toEqual([5000, 5000]); + expect(app.log.filter(line => line.startsWith("waitFor target"))).toHaveLength(2); +}); + +test.skipIf(process.platform !== "darwin")("re-resolving a tap target spends only what is left of the step's timeout", async () => { + native.denied = false; + const { app, views } = mountingApp(Infinity, () => { views.target = Infinity; }); + vi.useFakeTimers({ toFake: ["Date"] }); + const { result } = await run("- tapOn: { id: target, timeout: 1s }", 1, app).finally(() => vi.useRealTimers()); + expect(result.failure?.kind).toBe("not-found"); + expect(result.steps.find(step => step.command === "tapOn")!.durationMs).toBeLessThan(500); +}); + +test("--pointer requires a driver session rather than starting a second app", async () => { + const errors: string[] = []; + expect(await runCommand(["--pointer"], { backends: { other: () => ({ name: "other" } as never) }, stdout: () => {}, stderr: line => errors.push(line) })).toBe(2); + expect(errors).toEqual(["--pointer requires the driver backend"]); +}); diff --git a/tests/expo-dev.test.ts b/tests/expo-dev.test.ts index e62f46ebb..ea9e1ad56 100644 --- a/tests/expo-dev.test.ts +++ b/tests/expo-dev.test.ts @@ -1,10 +1,11 @@ import { spawnProcess } from "../packages/cli/src/process.ts"; import { expect, test } from "vitest"; import { createRequire } from "node:module"; -import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; import os from "node:os"; import { createContext, runInContext } from "node:vm"; import path from "node:path"; +import ts from "typescript"; const require = createRequire(import.meta.url); const { preparePatch, VERSION } = require("../packages/cli/src/expo-dev-patch.cjs"); @@ -20,6 +21,7 @@ test("Metro readiness does not wait for Expo's dependency checks", async () => { const exports: any = {}; const modules: Record<string, unknown> = { "@expo/config": { getConfig: () => ({ exp: { platforms: ["macos"] }, pkg: {} }) }, + "../events": { shouldReduceLogs: () => false }, chalk: { gray: (value: string) => value }, "../log": { log() {} }, "../utils/profile": { profile: (fn: unknown) => fn }, @@ -40,7 +42,9 @@ test("Metro readiness does not wait for Expo's dependency checks", async () => { new Function("require", "exports", source)((name: string) => name.endsWith("expo-dev-extension.cjs") ? { ready: (port: number) => { expect(port).toBe(19091); events.push("ready"); } } : modules[name] ?? {}, exports); - await expect(exports.startAsync(root, { dev: true }, {})).rejects.toThrow(serverFails ? "server failed" : "dependency check failed"); + const start = exports.startAsync(root, { dev: true }, {}); + // Expo ignores dependency check failures; later startup steps are not modeled here. + if (serverFails) await expect(start).rejects.toThrow("server failed"); else await start.catch(() => {}); expect(events).toEqual(serverFails ? [] : ["listening", "ready", "dependency check"]); } }); @@ -242,6 +246,42 @@ test("Expo's real key handler routes desktop actions over IPC and retains reload } finally { if (child.exitCode === null) child.kill(); } }, 20000); +test("Expo's localhost server stays reachable on 127.0.0.1", async () => { + const child = spawnProcess(["node", "--require", preload, "-e", "const server = require('node:net').createServer().listen(0, 'localhost', () => { console.log(server.address().address); server.close(); process.disconnect(); });"], { + cwd: root, env: { ...process.env, SPARK_PLATFORM: "macos" }, stdout: "pipe", stderr: "pipe", serialization: "json", ipc() {}, + }); + expect((await new Response(child.stdout).text()).trim()).toBe("127.0.0.1"); + expect(await child.exited).toBe(0); +}); + +test("localhost test harnesses preserve Node options and bind an IPv4 server", async () => { + const environments: NodeJS.ProcessEnv[] = []; + for (const file of readdirSync(path.join(root, "scripts")).filter(file => /^test-.*\.ts$/.test(file))) { + const source = ts.createSourceFile(file, readFileSync(path.join(root, "scripts", file), "utf8"), ts.ScriptTarget.Latest, true); + const visit = (node: ts.Node) => { + if (ts.isCallExpression(node) && node.expression.getText(source) === "spawnProcess" && /['"]--localhost['"]/.test(node.arguments[0]!.getText(source))) { + const options = node.arguments[1] as ts.ObjectLiteralExpression; + const env = options.properties.find(property => property.name?.getText(source) === "env") as ts.PropertyAssignment; + for (const inherited of [undefined, "--title=spark-localhost-test --dns-result-order=verbatim"]) { + const environment = runInContext(`(${env.initializer.getText(source)})`, createContext({ process: { env: { NODE_OPTIONS: inherited } } })); + expect(environment.NODE_OPTIONS, file).toBe(`${inherited ?? ""} --dns-result-order=ipv4first`); + environments.push(environment); + } + } + ts.forEachChild(node, visit); + }; + visit(source); + } + expect(environments).toHaveLength(32); + const child = spawnProcess(["node", "-e", "const http = require('node:http'); const server = http.createServer((req, res) => res.end('reachable')).listen(0, 'localhost', async () => { try { console.log(await (await fetch('http://127.0.0.1:' + server.address().port)).text()); } finally { server.close(); } });"], { + env: { ...process.env, ...environments.at(-1) }, stdout: "pipe", stderr: "pipe", + }); + try { + expect((await new Response(child.stdout).text()).trim()).toBe("reachable"); + expect(await child.exited).toBe(0); + } finally { if (child.exitCode === null) child.kill(); } +}); + test("Metro child processes ignore the inherited preload", async () => { const child = spawnProcess(["node", "--require", preload, "-e", "console.log('worker ready')"], { cwd: os.tmpdir(), env: { ...process.env, SPARK_EXPO_PRELOADED: "1" }, stdout: "pipe", stderr: "pipe", diff --git a/tests/fabric-lifecycle.test.ts b/tests/fabric-lifecycle.test.ts index f4970e6a6..5511c9941 100644 --- a/tests/fabric-lifecycle.test.ts +++ b/tests/fabric-lifecycle.test.ts @@ -30,7 +30,7 @@ test("consumer patch is version-gated and does not modify a hardlinked package c const surface = path.join(pkg, "React/Fabric/Surface/RCTFabricSurface.mm"); mkdirSync(path.dirname(surface), { recursive: true }); const manifest = path.join(pkg, "package.json"); - writeFileSync(manifest, JSON.stringify({ name: "react-native-macos", version: "0.81.7" })); + writeFileSync(manifest, JSON.stringify({ name: "react-native-macos", version: "0.83.0" })); const original = readFileSync(new URL("./fixtures/fabric-surface-lifecycle.mm", import.meta.url), "utf8"); const cache = path.join(root, "cached.mm"); writeFileSync(cache, original); linkSync(cache, surface); installSurfaceLifecyclePatch(root); @@ -39,6 +39,6 @@ test("consumer patch is version-gated and does not modify a hardlinked package c const inode = statSync(surface).ino; installSurfaceLifecyclePatch(root); expect(statSync(surface).ino).toBe(inode); writeFileSync(manifest, JSON.stringify({ name: "react-native-macos", version: "0.82.0" })); - expect(() => installSurfaceLifecyclePatch(root)).toThrow("requires react-native-macos@0.81.7"); + expect(() => installSurfaceLifecyclePatch(root)).toThrow("requires react-native-macos@0.83.0"); } finally { rmSync(root, {recursive: true, force: true}); } }); diff --git a/tests/fixtures/fabric-surface-lifecycle.mm b/tests/fixtures/fabric-surface-lifecycle.mm index 0420bd521..4d370988b 100644 --- a/tests/fixtures/fabric-surface-lifecycle.mm +++ b/tests/fixtures/fabric-surface-lifecycle.mm @@ -1,4 +1,4 @@ -// Lifecycle excerpt from react-native-macos 0.81.7 RCTFabricSurface.mm. +// Lifecycle excerpt from react-native-macos 0.83.0 RCTFabricSurface.mm. // Copyright (c) Meta Platforms, Inc. and affiliates. MIT licensed. std::mutex _surfaceMutex; diff --git a/tests/image-shims.test.ts b/tests/image-shims.test.ts new file mode 100644 index 000000000..bd07ba2e1 --- /dev/null +++ b/tests/image-shims.test.ts @@ -0,0 +1,128 @@ +import { createRequire } from "node:module"; +import { readFileSync } from "node:fs"; +import path from "node:path"; +import React, { act } from "react"; +import ts from "typescript"; +import { afterEach, beforeEach, expect, test, vi } from "vitest"; + +const require = createRequire(import.meta.url); +const { create } = require("react-test-renderer"); +const absoluteFill = { position: "absolute", left: 0, top: 0, right: 0, bottom: 0 }; +const native = { + View: "View", + Image: Object.assign(function Image(props: object) { return React.createElement("Image", props); }, { + getSize: vi.fn(async () => ({ width: 400, height: 200 })), + getSizeWithHeaders: vi.fn(async () => ({ width: 400, height: 200 })), + resolveAssetSource: vi.fn(() => ({ uri: "asset@2x.png", width: 200, height: 100, scale: 2 })), + }), + StyleSheet: { create: (styles: object) => styles, absoluteFill }, +}; + +// Exercise the shipped patch source, with only the native host and image loader mocked. +function shim(packageName: string, file: string): any { + const patch = readFileSync(path.join(import.meta.dirname, `../patches/${packageName}.patch`), "utf8"); + const section = patch.split(`+++ b/src/${file}\n`)[1].split("diff --git ")[0]; + const source = section.split("\n").filter(line => line.startsWith("+")).map(line => line.slice(1)).join("\n"); + const directory = path.join(import.meta.dirname, "../node_modules", packageName.split("@")[0], "src"); + const evaluate = (source: string, filename: string): any => { + const module = { exports: {} }; + const code = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, jsx: ts.JsxEmit.React, esModuleInterop: true } }).outputText; + new Function("require", "module", "exports", code)((name: string) => { + if (name === "react-native") return native; + if (name.startsWith(".")) { + const target = path.resolve(path.dirname(filename), `${name}.ts`); + return evaluate(readFileSync(target, "utf8"), target); + } + return require(name); + }, module, module.exports); + return module.exports; + }; + return evaluate(source, path.join(directory, file)); +} + +let rendered: any; +beforeEach(() => { + (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; + native.Image.getSize.mockResolvedValue({ width: 400, height: 200 }); + native.Image.getSizeWithHeaders.mockReset().mockResolvedValue({ width: 400, height: 200 }); + const error = console.error; + vi.spyOn(console, "error").mockImplementation((...args) => { + if (!String(args[0]).startsWith("react-test-renderer is deprecated")) error(...args); + }); +}); +afterEach(async () => { + if (rendered) await act(async () => { rendered.unmount(); rendered = undefined; }); + vi.restoreAllMocks(); +}); +async function mount(component: any, props: object) { + await act(async () => { rendered = create(React.createElement(component, props)); }); +} +const images = () => rendered.root.findAllByType("Image"); +const style = (value: any): any => Object.assign({}, ...(Array.isArray(value) ? value.flat(Infinity).filter(Boolean) : [value])); + +test("FastImageBackground routes layout, test and accessibility props to the container and imageProps to the image", async () => { + const { FastImageBackground } = shim("react-native-fast-image@10.9.9", "index.macos.tsx"); + const onLayout = vi.fn(), onLoad = vi.fn(); + await mount(FastImageBackground, { testID: "background", onLayout, accessibilityLabel: "container", source: { uri: "photo" }, onLoad, + imageProps: { testID: "image", accessibilityLabel: "photo", onLoad: undefined }, children: "overlay" }); + const views = rendered.root.findAllByType("View"); + expect(views[0].props).toMatchObject({ testID: "background", onLayout, accessibilityLabel: "container" }); + expect(views[1].props).toMatchObject({ testID: "image", accessibilityLabel: "photo" }); + expect(views[1].props.onLayout).toBeUndefined(); + await act(async () => images()[0].props.onLoad({ nativeEvent: { source: { uri: "photo", width: 1, height: 1 } } })); + expect(onLoad).toHaveBeenCalledWith({ nativeEvent: { width: 400, height: 200 } }); +}); + +test("FastImage configureCache resolves the same explicit unsupported result as cache siblings", async () => { + const { default: FastImage } = shim("react-native-fast-image@10.9.9", "index.macos.tsx"); + await expect(FastImage.configureCache()).resolves.toEqual({ ok: false, error: "FastImage.configureCache is not supported on macOS" }); +}); + +test.each([7, { uri: "photo", width: 800, height: 600, scale: 2, headers: { Accept: "image/png" } }])( + "FastImage strips intrinsic layout dimensions from main and placeholder sources: %j", async source => { + const { default: FastImage } = shim("react-native-fast-image@10.9.9", "index.macos.tsx"); + await mount(FastImage, { source, placeholder: source, style: { width: 120, height: 120 } }); + for (const image of images()) { + expect(image.props.source).not.toHaveProperty("width"); + expect(image.props.source).not.toHaveProperty("height"); + expect(style(image.props.style)).toMatchObject(absoluteFill); + } + }, +); + +test("expo-image cover/contain and bundled source dimensions leave RN Image filling its container", async () => { + const { default: ExpoImage } = shim("expo-image@55.0.11", "ExpoImage.macos.tsx"); + await mount(ExpoImage, { source: [{ uri: "asset@2x.png", width: 800, height: 600, scale: 2 }], placeholder: [{ uri: "preview", width: 80, height: 60 }], + contentFit: "cover", placeholderContentFit: "contain", style: { width: 120, height: 120 } }); + expect(images().map((image: any) => image.props.resizeMode)).toEqual(["contain", "cover"]); + for (const image of images()) { + expect(image.props.source).not.toHaveProperty("width"); + expect(image.props.source).not.toHaveProperty("height"); + expect(style(image.props.style)).toEqual(absoluteFill); + } +}); + +test("expo-image none centers natural dimensions for an oversized image and placeholder instead of shrinking", async () => { + const { default: ExpoImage } = shim("expo-image@55.0.11", "ExpoImage.macos.tsx"); + await mount(ExpoImage, { source: [{ uri: "photo" }], placeholder: [{ uri: "preview", scale: 2 }], contentFit: "none", placeholderContentFit: "none", + style: { width: 120, height: 120 } }); + expect(images().map((image: any) => style(image.props.style))).toEqual([ + { position: "absolute", width: 200, height: 100, left: "50%", top: "50%", right: undefined, bottom: undefined, transform: [{ translateX: -100 }, { translateY: -50 }] }, + { position: "absolute", width: 400, height: 200, left: "50%", top: "50%", right: undefined, bottom: undefined, transform: [{ translateX: -200 }, { translateY: -100 }] }, + ]); + expect(style(rendered.root.findByType("View").props.style)).toMatchObject({ overflow: "hidden", width: 120, height: 120 }); +}); + + +test("expo-image none sizes authenticated sources and ignores a previous source's late size", async () => { + const { default: ExpoImage } = shim("expo-image@55.0.11", "ExpoImage.macos.tsx"); + let resolvePrevious!: (size: { width: number; height: number }) => void; + native.Image.getSizeWithHeaders.mockImplementationOnce(() => new Promise(resolve => { resolvePrevious = resolve; })); + const headers = { Authorization: "fixture" }; + await mount(ExpoImage, { source: [{ uri: "previous", headers }], contentFit: "none" }); + await act(async () => rendered.update(React.createElement(ExpoImage, { source: [{ uri: "next", headers }], contentFit: "none" }))); + expect(native.Image.getSizeWithHeaders).toHaveBeenCalledWith("next", headers); + expect(style(images()[0].props.style)).toMatchObject({ width: 400, height: 200 }); + await act(async () => resolvePrevious({ width: 900, height: 800 })); + expect(style(images()[0].props.style)).toMatchObject({ width: 400, height: 200 }); +}); diff --git a/tests/keyboard-events.test.ts b/tests/keyboard-events.test.ts index 52b9318eb..a1eb6f385 100644 --- a/tests/keyboard-events.test.ts +++ b/tests/keyboard-events.test.ts @@ -27,7 +27,7 @@ test("keyboard patch validates all sources before writes and preserves package c const dependency = path.join(root, "node_modules/react-native-macos"); mkdirSync(path.join(dependency, "React/Views"), { recursive: true }); const manifest = path.join(dependency, "package.json"); - writeFileSync(manifest, JSON.stringify({ name: "react-native-macos", version: "0.81.7" })); + writeFileSync(manifest, JSON.stringify({ name: "react-native-macos", version: "0.83.0" })); for (const [index, file] of files.entries()) { const cache = path.join(root, `cache-${index}`); writeFileSync(cache, pristine(file)); linkSync(cache, path.join(dependency, file)); @@ -46,6 +46,6 @@ test("keyboard patch validates all sources before writes and preserves package c expect(statSync(installed).ino).toBe(inode); } writeFileSync(manifest, JSON.stringify({ name: "react-native-macos", version: "0.82.0" })); - expect(() => installKeyboardEventsPatch(root)).toThrow("requires react-native-macos@0.81.7"); + expect(() => installKeyboardEventsPatch(root)).toThrow("requires react-native-macos@0.83.0"); } finally { rmSync(root, { recursive: true, force: true }); } }); diff --git a/tests/macos-entries.test.ts b/tests/macos-entries.test.ts new file mode 100644 index 000000000..fe5043212 --- /dev/null +++ b/tests/macos-entries.test.ts @@ -0,0 +1,12 @@ +import { existsSync, readdirSync } from "node:fs"; +import path from "node:path"; +import { expect, test } from "vitest"; + +test("every iOS source has an explicit macOS counterpart", () => { + // Expo SDK 55 falls back to iOS before native/generic files on macOS. + const iosFiles = readdirSync("packages", { recursive: true, encoding: "utf8" }) + .filter(file => /\.ios\.[cm]?[jt]sx?$/.test(file)); + expect(iosFiles.length).toBeGreaterThan(0); + const missing = iosFiles.filter(file => !existsSync(path.join("packages", file.replace(".ios.", ".macos.")))); + expect(missing, "Add a macOS re-export of the intended desktop/generic implementation").toEqual([]); +}); diff --git a/tests/macos-host.test.ts b/tests/macos-host.test.ts new file mode 100644 index 000000000..094b17a02 --- /dev/null +++ b/tests/macos-host.test.ts @@ -0,0 +1,58 @@ +import { expect, test } from "vitest"; +import { createRequire } from "node:module"; +import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +const require = createRequire(import.meta.url); +const xcode = require("xcode"); +const { installHostFiles, installHostProject } = require("../packages/config-plugin/macos-host.cjs"); +const { removeExpoApplicationAlias } = require("../packages/config-plugin/native-patches.cjs"); +const template = path.join(path.dirname(require.resolve("expo-desktop-template-bare-minimum/package.json")), "macos"); + +test("the SwiftUI template host is replaced by Spark's AppKit host files", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "spark-macos-host-")); + try { + cpSync(template, root, { recursive: true }); + installHostFiles(root); + const host = path.join(root, "HelloWorld-macOS"); + expect(existsSync(path.join(host, "AppDelegate.swift"))).toBe(false); + expect(readFileSync(path.join(host, "AppDelegate.mm"), "utf8")).toContain("jsBundleURLForBundleRoot"); + for (const name of ["AppDelegate.h", "main.m", "Main.storyboard"]) expect(existsSync(path.join(host, name))).toBe(true); + installHostFiles(root); + } finally { rmSync(root, { recursive: true, force: true }); } +}); + +test("the macOS target compiles the AppKit host and its menu storyboard", () => { + const project = xcode.project(path.join(template, "HelloWorld.xcodeproj/project.pbxproj")).parseSync(); + installHostProject(project); + const contents = project.writeSync(); + expect(contents).not.toContain("AppDelegate.swift"); + installHostProject(project); + expect(project.writeSync()).toBe(contents); + const objects = project.hash.project.objects; + const macOS = Object.values(objects.PBXNativeTarget).find((target: any) => target.name?.replaceAll('"', "") === "HelloWorld-macOS") as any; + const phase = (isa: string) => macOS.buildPhases.map((entry: any) => objects[isa][entry.value]).find(Boolean); + const files = (isa: string) => phase(isa).files.map((file: any) => objects.PBXFileReference[objects.PBXBuildFile[file.value].fileRef]); + expect(files("PBXSourcesBuildPhase").map((file: any) => file.path.replaceAll('"', "")).sort()).toEqual(["AppDelegate.mm", "main.m"]); + expect(files("PBXResourcesBuildPhase").find((file: any) => file.path === '"Main.storyboard"').lastKnownFileType).toBe("file.storyboard"); + const group = Object.values(objects.PBXGroup).find((entry: any) => entry.path?.replaceAll('"', "") === "HelloWorld-macOS") as any; + expect(group.children.map((child: any) => child.comment)).toEqual(expect.arrayContaining(["AppDelegate.mm", "main.m", "Main.storyboard"])); +}); + +test("Expo's duplicate UIApplication alias is removed before native fingerprinting", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "spark-expo-alias-")); + try { + const write = (file: string, contents: string) => { mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); writeFileSync(path.join(root, file), contents); }; + write("node_modules/react-native-macos/package.json", "{}"); + write("node_modules/react-native-macos/ReactApple/Libraries/RCTUIKit/RCTUIKitCompat.h", "@compatibility_alias UIApplication NSApplication;\n"); + write("node_modules/expo-modules-core/package.json", "{}"); + const header = "#import <Foundation/Foundation.h>\n\n#ifndef UIApplication\n@compatibility_alias UIApplication NSApplication;\n#endif\n"; + write("node_modules/expo-modules-core/ios/Platform/Platform.h", header); + const roots = [path.join(root, "node_modules/react-native-macos"), path.join(root, "node_modules/expo-modules-core")]; + removeExpoApplicationAlias(...roots); + const patched = readFileSync(path.join(root, "node_modules/expo-modules-core/ios/Platform/Platform.h"), "utf8"); + expect(patched).toBe("#import <Foundation/Foundation.h>\n\n\n"); + removeExpoApplicationAlias(...roots); + expect(readFileSync(path.join(root, "node_modules/expo-modules-core/ios/Platform/Platform.h"), "utf8")).toBe(patched); + } finally { rmSync(root, { recursive: true, force: true }); } +}); diff --git a/tests/native-dependency-patches.test.ts b/tests/native-dependency-patches.test.ts index 1aa5ec375..b148a4e5b 100644 --- a/tests/native-dependency-patches.test.ts +++ b/tests/native-dependency-patches.test.ts @@ -313,3 +313,71 @@ test.each(["react-native-enriched-markdown", "react-native-enriched-html"])("%s prepareNativePatches(root, installedPackages(root)); } finally { rmSync(root, { recursive: true, force: true }); } }); + +test("optional pins patch only the app's own installs of the pinned version, without marking them SDK packages", () => { + const { root, spark, packages } = fixture(); + const name = "react-native-mmkv"; + try { + const directory = path.join(packages, name); + mkdirSync(directory, { recursive: true }); + writeFileSync(path.join(directory, "package.json"), JSON.stringify({ name, version: "4.3.2" }, null, 2) + "\n"); + writeFileSync(path.join(directory, "Native.podspec"), "upstream\n"); + writeFileSync(path.join(spark, "patches/dependencies.json"), JSON.stringify({ [name]: { version: "4.3.2", integrity: "sha512-fixture", patch: `${name}.patch`, optional: true } })); + writeFileSync(path.join(spark, "patches", `${name}.patch`), readFileSync(path.join(spark, "patches/camera", `${names[0]}.patch`))); + prepareNativePatches(root); + expect(readFileSync(path.join(directory, "Native.podspec"), "utf8")).toBe("upstream\n"); + const app = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")); + writeFileSync(path.join(root, "package.json"), JSON.stringify({ ...app, dependencies: { ...app.dependencies, [name]: "4.3.2" } })); + expect(prepareNativePatches(root)[name]).toEqual({ root: realpathSync(directory) }); + expect(readFileSync(path.join(directory, "macos/View.swift"), "utf8")).toBe("AppKit\n"); + const receipt = JSON.parse(readFileSync(path.join(directory, "package.json"), "utf8")).spark; + expect(receipt).toMatchObject({ dependencyPatch: { version: "4.3.2" } }); + expect(receipt.sdk).toBeUndefined(); + // Another version keeps upstream behavior instead of failing the app's native preparation. + rmSync(directory, { recursive: true }); + mkdirSync(directory, { recursive: true }); + writeFileSync(path.join(directory, "package.json"), JSON.stringify({ name, version: "4.2.0" }, null, 2) + "\n"); + writeFileSync(path.join(directory, "Native.podspec"), "upstream\n"); + expect(prepareNativePatches(root)[name]).toBeUndefined(); + expect(readFileSync(path.join(directory, "Native.podspec"), "utf8")).toBe("upstream\n"); + } finally { rmSync(root, { recursive: true, force: true }); } +}); + +test("package.json hunks apply to tab-indented manifests and still converge after the receipt rewrites them", () => { + const { root, spark, packages } = fixture(); + const name = "tabbed"; + try { + const directory = path.join(packages, name); + mkdirSync(directory, { recursive: true }); + writeFileSync(path.join(directory, "package.json"), JSON.stringify({ name, version: "1.0.0", codegenConfig: { name: "spec" } }, null, "\t") + "\n"); + const app = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")); + writeFileSync(path.join(root, "package.json"), JSON.stringify({ ...app, dependencies: { ...app.dependencies, [name]: "1.0.0" } })); + writeFileSync(path.join(spark, "patches/dependencies.json"), JSON.stringify({ [name]: { version: "1.0.0", integrity: "sha512-fixture", patch: `${name}.patch`, optional: true } })); + writeFileSync(path.join(spark, "patches", `${name}.patch`), [ + "diff --git a/package.json b/package.json", "--- a/package.json", "+++ b/package.json", "@@ -3,5 +3,6 @@", + ' "version": "1.0.0",', ' "codegenConfig": {', '- "name": "spec"', '+ "name": "spec",', '+ "ios": {}', " }", " }", "", + ].join("\n")); + prepareNativePatches(root); + const first = readFileSync(path.join(directory, "package.json"), "utf8"); + expect(JSON.parse(first).codegenConfig).toEqual({ name: "spec", ios: {} }); + prepareNativePatches(root); + expect(readFileSync(path.join(directory, "package.json"), "utf8")).toBe(first); + } finally { rmSync(root, { recursive: true, force: true }); } +}); + +test("optional pins patch packages whose exports map hides package.json", () => { + const { root, spark, packages } = fixture(); + const name = "exported"; + try { + const directory = path.join(packages, name); + mkdirSync(directory, { recursive: true }); + writeFileSync(path.join(directory, "package.json"), JSON.stringify({ name, version: "1.0.0", exports: { ".": "./index.js" } }, null, 2) + "\n"); + writeFileSync(path.join(directory, "Native.podspec"), "upstream\n"); + const app = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")); + writeFileSync(path.join(root, "package.json"), JSON.stringify({ ...app, dependencies: { ...app.dependencies, [name]: "1.0.0" } })); + writeFileSync(path.join(spark, "patches/dependencies.json"), JSON.stringify({ [name]: { version: "1.0.0", integrity: "sha512-fixture", patch: `${name}.patch`, optional: true } })); + writeFileSync(path.join(spark, "patches", `${name}.patch`), ["diff --git a/Native.podspec b/Native.podspec", "--- a/Native.podspec", "+++ b/Native.podspec", "@@ -1 +1 @@", "-upstream", "+patched", ""].join("\n")); + prepareNativePatches(root); + expect(readFileSync(path.join(directory, "Native.podspec"), "utf8")).toBe("patched\n"); + } finally { rmSync(root, { recursive: true, force: true }); } +}); diff --git a/tests/native-graph.test.ts b/tests/native-graph.test.ts index 8e08c2a8d..491e4dcf8 100644 --- a/tests/native-graph.test.ts +++ b/tests/native-graph.test.ts @@ -8,6 +8,7 @@ import { hashFiles, dependencyStamp, nativePackages, + lacksMacOSPod, nativePreparationFingerprint, installedPackages, localSigningIdentity, @@ -351,3 +352,26 @@ test.each(["react-native-enriched-markdown", "react-native-enriched-html", "reac expect(installedPackages(root).map(pkg => pkg.name)).toContain(name); } finally { rmSync(root, { recursive: true, force: true }); } }); + +test("codegen libraries whose podspecs list platforms without macOS are left out of macOS codegen", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "spark-macos-pod-")); + onTestFinished(() => rmSync(root, { recursive: true, force: true })); + const pkg = (name: string, podspec: string | null, codegen = true) => { + const directory = path.join(root, name); + mkdirSync(directory); + if (podspec !== null) writeFileSync(path.join(directory, "Lib.podspec"), podspec); + return { name, root: directory, json: codegen ? { codegenConfig: { name: "Spec" } } : {} }; + }; + expect(lacksMacOSPod(pkg("ios-only", 's.platforms = { :ios => "15.1", :tvos => "15.1" }'))).toBe(true); + expect(lacksMacOSPod(pkg("with-macos", 's.platforms = { :ios => "15.1", :osx => "11.0" }'))).toBe(false); + expect(lacksMacOSPod(pkg("with-macos-new-syntax", "s.platforms = { ios: '11.0', osx: '10.15' }"))).toBe(false); + expect(lacksMacOSPod(pkg("singular-ios", "s.platform = :ios, '13.4'"))).toBe(true); + expect(lacksMacOSPod(pkg("singular-macos", "s.platform = :osx, '14.0'"))).toBe(false); + expect(lacksMacOSPod(pkg("macos-in-comment", "s.platforms = { ios: '15.0' }\n# macos: not built"))).toBe(true); + expect(lacksMacOSPod(pkg("osx-deployment-target", "s.platforms = { :ios => '15.1' }\ns.osx.deployment_target = '14.0'"))).toBe(false); + expect(lacksMacOSPod(pkg("computed-platforms", "s.platforms = min_supported_versions"))).toBe(false); + expect(lacksMacOSPod(pkg("all-platforms", "s.source_files = 'ios/**/*'"))).toBe(false); + expect(lacksMacOSPod(pkg("no-codegen", 's.platforms = { :ios => "15.1" }', false))).toBe(false); + expect(lacksMacOSPod(pkg("no-podspec", null))).toBe(false); + expect(lacksMacOSPod(pkg("react-native-macos", 's.platforms = { :ios => "15.1" }'))).toBe(false); +}); diff --git a/tests/package-distribution.test.ts b/tests/package-distribution.test.ts index 3dd80eb40..b42c5080e 100644 --- a/tests/package-distribution.test.ts +++ b/tests/package-distribution.test.ts @@ -136,13 +136,17 @@ test("single Spark archive resolves public exports and preserves private native mkdirSync(pluginStub); writeFileSync(path.join(pluginStub, "package.json"), JSON.stringify({ name: "expo-desktop-config-plugins", main: "index.cjs" })); writeFileSync(path.join(pluginStub, "index.cjs"), ` - exports.withEntitlementsPlist = exports.withAppDelegate = exports.withPodfile = config => config; + exports.withEntitlementsPlist = exports.withAppDelegate = exports.withPodfile = exports.withXcodeProject = config => config; exports.generatedInfo = {}; exports.withInfoPlist = (config, action) => { Object.assign(exports.generatedInfo, action({ modRequest: { projectRoot: ${JSON.stringify(app)} }, modResults: {} }).modResults); return config; }; `); + const configPluginsStub = path.join(app, "node_modules/@expo/config-plugins"); + mkdirSync(configPluginsStub, { recursive: true }); + writeFileSync(path.join(configPluginsStub, "package.json"), JSON.stringify({ name: "@expo/config-plugins", main: "index.cjs" })); + writeFileSync(path.join(configPluginsStub, "index.cjs"), "exports.withMod = config => config; exports.IOSConfig = {};"); const plugin = spawnProcess([process.execPath, "-e", ` const { createRequire } = require("node:module"); const req = createRequire(${JSON.stringify(path.join(app, "package.json"))}); @@ -154,7 +158,7 @@ test("single Spark archive resolves public exports and preserves private native expect(JSON.parse(pluginOut).SparkFrameworkVersion).toBe(manifest.version); const singletonPeers = ["expo", "react", "react-native", "react-dom"]; for (const name of singletonPeers) expect(manifest.dependencies).not.toHaveProperty(name); - expect(manifest.peerDependencies).toMatchObject({ expo: "54.0.37", react: "^19.1.0", "react-native": "0.81.6", "react-dom": "^19.1.0" }); + expect(manifest.peerDependencies).toMatchObject({ expo: "55.0.32", react: "^19.2.0", "react-native": "0.83.10", "react-dom": "^19.2.0" }); expect(manifest.peerDependenciesMeta["react-dom"]).toEqual({ optional: true }); expect(manifest.dependencies["@react-native-runtimes/core"]).toBe("0.1.0-alpha.2"); expect(manifest.dependencies["react-native-nitro-modules"]).toBe("0.37.0"); @@ -313,8 +317,8 @@ test("Create Spark archive is peer-free and runs without installing the framewor for (const variant of ["blank-typescript", "windows", "universal"]) { expect(existsSync(path.join(destination, "templates", variant, "README.md"))).toBe(true); const template = JSON.parse(readFileSync(path.join(destination, "templates", variant, "package.json"), "utf8")); - expect(template.dependencies).toMatchObject({ "@legendapp/spark": VERSION, react: "19.1.4", "react-native": "0.81.6", expo: "54.0.37" }); - expect(template.overrides).toMatchObject({ react: "19.1.4", "react-native": "0.81.6", "react-native-macos": "0.81.7" }); + expect(template.dependencies).toMatchObject({ "@legendapp/spark": VERSION, react: "19.2.0", "react-native": "0.83.10", expo: "55.0.32" }); + expect(template.overrides).toMatchObject({ react: "19.2.0", "react-native": "0.83.10", "react-native-macos": "0.83.0" }); } } finally { rmSync(root, { recursive: true, force: true }); } }, 120_000); diff --git a/tests/packed-consumer.integration.ts b/tests/packed-consumer.integration.ts index c6ad15780..c233e1d90 100644 --- a/tests/packed-consumer.integration.ts +++ b/tests/packed-consumer.integration.ts @@ -89,12 +89,12 @@ function manifest(archive: string, react: string) { private: true, dependencies: { "@legendapp/spark": `file:${archive}`, - expo: "54.0.37", + expo: "55.0.32", react, - ...(react === "19.1.4" ? { "react-dom": react } : {}), - "react-native": "0.81.6", + ...(react === "19.2.0" ? { "react-dom": react } : {}), + "react-native": "0.83.10", }, - devDependencies: { "@types/node": "24.13.6", "@types/react": "19.1.10", typescript: "5.9.3" }, + devDependencies: { "@types/node": "24.13.6", "@types/react": "19.2.18", typescript: "5.9.3" }, }; } try { @@ -106,14 +106,14 @@ try { const archiveName = path.basename(archive); const archiveHash = createHash("sha256").update(readFileSync(archive)).digest("hex"); const consumer = path.join(root, "consumer"); mkdirSync(consumer); - writeFileSync(path.join(consumer, "package.json"), JSON.stringify(manifest(archive, "19.1.4"), null, 2) + "\n"); + writeFileSync(path.join(consumer, "package.json"), JSON.stringify(manifest(archive, "19.2.0"), null, 2) + "\n"); const install = await runNpm(consumer, managerInstallArgs("npm")); if (install.status !== 0) throw new Error(`Compatible packed consumer install failed (${install.status}):\n${install.output}`); const require = createRequire(path.join(consumer, "package.json")); const pkg = require("@legendapp/spark/package.json"); - if (pkg.version !== VERSION || pkg.peerDependencies.react !== "^19.1.0" || pkg.peerDependencies["react-native"] !== "0.81.6") throw new Error("Installed packed Spark peer baseline differs from the release manifest"); - if (pkg.peerDependencies["react-dom"] !== "^19.1.0" || pkg.peerDependenciesMeta?.["react-dom"]?.optional !== true) throw new Error("React DOM must remain an optional public peer"); + if (pkg.version !== VERSION || pkg.peerDependencies.react !== "^19.2.0" || pkg.peerDependencies["react-native"] !== "0.83.10") throw new Error("Installed packed Spark peer baseline differs from the release manifest"); + if (pkg.peerDependencies["react-dom"] !== "^19.2.0" || pkg.peerDependenciesMeta?.["react-dom"]?.optional !== true) throw new Error("React DOM must remain an optional public peer"); if (pkg.packageManager) throw new Error("The published SDK must not impose the repository package manager on consumers"); await verifyCliRuntimes(consumer); const cliToolchain = verifyCliToolchain(consumer); @@ -183,7 +183,7 @@ try { } const directory = path.join(root, `${manager}-consumer`); mkdirSync(directory); try { - writeFileSync(path.join(directory, "package.json"), JSON.stringify(manifest(archive, "19.1.4"), null, 2) + "\n"); + writeFileSync(path.join(directory, "package.json"), JSON.stringify(manifest(archive, "19.2.0"), null, 2) + "\n"); console.log(`Installing packed SDK with ${manager}@${version.output.trim()} (lifecycle scripts disabled).`); const managerInstall = await runManager(manager, directory, managerInstallArgs(manager)); if (managerInstall.status !== 0) throw new Error(`${manager}@${version.output.trim()} failed to install the packed SDK graph:\n${managerInstall.output}`); diff --git a/tests/refresh-local-packages.test.ts b/tests/refresh-local-packages.test.ts index be53805a1..05ca47800 100644 --- a/tests/refresh-local-packages.test.ts +++ b/tests/refresh-local-packages.test.ts @@ -25,9 +25,9 @@ for (const manager of ["npm", "pnpm", "yarn", "bun"] as const) { const pkg = JSON.parse(first); const pins = manager === "yarn" ? pkg.resolutions : manager === "pnpm" ? parse(readFileSync(path.join(root, "pnpm-workspace.yaml"), "utf8")).overrides : pkg.overrides; expect(pkg.dependencies["expo-desktop"]).toBe("1.0.6"); - expect(pkg.dependencies["expo-desktop-template-bare-minimum"]).toBe("54.81.6"); + expect(pkg.dependencies["expo-desktop-template-bare-minimum"]).toBe("55.83.10"); expect(pins["expo-desktop-config-plugins"]).toBe("1.3.2"); - expect(pins["react-native-macos"]).toBe("0.81.7"); + expect(pins["react-native-macos"]).toBe("0.83.0"); expect(pkg.dependencies["@legendapp/spark"]).toBe(pins["@legendapp/spark"]); await refreshLocalPackages(root, manifest, manager); expect(readFileSync(path.join(root, "package.json"), "utf8")).toBe(first); diff --git a/tests/rtl-scroll-view-native.test.ts b/tests/rtl-scroll-view-native.test.ts index cf7d12b54..37d20c4d2 100644 --- a/tests/rtl-scroll-view-native.test.ts +++ b/tests/rtl-scroll-view-native.test.ts @@ -33,7 +33,7 @@ test("RTL scroll patch install checks the version and preserves package cache ha const dependency = path.join(root, "node_modules/react-native-macos"), cache = path.join(root, "cache.mm"); const installed = path.join(dependency, "React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm"); mkdirSync(path.dirname(installed), { recursive: true }); - writeFileSync(path.join(dependency, "package.json"), JSON.stringify({ name: "react-native-macos", version: "0.81.7" })); + writeFileSync(path.join(dependency, "package.json"), JSON.stringify({ name: "react-native-macos", version: "0.83.0" })); writeFileSync(cache, pristine); linkSync(cache, installed); installRTLScrollViewPatch(root); expect(readFileSync(cache, "utf8")).toBe(pristine); @@ -41,7 +41,7 @@ test("RTL scroll patch install checks the version and preserves package cache ha const inode = statSync(installed).ino; installRTLScrollViewPatch(root); expect(statSync(installed).ino).toBe(inode); writeFileSync(path.join(dependency, "package.json"), JSON.stringify({ name: "react-native-macos", version: "0.82.0" })); - expect(() => installRTLScrollViewPatch(root)).toThrow("requires react-native-macos@0.81.7"); + expect(() => installRTLScrollViewPatch(root)).toThrow("requires react-native-macos@0.83.0"); } finally { rmSync(root, { recursive: true, force: true }); } }); diff --git a/tests/rtl-text-alignment-native.test.ts b/tests/rtl-text-alignment-native.test.ts index bf149c2f5..556e54263 100644 --- a/tests/rtl-text-alignment-native.test.ts +++ b/tests/rtl-text-alignment-native.test.ts @@ -24,7 +24,7 @@ test("RTL text alignment patch install checks the version and preserves package const dependency = path.join(root, "node_modules/react-native-macos"), cache = path.join(root, "cache.mm"); const installed = path.join(dependency, "ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm"); mkdirSync(path.dirname(installed), { recursive: true }); - writeFileSync(path.join(dependency, "package.json"), JSON.stringify({ name: "react-native-macos", version: "0.81.7" })); + writeFileSync(path.join(dependency, "package.json"), JSON.stringify({ name: "react-native-macos", version: "0.83.0" })); writeFileSync(cache, pristine); linkSync(cache, installed); installRTLTextAlignmentPatch(root); expect(readFileSync(cache, "utf8")).toBe(pristine); @@ -32,7 +32,7 @@ test("RTL text alignment patch install checks the version and preserves package const inode = statSync(installed).ino; installRTLTextAlignmentPatch(root); expect(statSync(installed).ino).toBe(inode); writeFileSync(path.join(dependency, "package.json"), JSON.stringify({ name: "react-native-macos", version: "0.82.0" })); - expect(() => installRTLTextAlignmentPatch(root)).toThrow("requires react-native-macos@0.81.7"); + expect(() => installRTLTextAlignmentPatch(root)).toThrow("requires react-native-macos@0.83.0"); } finally { rmSync(root, { recursive: true, force: true }); } }); diff --git a/tests/rtl-text-input-alignment-native.test.ts b/tests/rtl-text-input-alignment-native.test.ts index c889e4778..bf43bdd4d 100644 --- a/tests/rtl-text-input-alignment-native.test.ts +++ b/tests/rtl-text-input-alignment-native.test.ts @@ -27,7 +27,7 @@ test("RTL TextInput alignment patch install checks the version and preserves pac const dependency = path.join(root, "node_modules/react-native-macos"), cache = path.join(root, "cache.mm"); const installed = path.join(dependency, file); mkdirSync(path.dirname(installed), { recursive: true }); - writeFileSync(path.join(dependency, "package.json"), JSON.stringify({ name: "react-native-macos", version: "0.81.7" })); + writeFileSync(path.join(dependency, "package.json"), JSON.stringify({ name: "react-native-macos", version: "0.83.0" })); writeFileSync(cache, pristine); linkSync(cache, installed); installRTLTextInputAlignmentPatch(root); expect(readFileSync(cache, "utf8")).toBe(pristine); @@ -35,7 +35,7 @@ test("RTL TextInput alignment patch install checks the version and preserves pac const inode = statSync(installed).ino; installRTLTextInputAlignmentPatch(root); expect(statSync(installed).ino).toBe(inode); writeFileSync(path.join(dependency, "package.json"), JSON.stringify({ name: "react-native-macos", version: "0.82.0" })); - expect(() => installRTLTextInputAlignmentPatch(root)).toThrow("requires react-native-macos@0.81.7"); + expect(() => installRTLTextInputAlignmentPatch(root)).toThrow("requires react-native-macos@0.83.0"); } finally { rmSync(root, { recursive: true, force: true }); } }); diff --git a/tests/sdk-api.test.ts b/tests/sdk-api.test.ts index f1db238ed..653547f5d 100644 --- a/tests/sdk-api.test.ts +++ b/tests/sdk-api.test.ts @@ -40,10 +40,10 @@ const tray = await import("../packages/tray/src/index.ts"); const updates = await import("../packages/updates/src/index.ts"); const app = await import("../packages/desktop-app/src/index.ts"); const files = await import("../packages/file-system/src/index.ts"); -const clipboard = await import("../packages/clipboard/src/index.ts"); -const links = await import("../packages/desktop-links/src/index.ts"); +const clipboard = await import("../packages/clipboard/src/index.macos.ts"); +const links = await import("../packages/desktop-links/src/index.macos.ts"); const documents = await import("../packages/documents/src/requests.ts"); -const secureStore = await import("../packages/secure-storage/src/index.ts"); +const secureStore = await import("../packages/secure-storage/src/index.macos.ts"); const shortcuts = await import("../packages/desktop-shortcuts/src/index.ts"); const menus = await import("../packages/native-menu/src/index.ts"); const context = await import("../packages/context-menu/src/index.ts"); diff --git a/tests/templates.test.ts b/tests/templates.test.ts index 64b47f741..c9dc79551 100644 --- a/tests/templates.test.ts +++ b/tests/templates.test.ts @@ -59,22 +59,22 @@ test("all templates retain the tested desktop matrix", () => { for (const folder of ["blank-typescript", "windows", "universal"]) { const pkg = JSON.parse(readFileSync(path.join(templates, folder, "package.json"), "utf8")); expect(pkg.dependencies["expo-desktop"]).toBe("1.0.6"); - expect(pkg.dependencies["expo-desktop-template-bare-minimum"]).toBe("54.81.6"); + expect(pkg.dependencies["expo-desktop-template-bare-minimum"]).toBe("55.83.10"); expect(pkg.dependencies["expo-desktop-prebuild-config"]).toBe("1.1.3"); expect(pkg.dependencies["expo-desktop-config-plugins"]).toBe("1.3.2"); - expect(pkg.dependencies["expo-desktop-metro-config"]).toBe("54.81.3"); - expect(pkg.overrides["expo-desktop-modules-core"]).toBe("54.0.14"); - expect(pkg.overrides["expo-desktop-stubs"]).toBe("54.0.15"); + expect(pkg.dependencies["expo-desktop-metro-config"]).toBe("55.83.3"); + expect(pkg.overrides["expo-desktop-modules-core"]).toBe("55.0.2"); + expect(pkg.overrides["expo-desktop-stubs"]).toBe("55.0.2"); const { expo } = JSON.parse(readFileSync(path.join(templates, folder, "app.json"), "utf8")); expect(expo.macos).toMatchObject({ version: expo.version, buildNumber: "1" }); - expect(pkg.dependencies.expo).toBe("54.0.37"); + expect(pkg.dependencies.expo).toBe("55.0.32"); const yarn = structuredClone(pkg); applyOverrides(yarn, {}, "yarn"); expect(yarn.resolutions.expo).toBe(pkg.dependencies.expo); - expect(pkg.overrides["@expo/cli"]).toBe("54.0.27"); - expect(pkg.dependencies["react-native"]).toBe("0.81.6"); - expect(pkg.overrides["react-native-macos"]).toBe("0.81.7"); - expect(pkg.overrides["react-native-windows"]).toBe("0.81.35"); + expect(pkg.overrides["@expo/cli"]).toBe("55.0.37"); + expect(pkg.dependencies["react-native"]).toBe("0.83.10"); + expect(pkg.overrides["react-native-macos"]).toBe("0.83.0"); + expect(pkg.overrides["react-native-windows"]).toBe("0.83.2"); expect(pkg.scripts.postinstall).toBe("node node_modules/@legendapp/spark/init-template.cjs"); for (const name of ["@legendapp/spark", "react-native-enriched-markdown", "fsevents"]) { expect(pkg.allowScripts[name]).toBe(true); diff --git a/tests/test-driver-native.test.ts b/tests/test-driver-native.test.ts index 6041ccb3d..b32bafaf7 100644 --- a/tests/test-driver-native.test.ts +++ b/tests/test-driver-native.test.ts @@ -5,15 +5,18 @@ import path from "node:path"; const darwin = process.platform === "darwin"; const root = path.resolve(import.meta.dirname, ".."); -let directory = "", harness = ""; +let directory = "", harness = "", pointer = ""; // Unix socket paths are limited to 104 bytes, so run directories live under /tmp. beforeAll(() => { if (!darwin) return; directory = mkdtempSync("/tmp/spark-driver-test-"); harness = path.join(directory, "test-driver"); const native = path.join(root, "packages/desktop-app/macos"); - execFileSync("clang++", ["-fobjc-arc", "-fblocks", "-std=c++20", "-framework", "AppKit", "-framework", "QuartzCore", "-I", native, + execFileSync("clang++", ["-fobjc-arc", "-fblocks", "-std=c++20", "-framework", "AppKit", "-framework", "QuartzCore", "-framework", "CoreGraphics", "-I", native, path.join(native, "SparkTestDriver.mm"), path.join(root, "tests/test-driver.native.mm"), "-o", harness]); + pointer = path.join(directory, "macos-pointer"); + execFileSync("clang", ["-fobjc-arc", "-framework", "Foundation", "-framework", "ApplicationServices", + path.join(root, "scripts/e2e/macos-pointer.m"), "-o", pointer]); }, 60000); // SparkWindow.mm includes SparkDesktop.h, which needs one React Native type. let windowState = ""; @@ -24,12 +27,33 @@ beforeAll(() => { writeFileSync(path.join(shim, "RCTBridgeModule.h"), "typedef void (^RCTPromiseRejectBlock)(NSString *code, NSString *message, NSError *error);\n"); windowState = path.join(directory, "driver-window-state"); const native = path.join(root, "packages/desktop-app/macos"); - execFileSync("clang++", ["-fobjc-arc", "-fblocks", "-std=c++20", "-framework", "AppKit", "-framework", "QuartzCore", "-I", native, "-I", path.join(directory, "include"), + execFileSync("clang++", ["-fobjc-arc", "-fblocks", "-std=c++20", "-framework", "AppKit", "-framework", "QuartzCore", "-framework", "CoreGraphics", "-I", native, "-I", path.join(directory, "include"), path.join(native, "SparkTestDriver.mm"), path.join(native, "SparkWindow.mm"), path.join(root, "tests/driver-window-state.native.mm"), "-o", windowState]); }, 60000); afterAll(() => { if (directory) rmSync(directory, { recursive: true, force: true }); }); const run = (mode: string) => spawnSync(harness, [mode, directory], { encoding: "utf8", timeout: 30000 }); +test.skipIf(!darwin)("the OS pointer helper preflights permission without prompting or posting input", () => { + const result = spawnSync(pointer, ["check"], { encoding: "utf8", timeout: 10000 }); + expect(result.signal).toBeNull(); + expect([0, 77]).toContain(result.status); + if (result.status === 77) expect(result.stderr).toContain("E_PERMISSION_DENIED: Real pointer input needs Accessibility permission"); +}); + +test.skipIf(!darwin)("the OS pointer helper rejects bad arguments before posting", () => { + const usage = spawnSync(pointer, [], { encoding: "utf8", timeout: 10000 }); + expect(usage.status).toBe(64); + expect(usage.stderr).toContain("usage: macos-pointer"); + for (const pid of ["0", "-1", "123bad", "999999999999999999999999"]) { + const result = spawnSync(pointer, [pid, "bad", "bad"], { encoding: "utf8", timeout: 10000 }); + expect(result.status).toBe(64); + expect(result.stderr).toContain("Expected a positive process ID"); + } + const decode = spawnSync(pointer, [String(process.pid), "bad", "bad"], { encoding: "utf8", timeout: 10000 }); + expect([1, 77]).toContain(decode.status); + if (decode.status === 1) expect(decode.stderr).toContain("E_EVENT: Could not decode mouse events"); +}); + test.skipIf(!darwin)("the driver is inert unless the runner launches the app", () => { expect(run("inert").stdout).toContain("inert passed"); }); @@ -49,6 +73,10 @@ test.skipIf(!darwin)("a client that disconnects before its reply cannot kill the expect(result.signal).toBeNull(); expect(result.stdout, result.stderr).toContain("disconnect passed"); }); +test.skipIf(!darwin)("disconnect restores pointer window and activation state before quitting", () => { + const result = run("pointer-disconnect"); + expect(result.stdout, result.stderr).toContain("pointer-disconnect passed"); +}); test.skipIf(!darwin)("protocol: ping, navigate, openFile, waitFor, appearance, settled private captures and quit", () => { const result = run("session"); expect(result.stdout, result.stderr).toContain("session passed"); diff --git a/tests/test-driver.native.mm b/tests/test-driver.native.mm index 96e84fd56..13fde4a21 100644 --- a/tests/test-driver.native.mm +++ b/tests/test-driver.native.mm @@ -20,6 +20,15 @@ @implementation Delegate - (void)application:(NSApplication *)application openURLs:(NSArray<NSURL *> *)urls { [openedURLs addObjectsFromArray:urls]; } - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { terminateRequested = YES; return NSTerminateCancel; } @end +@interface PointerWindow : NSWindow +@property BOOL shrinkOnOrder; +@end +@implementation PointerWindow +- (void)makeKeyAndOrderFront:(id)sender { + [super makeKeyAndOrderFront:sender]; + if (self.shrinkOnOrder) { self.shrinkOnOrder = NO; [self setContentSize:NSMakeSize(200, 300)]; } +} +@end @interface DrawnView : NSView @end @implementation DrawnView @@ -88,7 +97,7 @@ static void SendRaw(int fd, NSString *text) { static void Client(void (^body)(void)) { [NSThread detachNewThreadWithBlock:body]; } static NSWindow *MakeWindow(void) { - NSWindow *window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 400, 300) + NSWindow *window = [[PointerWindow alloc] initWithContentRect:NSMakeRect(0, 0, 400, 300) styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable backing:NSBackingStoreBuffered defer:NO]; window.identifier = @"spark.main"; window.title = @"Probe"; window.releasedWhenClosed = NO; NSView *content = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 400, 300)]; @@ -149,7 +158,7 @@ int main(int argc, char **argv) { @autoreleasepool { Check(SparkTestDriverStart() && SparkTestDriverActive(), @"driver start"); // Optional modules add commands; built-in and duplicate names are refused. SparkTestDriverRegisterCommand(@"echo", ^(NSDictionary *request, SparkTestDriverReply reply) { reply(@{ @"ok": @YES, @"echo": request[@"value"] ?: NSNull.null }); }); - for (NSString *taken in @[@"ping", @"openFile", @"echo"]) { + for (NSString *taken in @[@"ping", @"openFile", @"pointerEvents", @"restorePointer", @"echo"]) { BOOL refused = NO; @try { SparkTestDriverRegisterCommand(taken, ^(NSDictionary *, SparkTestDriverReply reply) { reply(@{ @"ok": @NO }); }); } @catch (NSException *) { refused = YES; } Check(refused, [NSString stringWithFormat:@"%@ cannot be registered twice", taken]); @@ -178,6 +187,15 @@ int main(int argc, char **argv) { @autoreleasepool { close(fd); finished = YES; }); + else if ([mode isEqual:@"pointer-disconnect"]) Client(^{ + int fd = Connect(); + SendRaw(fd, [NSString stringWithFormat:@"{\"token\":\"%s\"}\n", token]); + Check([Receive(fd)[@"ok"] boolValue], @"authenticated"); + if (NSScreen.screens.count) + Check([Request(fd, @{ @"command": @"pointerEvents", @"window": @"main", @"x": @50, @"y": @60 })[@"ok"] boolValue], @"pointer before disconnect"); + close(fd); + finished = YES; + }); else if ([mode isEqual:@"session"]) Client(^{ int fd = Connect(); // Split writes exercise partial reads on the accepted socket. @@ -188,6 +206,7 @@ int main(int argc, char **argv) { @autoreleasepool { Check([hello[@"ok"] boolValue] && [hello[@"protocol"] isEqual:@1], @"authenticated"); Check([Request(fd, @{ @"command": @"ping" })[@"ok"] boolValue], @"ping"); Check([Code(Request(fd, @{ @"command": @"fly" })) isEqual:@"E_UNKNOWN_COMMAND"], @"unknown command"); + Check([Code(Request(fd, @{ @"command": @"pointerEvents", @"window": @"id:missing", @"x": @10, @"y": @20 })) isEqual:@"E_NO_WINDOW"], @"pointer requires an existing window"); Check([Request(fd, @{ @"command": @"navigate", @"url": @"spark-ks://windows/frame" })[@"ok"] boolValue], @"navigate"); Check([Code(Request(fd, @{ @"command": @"navigate", @"url": @"file:///etc/hosts" })) isEqual:@"E_INVALID_ARGUMENT"], @"file URLs are not navigation"); Check([Request(fd, @{ @"command": @"openFile", @"path": @"/etc/hosts" })[@"ok"] boolValue], @"openFile"); @@ -218,6 +237,10 @@ int main(int argc, char **argv) { @autoreleasepool { [NSTimer scheduledTimerWithTimeInterval:0.02 repeats:YES block:^(NSTimer *timer) { if (mounting) SparkTestDriverDidMount(); else [timer invalidate]; }]; }); Check([Code(Request(fd, @{ @"command": @"capture", @"window": @"main", @"name": @"busy", @"timeoutMs": @200 })) isEqual:@"E_NOT_SETTLED"], @"capture waits for mounts to settle"); + Check([Code(Request(fd, @{ @"command": @"pointerEvents", @"window": @"main", @"x": @50, @"y": @60, @"timeoutMs": @200 })) isEqual:@"E_NOT_SETTLED"], @"pointer input waits for mounts and gesture attachment to settle"); + Check(!window.isVisible && NSApp.activationPolicy == NSApplicationActivationPolicyProhibited, @"unsettled pointer input never shows or activates the app"); + Check([Code(Request(fd, @{ @"command": @"pointerEvents", @"window": @"id:missing", @"x": @10, @"y": @20, @"timeoutMs": @200 })) isEqual:@"E_NO_WINDOW"], @"pointer resolves its window before waiting"); + Check([Code(Request(fd, @{ @"command": @"pointerEvents", @"window": @"main", @"x": @"bad", @"y": @20, @"timeoutMs": @200 })) isEqual:@"E_INVALID_ARGUMENT"], @"pointer validates its point before waiting"); mounting = false; NSDictionary *capture = Request(fd, @{ @"command": @"capture", @"window": @"main", @"name": @"cache" }); Check([capture[@"ok"] boolValue] && [capture[@"file"] isEqual:@"cache.png"], [NSString stringWithFormat:@"capture %@", capture]); @@ -233,6 +256,56 @@ int main(int argc, char **argv) { @autoreleasepool { // Two requests in one write are answered in order. SendRaw(fd, @"{\"id\":\"a\",\"command\":\"ping\"}\n{\"id\":\"b\",\"command\":\"ping\"}\n"); Check([Receive(fd)[@"id"] isEqual:@"a"] && [Receive(fd)[@"id"] isEqual:@"b"], @"pipelined requests"); + if (NSScreen.screens.count) { + dispatch_sync(dispatch_get_main_queue(), ^{ + [window setFrameOrigin:NSMakePoint(100, 150)]; + }); + NSWindowAnimationBehavior animation = window.animationBehavior; + int64_t previousNumber = 0; + for (int tap = 0; tap < 2; tap++) { + NSDictionary *pointer = Request(fd, @{ @"command": @"pointerEvents", @"window": @"main", @"x": @50, @"y": @60 }); + Check([pointer[@"ok"] boolValue], [NSString stringWithFormat:@"pointer window geometry: %@", pointer]); + int64_t downNumber = 0; + for (NSString *key in @[@"down", @"up"]) { + NSData *data = [[NSData alloc] initWithBase64EncodedString:pointer[key] options:0]; + CGEventRef event = CGEventCreateFromData(NULL, (__bridge CFDataRef)data); + Check(event != NULL, @"pointer event data decodes"); + NSEvent *mouse = [NSEvent eventWithCGEvent:event]; + CGPoint screen = CGEventGetLocation(event); + Check(mouse.windowNumber == window.windowNumber && fabs(mouse.locationInWindow.x - 50) < 1 && fabs(mouse.locationInWindow.y - (NSHeight(window.frame) - 60)) < 1, [NSString stringWithFormat:@"encoded events preserve the window and AppKit-local hit point: %@ global=%@ frame=%@", NSStringFromPoint(mouse.locationInWindow), NSStringFromPoint(NSPointFromCGPoint(screen)), NSStringFromRect(window.frame)]); + Check(fabs(screen.x - (NSMinX(window.frame) + 50)) < 0.1 && fabs(screen.y - (NSMaxY(NSScreen.screens.firstObject.frame) - NSMaxY(window.frame) + 60)) < 0.1, @"encoded events preserve Quartz screen coordinates"); + Check(CGEventGetType(event) == ([key isEqual:@"down"] ? kCGEventLeftMouseDown : kCGEventLeftMouseUp), @"pointer encodes mouse down then up"); + int64_t number = CGEventGetIntegerValueField(event, kCGMouseEventNumber); + if ([key isEqual:@"down"]) { + Check(number > previousNumber, @"each tap gets a monotonic event number"); + downNumber = number; + } else Check(number == downNumber, @"down and up share kCGMouseEventNumber after serialization"); + CFRelease(event); + } + previousNumber = downNumber; + Check([pointer[@"pid"] intValue] == getpid(), @"pointer events target the owning process"); + Check(window.isVisible, @"pointerEvents explicitly shows the hidden window"); + } + mounting = true; + dispatch_sync(dispatch_get_main_queue(), ^{ + SparkTestDriverDidMount(); + [NSTimer scheduledTimerWithTimeInterval:0.02 repeats:YES block:^(NSTimer *timer) { if (mounting) SparkTestDriverDidMount(); else [timer invalidate]; }]; + }); + Check([Request(fd, @{ @"command": @"pointerEvents", @"window": @"main", @"x": @50, @"y": @60, @"timeoutMs": @0 })[@"ok"] boolValue], @"a zero timeout prepares pointer input without waiting to settle"); + mounting = false; + for (id x in @[@-1, @400, @"bad", NSNull.null]) + Check([Code(Request(fd, @{ @"command": @"pointerEvents", @"window": @"main", @"x": x, @"y": @20 })) isEqual:@"E_INVALID_ARGUMENT"], @"pointer coordinates must be inside the frame"); + dispatch_sync(dispatch_get_main_queue(), ^{ ((PointerWindow *)window).shrinkOnOrder = YES; }); + Check([Code(Request(fd, @{ @"command": @"pointerEvents", @"window": @"main", @"x": @350, @"y": @20 })) isEqual:@"E_INVALID_ARGUMENT"], @"pointer bounds use geometry after ordering and layout"); + dispatch_sync(dispatch_get_main_queue(), ^{ [window setContentSize:NSMakeSize(400, 300)]; }); + Check([Request(fd, @{ @"command": @"restorePointer" })[@"ok"] boolValue], @"restore pointer state"); + dispatch_sync(dispatch_get_main_queue(), ^{ + Check(!window.isVisible && !NSApp.keyWindow, @"pointer cleanup hides the window"); + Check(NSApp.activationPolicy == NSApplicationActivationPolicyProhibited, @"pointer cleanup restores prohibited activation"); + Check(window.animationBehavior == animation, @"pointer cleanup restores animation"); + }); + Check([Request(fd, @{ @"command": @"restorePointer" })[@"ok"] boolValue], @"pointer cleanup is idempotent"); + } else puts("pointer geometry skipped: no display"); Check([Request(fd, @{ @"command": @"quit" })[@"ok"] boolValue], @"quit"); Check(Receive(fd) == nil, @"quit ends the session"); finished = YES; @@ -242,6 +315,10 @@ int main(int argc, char **argv) { @autoreleasepool { Pump(^{ return (BOOL)(finished && terminateRequested); }, 20); Check(finished, @"client finished"); Check(terminateRequested, @"the app quits when the session ends"); + if ([mode isEqual:@"pointer-disconnect"]) { + Check(!window.isVisible, @"disconnect hides the pointer window before termination"); + Check(NSApp.activationPolicy == NSApplicationActivationPolicyProhibited, @"disconnect restores activation before termination"); + } Pump(^{ return (BOOL)(access([directory stringByAppendingPathComponent:@"driver.sock"].fileSystemRepresentation, F_OK) != 0); }, 2); Check(access([directory stringByAppendingPathComponent:@"driver.sock"].fileSystemRepresentation, F_OK) != 0, @"socket is removed"); if ([mode isEqual:@"session"]) { diff --git a/tests/text-input-accessibility.test.ts b/tests/text-input-accessibility.test.ts index 2e8645e2a..05d304ac5 100644 --- a/tests/text-input-accessibility.test.ts +++ b/tests/text-input-accessibility.test.ts @@ -25,7 +25,7 @@ test("the TextInput accessibility patch install checks the version and preserves const dependency = path.join(root, "node_modules/react-native-macos"), cache = path.join(root, "cache.m"); const installed = path.join(dependency, relative); mkdirSync(path.dirname(installed), { recursive: true }); - writeFileSync(path.join(dependency, "package.json"), JSON.stringify({ name: "react-native-macos", version: "0.81.7" })); + writeFileSync(path.join(dependency, "package.json"), JSON.stringify({ name: "react-native-macos", version: "0.83.0" })); writeFileSync(cache, pristine); linkSync(cache, installed); installTextInputAccessibilityPatch(root); expect(readFileSync(cache, "utf8")).toBe(pristine); @@ -33,6 +33,6 @@ test("the TextInput accessibility patch install checks the version and preserves const inode = statSync(installed).ino; installTextInputAccessibilityPatch(root); expect(statSync(installed).ino).toBe(inode); writeFileSync(path.join(dependency, "package.json"), JSON.stringify({ name: "react-native-macos", version: "0.82.0" })); - expect(() => installTextInputAccessibilityPatch(root)).toThrow("requires react-native-macos@0.81.7"); + expect(() => installTextInputAccessibilityPatch(root)).toThrow("requires react-native-macos@0.83.0"); } finally { rmSync(root, { recursive: true, force: true }); } }); diff --git a/tests/ui-controls.test.ts b/tests/ui-controls.test.ts index eb5e9a01c..483a49a1e 100644 --- a/tests/ui-controls.test.ts +++ b/tests/ui-controls.test.ts @@ -9,10 +9,12 @@ vi.mock("../packages/ui/src/SparkTextInputNativeComponent", () => ({ default: "S vi.mock("../packages/ui/src/SparkSelectNativeComponent", () => ({ default: "SparkSelect" })); vi.mock("../packages/ui/src/native-select/NativeSelectNativeComponent", () => ({ default: "NativeSelect" })); vi.mock("../packages/ui/src/native-select/NativeSegmentedControlNativeComponent", () => ({ default: "NativeSegmentedControl" })); -import { Button, TextInput, Select, SegmentedControl, getControlAvailability, type ControlRef } from "../packages/ui/src/index"; +vi.mock("expo-modules-core", () => ({ requireOptionalNativeModule: () => ({}) })); +import { Button, TextInput, Select, SegmentedControl, getControlAvailability, type ControlRef } from "../packages/ui/src/index.macos"; import { TextInput as MobileTextInput } from "../packages/ui/src/mobile-input"; import { Button as WebButton, TextInput as WebTextInput, Select as WebSelect } from "../packages/ui/src/index.web"; import { selectionIndex } from "../packages/ui/src/select"; +import { Select as AndroidSelect } from "../packages/ui/src/index.android"; let rendered: any; const event = (text: string, eventCount: number) => ({ nativeEvent: { text, eventCount } }); const options = [{ label: "One", value: "one" }, { label: "Two", value: "two" }]; @@ -89,3 +91,12 @@ test("invalid select values and option fields use shared errors", () => { for (const options of [[], [null], [{ label: "", value: "a" }], [{ label: "A", value: 1 }]]) expect(() => selectionIndex(options as never, "a")).toThrow(expect.objectContaining({ code: "E_INVALID_ARGUMENT" })); expect(() => selectionIndex([{ label: "A", value: "a", enabled: false }] as never, "a")).toThrow(expect.objectContaining({ code: "E_UNSUPPORTED_OPTION" })); }); +test("Android Select segments expose radio semantics with their checked state", async () => { + const require = createRequire(import.meta.url), stub = (id: string, exports: object) => { require.cache[require.resolve(id)] = { exports } as any; }; + stub("@expo/ui/jetpack-compose", { Host: "Host", Text: "ComposeText", SingleChoiceSegmentedButtonRow: "SegmentedRow", SegmentedButton: Object.assign(function SegmentedButton(props: any) { return props.children; }, { Label: "SegmentedLabel" }) }); + stub("@expo/ui/jetpack-compose/modifiers", { selectable: (selected: boolean, handler: () => void, role: string) => ({ selected, handler, role }) }); + const onValueChange = vi.fn(); await mount(React.createElement(AndroidSelect, { options, value: "two", onValueChange })); + const semantics = rendered.root.findAll((node: any) => node.type.name === "SegmentedButton").map((segment: any) => segment.props.modifiers[0]); + expect(semantics.map(({ role, selected }: any) => ({ role, selected }))).toEqual([{ role: "radioButton", selected: false }, { role: "radioButton", selected: true }]); + await act(async () => semantics[0].handler()); expect(onValueChange).toHaveBeenCalledWith("one"); +}); diff --git a/tests/upgrade.test.ts b/tests/upgrade.test.ts index 304997be2..56e0cd47b 100644 --- a/tests/upgrade.test.ts +++ b/tests/upgrade.test.ts @@ -27,7 +27,7 @@ beforeEach(() => { current = "0.0.1-next.3"; next = "0.0.1-next.5"; writeJson(path.join(root, "package.json"), { - name: "consumer", scripts: { dev: "spark dev" }, dependencies: { "@legendapp/spark": current, react: "^19.1.0", "react-native": "0.81.6", expo: "54.0.37" }, + name: "consumer", scripts: { dev: "spark dev" }, dependencies: { "@legendapp/spark": current, react: "^19.2.0", "react-native": "0.81.6", expo: "54.0.37" }, overrides: { "@legendapp/spark": current, react: "$react", "@expo/cli": "old", "@react-native-runtimes/core": "file:./spark-packages/old.tgz" }, custom: true, }); writeJson(path.join(root, "desktop.config.json"), { projectId: "upgrade-test", platforms: ["macos"] }); diff --git a/tests/window-api.test.ts b/tests/window-api.test.ts index ebc92cabd..60c581cd9 100644 --- a/tests/window-api.test.ts +++ b/tests/window-api.test.ts @@ -1,5 +1,5 @@ import { beforeEach, afterEach, expect, test, vi } from "vitest"; -const { call, platform, listeners, managed } = vi.hoisted(() => ({ call: vi.fn(), platform: { OS: "windows" }, listeners: new Set<(event: any) => void>(), managed: { getConstantsJson: () => JSON.stringify({ WINDOW_LEVEL_STATUS: 25, WINDOW_LEVEL_FLOATING: 3 }), openWindow: vi.fn(async (_options: string) => '{"success":true}'), setWindowOptions: vi.fn(async (_id: string, _options: string) => '{"success":true}') } })); +const { call, platform, listeners, managed } = vi.hoisted(() => ({ call: vi.fn(), platform: { OS: "windows" }, listeners: new Set<(event: any) => void>(), managed: { getConstantsJson: () => JSON.stringify({ WINDOW_LEVEL_STATUS: 25, WINDOW_LEVEL_FLOATING: 3 }), openWindow: vi.fn(async (_options: string) => '{"success":true}'), setMainWindowOptions: vi.fn(async (_options: string) => '{"success":true}'), setWindowOptions: vi.fn(async (_id: string, _options: string) => '{"success":true}') } })); vi.mock("react-native", () => ({ Platform: platform })); vi.mock("../packages/desktop-windows/src/NativeDesktopWindowManager", () => ({ default: { call } })); vi.mock("../packages/desktop-windows/src/window-manager/NativeWindowManager", () => ({ default: managed })); @@ -8,7 +8,7 @@ import * as windows from "../packages/desktop-windows/src/api"; const info = { id: "editor", instanceId: "instance-1", kind: "window", title: "Editor", parentId: null, modal: false, visible: true, focused: true, minimized: false, maximized: false, fullscreen: false, bounds: { displayId: "display", x: 10, y: 20, width: 640, height: 480 } }; const emit = (event: object) => listeners.forEach(listener => listener({ windowId: "editor", instanceId: "instance-1", ...event })); const tick = async () => { for (let n=0;n<10;n++) await Promise.resolve(); }; -beforeEach(() => { platform.OS = "windows"; call.mockReset().mockImplementation(async (method: string) => JSON.stringify(["open", "info", "observe", "completeOpen"].includes(method) ? info : method === "close" ? { closed: true } : null)); managed.openWindow.mockClear(); managed.setWindowOptions.mockClear(); listeners.clear(); }); +beforeEach(() => { platform.OS = "windows"; call.mockReset().mockImplementation(async (method: string) => JSON.stringify(["open", "info", "observe", "completeOpen"].includes(method) ? info : method === "close" ? { closed: true } : null)); managed.openWindow.mockClear(); managed.setWindowOptions.mockClear(); managed.setMainWindowOptions.mockReset().mockResolvedValue('{"success":true}'); listeners.clear(); }); afterEach(() => { emit({ type: "closed" }); vi.restoreAllMocks(); }); test("availability is answered synchronously without a native call", async () => { expect(windows.getWindowAvailability()).toEqual({ available: true }); @@ -251,3 +251,18 @@ test("macOS toolbar-only updates bypass portable window geometry", async () => { await windows.setWindowOptions("editor", {}); expect(call).toHaveBeenCalledTimes(2); }); + +test("macOS main-window chrome uses the host window setter rather than the secondary registry", async () => { + platform.OS = "macos"; + for (const trafficLights of [false, true]) { + await windows.setWindowOptions("main", { macos: { titleBar: { trafficLights } } }); + expect(JSON.parse(managed.setMainWindowOptions.mock.calls.at(-1)![0])).toEqual({ windowStyle: { trafficLights } }); + } + expect(call).not.toHaveBeenCalled(); + await windows.setWindowOptions("main", { title: "Photos", appearance: "dark", backgroundColor: "#191A1B" }); + expect(call).toHaveBeenCalledWith("options", JSON.stringify({ id: "main", options: { title: "Photos", appearance: "dark", backgroundColor: "#191A1B" } })); + expect(JSON.parse(managed.setMainWindowOptions.mock.calls.at(-1)![0])).toEqual({ windowStyle: { appearance: "dark", backgroundColor: "#191A1B" } }); + expect(managed.setWindowOptions).not.toHaveBeenCalled(); + managed.setMainWindowOptions.mockRejectedValueOnce(Object.assign(new Error("Main window not found"), { code: "E_NOT_FOUND" })); + await expect(windows.setWindowOptions("main", { macos: { titleBar: { trafficLights: true } } })).rejects.toMatchObject({ code: "E_NOT_FOUND" }); +}); diff --git a/tests/workspace-adapters.test.ts b/tests/workspace-adapters.test.ts index ed85d62fd..2411cd960 100644 --- a/tests/workspace-adapters.test.ts +++ b/tests/workspace-adapters.test.ts @@ -4,6 +4,7 @@ import path from "node:path"; import os from "node:os"; import { installWorkspaceAdapters } from "../scripts/install-workspace-adapters.ts"; import { createPatch } from "diff"; +import { transformSync } from "@babel/core"; function fixture() { const root = mkdtempSync(path.join(os.tmpdir(), "spark-workspace-adapter-")); const pkg = path.join(root, "node_modules/probe"); mkdirSync(pkg, { recursive: true }); @@ -71,22 +72,28 @@ test("manifest patches survive receipt stamping, interruption, and repeated inst } finally { f.close(); } }); -test("Windows platform discovery skips its CLI off Windows and preserves it on Windows", () => { +test.each(["", 'if (process.platform !== "win32") { module.exports = {}; return; }\n'])("Windows platform discovery parses in Expo and skips its CLI off Windows (previous guard: %j)", previousGuard => { const f = fixture(); try { const directory = path.join(f.root, "node_modules/react-native-windows"); mkdirSync(directory); const file = path.join(directory, "react-native.config.js"); - const installedConfig = readFileSync("node_modules/react-native-windows/react-native.config.js", "utf8"); - const upstream = installedConfig.slice(installedConfig.indexOf("const cli =")); - writeFileSync(file, upstream); + const upstream = `const cli = require("@react-native-windows/cli"); +module.exports = { + platforms: { windows: { npmPackageName: "react-native-windows" } }, + healthChecks: cli.getHealthChecks(), +}; +`; + writeFileSync(file, previousGuard + upstream); linkSync(file, path.join(f.root, "cached-windows-config.js")); installWorkspaceAdapters(f.root); const adapted = readFileSync(file, "utf8"); expect(adapted).not.toBe(upstream); installWorkspaceAdapters(f.root); expect(readFileSync(file, "utf8")).toBe(adapted); - expect(readFileSync(path.join(f.root, "cached-windows-config.js"), "utf8")).toBe(upstream); + expect(readFileSync(path.join(f.root, "cached-windows-config.js"), "utf8")).toBe(previousGuard + upstream); + // Expo's config loader parses CommonJS as a module before evaluating it. + expect(() => transformSync(adapted, { configFile: false, babelrc: false, sourceType: "module" })).not.toThrow(); const evaluate = new Function("module", "process", "require", adapted); for (const platform of ["darwin", "linux"]) { const module = { exports: {} };