From 6a907cf89b44c6e38208c9c389567c4c481a7a9c Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Thu, 4 Jun 2026 19:42:44 +0200 Subject: [PATCH] fix(calls): sandbox/prod VoIP-push failover + foreground CallKit-UI suppress - voip-push: build both APNs Provider (production+sandbox) and try each per token with memoization. Fixes BadDeviceToken on Xcode-Dev-Builds where the token is Sandbox-only. - stores/call: only call callkit.displayIncomingCall when app NOT in foreground \u2014 in foreground the /call route handles ringing UI, otherwise double UI (system banner + fullscreen). - patch react-native-callkeep: New-Arch TurboModule compatibility (no overloads, no Bundle params in @ReactMethod). - pushTokenRegistration: more verbose [voip] diagnostics. --- .../hooks/usePushTokenRegistration.ts | 20 ++++- apps/rebreak-native/stores/call.ts | 17 ++-- backend/server/services/voip-push.ts | 89 +++++++++++++------ package.json | 4 + patches/react-native-callkeep.patch | 72 +++++++++++++++ pnpm-lock.yaml | 17 ++-- 6 files changed, 177 insertions(+), 42 deletions(-) create mode 100644 patches/react-native-callkeep.patch diff --git a/apps/rebreak-native/hooks/usePushTokenRegistration.ts b/apps/rebreak-native/hooks/usePushTokenRegistration.ts index 6d41d23..7d019d9 100644 --- a/apps/rebreak-native/hooks/usePushTokenRegistration.ts +++ b/apps/rebreak-native/hooks/usePushTokenRegistration.ts @@ -35,26 +35,36 @@ const lastRegisteredVoipToken: { current: string | null } = { current: null }; /** Holt iOS-VoIP-Push-Token via PushKit. Resolve mit `null` wenn nicht verfügbar. */ async function fetchVoipToken(): Promise { - if (Platform.OS !== 'ios' || !RNVoipPushNotification) return null; + if (Platform.OS !== 'ios') { + if (__DEV__) console.log('[voip] skip (not iOS)'); + return null; + } + if (!RNVoipPushNotification) { + console.warn('[voip] react-native-voip-push-notification NOT linked — PushKit token cannot be fetched. Did the with-voip-pushkit-ios.js plugin run + prebuild?'); + return null; + } return new Promise((resolve) => { let resolved = false; const onToken = (token: string) => { if (resolved) return; resolved = true; + if (__DEV__) console.log('[voip] register event fired, token:', token ? token.slice(0, 20) + '…' : '(empty)'); resolve(token || null); }; try { // Listener registrieren BEVOR registerVoipToken — sonst race. RNVoipPushNotification.addEventListener('register', onToken); RNVoipPushNotification.registerVoipToken(); + if (__DEV__) console.log('[voip] registerVoipToken() called, waiting for callback…'); // Safety-Timeout: nach 4s aufgeben (Cert/Provisioning fehlt etc.). setTimeout(() => { if (resolved) return; resolved = true; + console.warn('[voip] timeout after 4s — NO PushKit token received. Check: Push-Notifications-Cap + Background-Modes(voip) in Xcode entitlements, physical device, VoIP-services-certificate in Apple Portal.'); resolve(null); }, 4000); } catch (err) { - if (__DEV__) console.warn('[voip] register failed:', err); + console.warn('[voip] register failed:', err); resolve(null); } }); @@ -141,7 +151,11 @@ export async function registerPushTokenWithBackend(): Promise { lastRegisteredVoipToken.current = voipToken; if (__DEV__) { console.log('[push] token registered:', token.slice(0, 30) + '…'); - if (voipToken) console.log('[voip] token registered:', voipToken.slice(0, 30) + '…'); + if (voipToken) { + console.log('[voip] token registered:', voipToken.slice(0, 30) + '…'); + } else if (Platform.OS === 'ios') { + console.warn('[voip] iOS without VoIP-token — incoming calls in background/killed will NOT wake the app. Backend will fall back to silent Expo-push.'); + } } return token; } catch (err) { diff --git a/apps/rebreak-native/stores/call.ts b/apps/rebreak-native/stores/call.ts index e4d4ee4..c6a5786 100644 --- a/apps/rebreak-native/stores/call.ts +++ b/apps/rebreak-native/stores/call.ts @@ -1,5 +1,5 @@ import { create } from 'zustand'; -import { NativeModules } from 'react-native'; +import { AppState, NativeModules } from 'react-native'; import type { RealtimeChannel } from '@supabase/supabase-js'; import { supabase } from '../lib/supabase'; import { apiFetch } from '../lib/api'; @@ -382,11 +382,16 @@ export const useCallStore = create((set, get) => { currentRole = 'callee'; loggedCallId = null; set({ status: 'incoming', peer: from, callId, muted: false, speaker: false, startedAt: null, endReason: null }); - // CallKit-/ConnectionService-UI hochziehen — das zeigt nativen Call-Screen - // über Lockscreen, sogar wenn die App im Background ist. RNCallKeep - // dedupliziert intern via UUID, also safe wenn AppDelegate's - // reportNewIncomingCall (VoIP-Push-Pfad) schon dieselbe UUID gemeldet hat. - try { callkit.displayIncomingCall(callId, from.nickname || 'ReBreak'); } catch {} + // CallKit-/ConnectionService-UI hochziehen — ABER nur wenn App NICHT im + // Vordergrund. Im Foreground kümmert sich der In-App /call-Screen darum, + // sonst gibt es Doppel-UI (System-Banner + Fullscreen). VoIP-Push-Pfad + // (AppDelegate.reportNewIncomingCall) läuft eh getrennt, das deduliziert + // CallKit intern via UUID. + if (AppState.currentState !== 'active') { + try { callkit.displayIncomingCall(callId, from.nickname || 'ReBreak'); } catch {} + } else { + clog('receiveIncoming: app foreground — skipping CallKit UI (using in-app /call screen)'); + } // CallKit (iOS) + ConnectionService (Android) spielen ihren eigenen // Ringtone — KEIN InCallManager.startRingtone() hier, sonst doppeltes // Klingeln. InCallManager.start() bleibt aus demselben Grund weg; der diff --git a/backend/server/services/voip-push.ts b/backend/server/services/voip-push.ts index d6fa3e4..4151a9f 100644 --- a/backend/server/services/voip-push.ts +++ b/backend/server/services/voip-push.ts @@ -28,47 +28,56 @@ type ApnProvider = InstanceType; type ApnNotification = InstanceType; let apnMod: ApnModule | null = null; -let provider: ApnProvider | null = null; +let providerProd: ApnProvider | null = null; +let providerSandbox: ApnProvider | null = null; let initialized = false; let topic: string | null = null; +/** Token → "prod" | "sandbox" Memoization. Vermeidet 2-Round-Trip pro Push. */ +const tokenEnvCache = new Map(); -async function getProvider(): Promise { - if (initialized) return provider; +async function ensureInit(): Promise { + if (initialized) return providerProd !== null || providerSandbox !== null; initialized = true; const p12Path = process.env.APNS_VOIP_P12_PATH; const p12Pass = process.env.APNS_VOIP_P12_PASSWORD; const tpc = process.env.APNS_VOIP_TOPIC; - const production = process.env.APNS_VOIP_PRODUCTION === "true"; if (!p12Path || !p12Pass || !tpc) { console.warn( "[voip-push] disabled — missing env (APNS_VOIP_P12_PATH/PASSWORD/TOPIC)", ); - return null; + return false; } if (!fs.existsSync(p12Path)) { console.warn(`[voip-push] disabled — p12 file not found at ${p12Path}`); - return null; + return false; } try { - // Dynamic import — vermeidet bundler statisches Tracing. const mod = (await import("@parse/node-apn")) as unknown as ApnModule & { default?: ApnModule; }; apnMod = mod.default ?? mod; topic = tpc; - provider = new apnMod.Provider({ + // VoIP-Services-Cert ist Universal — wir bauen BEIDE Provider (Production + + // Sandbox) und wählen pro Token via Memoization. So funktioniert es für + // Xcode-Dev-Builds (Sandbox) UND TestFlight/AppStore (Production) parallel. + providerProd = new apnMod.Provider({ pfx: p12Path, passphrase: p12Pass, - production, + production: true, }); - console.log(`[voip-push] initialized (topic=${tpc}, production=${production})`); - return provider; + providerSandbox = new apnMod.Provider({ + pfx: p12Path, + passphrase: p12Pass, + production: false, + }); + console.log(`[voip-push] initialized (topic=${tpc}, prod+sandbox providers ready)`); + return true; } catch (err) { console.error("[voip-push] init failed:", err); - return null; + return false; } } @@ -97,8 +106,8 @@ export interface VoIPCallPayload { * @returns true bei Erfolg (oder no-op wenn Service disabled), false bei Fehler. */ export async function sendVoIPPush(payload: VoIPCallPayload): Promise { - const p = await getProvider(); - if (!p || !topic || !apnMod) return true; // no-op, regulärer Push übernimmt + const ok = await ensureInit(); + if (!ok || !topic || !apnMod) return true; // no-op, regulärer Push übernimmt const note: ApnNotification = new apnMod.Notification(); note.topic = topic; @@ -116,28 +125,56 @@ export async function sendVoIPPush(payload: VoIPCallPayload): Promise { }, }; - try { - const result = await p.send(note, payload.voipToken); - if (result.failed.length > 0) { + // Reihenfolge: zuerst memoized Env (falls bekannt), sonst Production first → + // Sandbox als Fallback. + const cached = tokenEnvCache.get(payload.voipToken); + const order: Array<"prod" | "sandbox"> = + cached === "sandbox" ? ["sandbox", "prod"] : ["prod", "sandbox"]; + + for (const env of order) { + const prov = env === "prod" ? providerProd : providerSandbox; + if (!prov) continue; + try { + const result = await prov.send(note, payload.voipToken); + if (result.failed.length === 0) { + tokenEnvCache.set(payload.voipToken, env); + return true; + } const f = result.failed[0]; + const reason = (f.response as { reason?: string })?.reason; + // BadDeviceToken → nur dann Failover, sonst Abbruch (z.B. Unregistered). + if (reason === "BadDeviceToken") { + if (env === order[order.length - 1]) { + console.warn( + `[voip-push] failed token=${payload.voipToken.slice(0, 8)}… BadDeviceToken on both envs`, + ); + return false; + } + // try next env + continue; + } console.warn( - `[voip-push] failed token=${payload.voipToken.slice(0, 8)}… reason=`, + `[voip-push] failed token=${payload.voipToken.slice(0, 8)}… env=${env} reason=`, f.response ?? f.error, ); return false; + } catch (err) { + console.error("[voip-push] send threw:", err); + return false; } - return true; - } catch (err) { - console.error("[voip-push] send threw:", err); - return false; } + return false; } /** Cleanup bei Server-Shutdown — wichtig wegen HTTP/2-Verbindung an Apple. */ export function shutdownVoIPProvider(): void { - if (provider) { - provider.shutdown(); - provider = null; - initialized = false; + if (providerProd) { + providerProd.shutdown(); + providerProd = null; } + if (providerSandbox) { + providerSandbox.shutdown(); + providerSandbox = null; + } + initialized = false; } diff --git a/package.json b/package.json index 7a6b42d..503d973 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,10 @@ "metro-symbolicate": "0.83.3", "metro-transform-plugins": "0.83.3", "metro-transform-worker": "0.83.3" + }, + "patchedDependencies": { + "metro-core@0.83.3": "patches/metro-core@0.83.3.patch", + "react-native-callkeep": "patches/react-native-callkeep.patch" } } } diff --git a/patches/react-native-callkeep.patch b/patches/react-native-callkeep.patch new file mode 100644 index 0000000..5c11c05 --- /dev/null +++ b/patches/react-native-callkeep.patch @@ -0,0 +1,72 @@ +diff --git a/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java b/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java +index 025480ac97460cc45775ea11aa43af36522d5df6..106fbf0081b21ea02029add0aca86f55e8a55c07 100644 +--- a/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java ++++ b/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java +@@ -189,7 +189,7 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule implements Life + public void reportNewIncomingCall(String uuid, String number, String callerName, boolean hasVideo, String payload) { + Log.d(TAG, "[RNCallKeepModule] reportNewIncomingCall, uuid: " + uuid + ", number: " + number + ", callerName: " + callerName); + +- this.displayIncomingCall(uuid, number, callerName, hasVideo); ++ this.displayIncomingCall(uuid, number, callerName, hasVideo, null); + + // Send event to JS + WritableMap args = Arguments.createMap(); +@@ -434,17 +434,26 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule implements Life + this.hasListeners = false; + } + +- @ReactMethod +- public void displayIncomingCall(String uuid, String number, String callerName) { ++ // REBREAK_PATCH: @ReactMethod entfernt — RN New Architecture TurboModule ++ // erlaubt keine overloaded methods mit gleichem JS-Namen. JS ruft immer den ++ // 5-arg-Overload (siehe RNCallKeep.js displayIncomingCall implementation). ++ public void displayIncomingCall3args(String uuid, String number, String callerName) { + this.displayIncomingCall(uuid, number, callerName, false, null); + } + ++ // REBREAK_PATCH: TurboModule-Interop unterstützt android.os.Bundle nicht als ++ // @ReactMethod-Parameter. Daher: 4-arg-Variante (ohne Bundle) ist die einzige ++ // exposed @ReactMethod. JS-Wrapper ruft nie mit payload-Bundle. + @ReactMethod + public void displayIncomingCall(String uuid, String number, String callerName, boolean hasVideo) { +- this.displayIncomingCall(uuid, number, callerName, hasVideo, null); ++ this.displayIncomingCallInternal(uuid, number, callerName, hasVideo, null); + } + + public void displayIncomingCall(String uuid, String number, String callerName, boolean hasVideo, @Nullable Bundle payload) { ++ this.displayIncomingCallInternal(uuid, number, callerName, hasVideo, payload); ++ } ++ ++ private void displayIncomingCallInternal(String uuid, String number, String callerName, boolean hasVideo, @Nullable Bundle payload) { + if (!isConnectionServiceAvailable() || !hasPhoneAccount()) { + Log.w(TAG, "[RNCallKeepModule] displayIncomingCall ignored due to no ConnectionService or no phone account"); + return; +@@ -483,17 +492,25 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule implements Life + conn.onAnswer(); + } + +- @ReactMethod +- public void startCall(String uuid, String number, String callerName) { ++ // REBREAK_PATCH: @ReactMethod entfernt (siehe displayIncomingCall above). ++ public void startCall3args(String uuid, String number, String callerName) { + this.startCall(uuid, number, callerName, false, null); + } + ++ public void startCall4args(String uuid, String number, String callerName, boolean hasVideo) { ++ this.startCall(uuid, number, callerName, hasVideo, null); ++ } ++ + @ReactMethod + public void startCall(String uuid, String number, String callerName, boolean hasVideo) { +- this.startCall(uuid, number, callerName, hasVideo, null); ++ this.startCallInternal(uuid, number, callerName, hasVideo, null); + } + + public void startCall(String uuid, String number, String callerName, boolean hasVideo, @Nullable Bundle payload) { ++ this.startCallInternal(uuid, number, callerName, hasVideo, payload); ++ } ++ ++ private void startCallInternal(String uuid, String number, String callerName, boolean hasVideo, @Nullable Bundle payload) { + Log.d(TAG, "[RNCallKeepModule] startCall called, uuid: " + uuid + ", number: " + number + ", callerName: " + callerName + ", payload: " + payload); + + if (!isConnectionServiceAvailable() || !hasPhoneAccount() || !hasPermissions() || number == null) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 491f634..61bb60d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,9 @@ patchedDependencies: metro-core@0.83.3: hash: dbd76dee4e5497574765c5986b0e889264e7251ea7b5e849e2967d2eb2efb757 path: patches/metro-core@0.83.3.patch + react-native-callkeep: + hash: cba8c2dd49745c7a4f62437ea38db7e1d457966cd037dc405c0dca06d84850dd + path: patches/react-native-callkeep.patch importers: @@ -42,7 +45,7 @@ importers: version: 14.3.0(vue@3.5.34(typescript@5.9.3)) '@vueuse/nuxt': specifier: ^14.2.1 - version: 14.3.0(magicast@0.5.3)(nuxt@4.1.3(@electric-sql/pglite@0.4.1)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(eslint@10.3.0(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.32.0)(magicast@0.5.3)(mysql2@3.15.3)(optionator@0.9.4)(rollup@4.60.3)(terser@5.46.2)(typescript@5.9.3)(yaml@2.8.4))(vue@3.5.34(typescript@5.9.3)) + version: 14.3.0(magicast@0.5.3)(nuxt@4.1.3(@electric-sql/pglite@0.4.1)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(eslint@10.3.0(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.32.0)(magicast@0.5.3)(mysql2@3.15.3)(optionator@0.9.4)(rollup@4.60.3)(terser@5.46.2)(typescript@5.9.3)(vite@7.3.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.4))(yaml@2.8.4))(vue@3.5.34(typescript@5.9.3)) nuxt: specifier: 4.1.3 version: 4.1.3(@electric-sql/pglite@0.4.1)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(eslint@10.3.0(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.32.0)(magicast@0.5.3)(mysql2@3.15.3)(optionator@0.9.4)(rollup@4.60.3)(terser@5.46.2)(typescript@5.9.3)(vite@7.3.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.4))(yaml@2.8.4) @@ -73,7 +76,7 @@ importers: version: 1.2.3 '@nuxt/fonts': specifier: ^0.11.4 - version: 0.11.4(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(ioredis@5.10.1)(magicast@0.5.3) + version: 0.11.4(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(ioredis@5.10.1)(magicast@0.5.3)(vite@7.3.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.4)) '@nuxt/icon': specifier: ^1.10.0 version: 1.15.0(magicast@0.5.3)(vite@7.3.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.4))(vue@3.5.34(typescript@5.9.3)) @@ -91,7 +94,7 @@ importers: version: 3.0.3(magicast@0.5.3)(vue@3.5.34(typescript@5.9.3)) '@vueuse/nuxt': specifier: ^14.2.1 - version: 14.3.0(magicast@0.5.3)(nuxt@4.1.3(@electric-sql/pglite@0.4.1)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(eslint@10.3.0(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.32.0)(magicast@0.5.3)(mysql2@3.15.3)(optionator@0.9.4)(rollup@4.60.3)(terser@5.46.2)(typescript@5.9.3)(yaml@2.8.4))(vue@3.5.34(typescript@5.9.3)) + version: 14.3.0(magicast@0.5.3)(nuxt@4.1.3(@electric-sql/pglite@0.4.1)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(eslint@10.3.0(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.32.0)(magicast@0.5.3)(mysql2@3.15.3)(optionator@0.9.4)(rollup@4.60.3)(terser@5.46.2)(typescript@5.9.3)(vite@7.3.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.4))(yaml@2.8.4))(vue@3.5.34(typescript@5.9.3)) chart.js: specifier: ^4.5.1 version: 4.5.1 @@ -275,7 +278,7 @@ importers: version: 1.2.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0) react-native-callkeep: specifier: ^4.3.16 - version: 4.3.16(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0)) + version: 4.3.16(patch_hash=cba8c2dd49745c7a4f62437ea38db7e1d457966cd037dc405c0dca06d84850dd)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0)) react-native-gesture-handler: specifier: ~2.28.0 version: 2.28.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0) @@ -11716,7 +11719,7 @@ snapshots: - utf-8-validate - vue - '@nuxt/fonts@0.11.4(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(ioredis@5.10.1)(magicast@0.5.3)': + '@nuxt/fonts@0.11.4(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(ioredis@5.10.1)(magicast@0.5.3)(vite@7.3.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.4))': dependencies: '@nuxt/devtools-kit': 2.7.0(magicast@0.5.3)(vite@7.3.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.4)) '@nuxt/kit': 3.21.4(magicast@0.5.3) @@ -14329,7 +14332,7 @@ snapshots: transitivePeerDependencies: - magicast - '@vueuse/nuxt@14.3.0(magicast@0.5.3)(nuxt@4.1.3(@electric-sql/pglite@0.4.1)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(eslint@10.3.0(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.32.0)(magicast@0.5.3)(mysql2@3.15.3)(optionator@0.9.4)(rollup@4.60.3)(terser@5.46.2)(typescript@5.9.3)(yaml@2.8.4))(vue@3.5.34(typescript@5.9.3))': + '@vueuse/nuxt@14.3.0(magicast@0.5.3)(nuxt@4.1.3(@electric-sql/pglite@0.4.1)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(eslint@10.3.0(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.32.0)(magicast@0.5.3)(mysql2@3.15.3)(optionator@0.9.4)(rollup@4.60.3)(terser@5.46.2)(typescript@5.9.3)(vite@7.3.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.4))(yaml@2.8.4))(vue@3.5.34(typescript@5.9.3))': dependencies: '@nuxt/kit': 4.4.4(magicast@0.5.3) '@vueuse/core': 14.3.0(vue@3.5.34(typescript@5.9.3)) @@ -18874,7 +18877,7 @@ snapshots: sf-symbols-typescript: 2.2.0 use-latest-callback: 0.2.6(react@19.1.0) - react-native-callkeep@4.3.16(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0)): + react-native-callkeep@4.3.16(patch_hash=cba8c2dd49745c7a4f62437ea38db7e1d457966cd037dc405c0dca06d84850dd)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0)): dependencies: react-native: 0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0)