diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b0639e1..cc7bd7b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -19,8 +19,8 @@ jobs:
- run: npm ci
- - name: Typecheck shared package
- run: npm run typecheck --workspace=@farscry/shared
+ - name: Build shared package
+ run: npm run build --workspace=@farscry/shared
- name: Typecheck signaling server
run: npm run typecheck --workspace=@farscry/signaling
@@ -41,6 +41,9 @@ jobs:
- run: npm ci
+ - name: Build shared package
+ run: npm run build --workspace=@farscry/shared
+
- name: Run server tests
run: npm run test --workspace=@farscry/signaling
@@ -59,7 +62,7 @@ jobs:
- uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2
+ ruby-version: '3.1'
bundler-cache: true
working-directory: apps/mobile
diff --git a/apps/mobile/.env.example b/apps/mobile/.env.example
new file mode 100644
index 0000000..67d78cf
--- /dev/null
+++ b/apps/mobile/.env.example
@@ -0,0 +1,3 @@
+SUPABASE_URL=https://your-project.supabase.co
+SUPABASE_ANON_KEY=your-anon-key
+SIGNALING_URL=ws://localhost:8080
diff --git a/apps/mobile/App.tsx b/apps/mobile/App.tsx
index 5da1298..ac79334 100644
--- a/apps/mobile/App.tsx
+++ b/apps/mobile/App.tsx
@@ -3,6 +3,9 @@ import {StatusBar} from 'react-native';
import {SafeAreaProvider} from 'react-native-safe-area-context';
import {NavigationContainer} from '@react-navigation/native';
import {GestureHandlerRootView} from 'react-native-gesture-handler';
+import {AuthProvider} from './src/stores/authStore';
+import {ContactsProvider} from './src/stores/contactsStore';
+import {CallProvider} from './src/stores/callStore';
import {RootNavigator} from './src/navigation/RootNavigator';
import {colors} from './src/theme/colors';
@@ -27,12 +30,18 @@ const navTheme = {
export default function App() {
return (
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/apps/mobile/Gemfile.lock b/apps/mobile/Gemfile.lock
new file mode 100644
index 0000000..4ab371b
--- /dev/null
+++ b/apps/mobile/Gemfile.lock
@@ -0,0 +1,108 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ CFPropertyList (3.0.9)
+ activesupport (6.1.7.10)
+ concurrent-ruby (~> 1.0, >= 1.0.2)
+ i18n (>= 1.6, < 2)
+ minitest (>= 5.1)
+ tzinfo (~> 2.0)
+ zeitwerk (~> 2.3)
+ addressable (2.8.9)
+ public_suffix (>= 2.0.2, < 8.0)
+ algoliasearch (1.27.5)
+ httpclient (~> 2.8, >= 2.8.3)
+ json (>= 1.5.1)
+ atomos (0.1.3)
+ benchmark (0.5.0)
+ bigdecimal (4.0.1)
+ claide (1.1.0)
+ cocoapods (1.15.2)
+ addressable (~> 2.8)
+ claide (>= 1.0.2, < 2.0)
+ cocoapods-core (= 1.15.2)
+ cocoapods-deintegrate (>= 1.0.3, < 2.0)
+ cocoapods-downloader (>= 2.1, < 3.0)
+ cocoapods-plugins (>= 1.0.0, < 2.0)
+ cocoapods-search (>= 1.0.0, < 2.0)
+ cocoapods-trunk (>= 1.6.0, < 2.0)
+ cocoapods-try (>= 1.1.0, < 2.0)
+ colored2 (~> 3.1)
+ escape (~> 0.0.4)
+ fourflusher (>= 2.3.0, < 3.0)
+ gh_inspector (~> 1.0)
+ molinillo (~> 0.8.0)
+ nap (~> 1.0)
+ ruby-macho (>= 2.3.0, < 3.0)
+ xcodeproj (>= 1.23.0, < 2.0)
+ cocoapods-core (1.15.2)
+ activesupport (>= 5.0, < 8)
+ addressable (~> 2.8)
+ algoliasearch (~> 1.0)
+ concurrent-ruby (~> 1.1)
+ fuzzy_match (~> 2.0.4)
+ nap (~> 1.0)
+ netrc (~> 0.11)
+ public_suffix (~> 4.0)
+ typhoeus (~> 1.0)
+ cocoapods-deintegrate (1.0.5)
+ cocoapods-downloader (2.1)
+ cocoapods-plugins (1.0.0)
+ nap
+ cocoapods-search (1.0.1)
+ cocoapods-trunk (1.6.0)
+ nap (>= 0.8, < 2.0)
+ netrc (~> 0.11)
+ cocoapods-try (1.2.0)
+ colored2 (3.1.2)
+ concurrent-ruby (1.3.3)
+ escape (0.0.4)
+ ethon (0.15.0)
+ ffi (>= 1.15.0)
+ ffi (1.17.3)
+ fourflusher (2.3.1)
+ fuzzy_match (2.0.4)
+ gh_inspector (1.1.3)
+ httpclient (2.9.0)
+ mutex_m
+ i18n (1.14.8)
+ concurrent-ruby (~> 1.0)
+ json (2.7.6)
+ logger (1.7.0)
+ minitest (5.25.4)
+ molinillo (0.8.0)
+ mutex_m (0.3.0)
+ nanaimo (0.3.0)
+ nap (1.1.0)
+ netrc (0.11.0)
+ public_suffix (4.0.7)
+ rexml (3.4.4)
+ ruby-macho (2.5.1)
+ typhoeus (1.5.0)
+ ethon (>= 0.9.0, < 0.16.0)
+ tzinfo (2.0.6)
+ concurrent-ruby (~> 1.0)
+ xcodeproj (1.25.1)
+ CFPropertyList (>= 2.3.3, < 4.0)
+ atomos (~> 0.1.3)
+ claide (>= 1.0.2, < 2.0)
+ colored2 (~> 3.1)
+ nanaimo (~> 0.3.0)
+ rexml (>= 3.3.6, < 4.0)
+ zeitwerk (2.6.18)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ activesupport (>= 6.1.7.5, != 7.1.0)
+ benchmark
+ bigdecimal
+ cocoapods (>= 1.13, != 1.15.1, != 1.15.0)
+ concurrent-ruby (< 1.3.4)
+ logger
+ mutex_m
+ xcodeproj (< 1.26.0)
+
+BUNDLED WITH
+ 2.5.6
diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle
index 2aafc43..5be6dd9 100644
--- a/apps/mobile/android/app/build.gradle
+++ b/apps/mobile/android/app/build.gradle
@@ -7,15 +7,11 @@ apply plugin: "com.facebook.react"
* By default you don't need to apply any configuration, just uncomment the lines you need.
*/
react {
- /* Folders */
- // The root of your project, i.e. where "package.json" lives. Default is '../..'
- // root = file("../../")
- // The folder where the react-native NPM package is. Default is ../../node_modules/react-native
- // reactNativeDir = file("../../node_modules/react-native")
- // The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
- // codegenDir = file("../../node_modules/@react-native/codegen")
- // The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js
- // cliFile = file("../../node_modules/react-native/cli.js")
+ /* Folders — overridden for monorepo (npm workspaces hoists to root node_modules) */
+ root = file("../../../../")
+ reactNativeDir = file("../../../../node_modules/react-native")
+ codegenDir = file("../../../../node_modules/@react-native/codegen")
+ cliFile = file("../../../../node_modules/react-native/cli.js")
/* Variants */
// The list of variants to that are debuggable. For those we're going to
diff --git a/apps/mobile/android/build.gradle b/apps/mobile/android/build.gradle
index dad99b0..d5d22bf 100644
--- a/apps/mobile/android/build.gradle
+++ b/apps/mobile/android/build.gradle
@@ -18,4 +18,15 @@ buildscript {
}
}
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ // AsyncStorage v3 ships a local Maven repo for shared_storage
+ maven {
+ url("$rootDir/../../../node_modules/@react-native-async-storage/async-storage/android/local_repo")
+ }
+ }
+}
+
apply plugin: "com.facebook.react.rootproject"
diff --git a/apps/mobile/android/settings.gradle b/apps/mobile/android/settings.gradle
index ecb2bdd..af0cb7f 100644
--- a/apps/mobile/android/settings.gradle
+++ b/apps/mobile/android/settings.gradle
@@ -1,6 +1,6 @@
-pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
+pluginManagement { includeBuild("../../../node_modules/@react-native/gradle-plugin") }
plugins { id("com.facebook.react.settings") }
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
rootProject.name = 'com.farscry.app'
include ':app'
-includeBuild('../node_modules/@react-native/gradle-plugin')
+includeBuild('../../../node_modules/@react-native/gradle-plugin')
diff --git a/apps/mobile/ios/Farscry.xcodeproj/project.pbxproj b/apps/mobile/ios/Farscry.xcodeproj/project.pbxproj
index 849dc0e..3769e1a 100644
--- a/apps/mobile/ios/Farscry.xcodeproj/project.pbxproj
+++ b/apps/mobile/ios/Farscry.xcodeproj/project.pbxproj
@@ -11,6 +11,7 @@
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
+ D70DD04924B96B78169E29B7 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@@ -159,6 +160,7 @@
files = (
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
+ D70DD04924B96B78169E29B7 /* PrivacyInfo.xcprivacy in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -271,7 +273,7 @@
"-ObjC",
"-lc++",
);
- PRODUCT_BUNDLE_IDENTIFIER = "com.farscry.app";
+ PRODUCT_BUNDLE_IDENTIFIER = com.farscry.app;
PRODUCT_NAME = Farscry;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -300,7 +302,7 @@
"-ObjC",
"-lc++",
);
- PRODUCT_BUNDLE_IDENTIFIER = "com.farscry.app";
+ PRODUCT_BUNDLE_IDENTIFIER = com.farscry.app;
PRODUCT_NAME = Farscry;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SWIFT_VERSION = 5.0;
@@ -370,6 +372,10 @@
);
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
+ OTHER_CFLAGS = (
+ "$(inherited)",
+ "-DRCT_REMOVE_LEGACY_ARCH=1",
+ );
OTHER_CPLUSPLUSFLAGS = (
"$(OTHER_CFLAGS)",
"-DFOLLY_NO_CONFIG",
@@ -377,8 +383,13 @@
"-DFOLLY_USE_LIBCPP=1",
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
+ "-DRCT_REMOVE_LEGACY_ARCH=1",
);
+ REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native";
SDKROOT = iphoneos;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
+ SWIFT_ENABLE_EXPLICIT_MODULES = NO;
+ USE_HERMES = true;
};
name = Debug;
};
@@ -435,6 +446,10 @@
"\"$(inherited)\"",
);
MTL_ENABLE_DEBUG_INFO = NO;
+ OTHER_CFLAGS = (
+ "$(inherited)",
+ "-DRCT_REMOVE_LEGACY_ARCH=1",
+ );
OTHER_CPLUSPLUSFLAGS = (
"$(OTHER_CFLAGS)",
"-DFOLLY_NO_CONFIG",
@@ -442,8 +457,12 @@
"-DFOLLY_USE_LIBCPP=1",
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
+ "-DRCT_REMOVE_LEGACY_ARCH=1",
);
+ REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native";
SDKROOT = iphoneos;
+ SWIFT_ENABLE_EXPLICIT_MODULES = NO;
+ USE_HERMES = true;
VALIDATE_PRODUCT = YES;
};
name = Release;
diff --git a/apps/mobile/ios/Farscry.xcworkspace/contents.xcworkspacedata b/apps/mobile/ios/Farscry.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..e6d947d
--- /dev/null
+++ b/apps/mobile/ios/Farscry.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/apps/mobile/ios/Farscry/Info.plist b/apps/mobile/ios/Farscry/Info.plist
index 115a20d..e09314b 100644
--- a/apps/mobile/ios/Farscry/Info.plist
+++ b/apps/mobile/ios/Farscry/Info.plist
@@ -28,14 +28,22 @@
NSAppTransportSecurity
-
NSAllowsArbitraryLoads
NSAllowsLocalNetworking
- NSLocationWhenInUseUsageDescription
-
+ NSMicrophoneUsageDescription
+ Farscry needs microphone access for voice and video calls
+ NSCameraUsageDescription
+ Farscry needs camera access for video calls
+ UIBackgroundModes
+
+ voip
+ audio
+
+ RCTNewArchEnabled
+
UILaunchStoryboardName
LaunchScreen
UIRequiredDeviceCapabilities
diff --git a/apps/mobile/ios/Podfile.lock b/apps/mobile/ios/Podfile.lock
new file mode 100644
index 0000000..72d0ecd
--- /dev/null
+++ b/apps/mobile/ios/Podfile.lock
@@ -0,0 +1,2402 @@
+PODS:
+ - AsyncStorage (3.0.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - FBLazyVector (0.84.1)
+ - hermes-engine (250829098.0.9):
+ - hermes-engine/Pre-built (= 250829098.0.9)
+ - hermes-engine/Pre-built (250829098.0.9)
+ - JitsiWebRTC (124.0.2)
+ - RCTDeprecation (0.84.1)
+ - RCTRequired (0.84.1)
+ - RCTSwiftUI (0.84.1)
+ - RCTSwiftUIWrapper (0.84.1):
+ - RCTSwiftUI
+ - RCTTypeSafety (0.84.1):
+ - FBLazyVector (= 0.84.1)
+ - RCTRequired (= 0.84.1)
+ - React-Core (= 0.84.1)
+ - React (0.84.1):
+ - React-Core (= 0.84.1)
+ - React-Core/DevSupport (= 0.84.1)
+ - React-Core/RCTWebSocket (= 0.84.1)
+ - React-RCTActionSheet (= 0.84.1)
+ - React-RCTAnimation (= 0.84.1)
+ - React-RCTBlob (= 0.84.1)
+ - React-RCTImage (= 0.84.1)
+ - React-RCTLinking (= 0.84.1)
+ - React-RCTNetwork (= 0.84.1)
+ - React-RCTSettings (= 0.84.1)
+ - React-RCTText (= 0.84.1)
+ - React-RCTVibration (= 0.84.1)
+ - React-callinvoker (0.84.1)
+ - React-Core (0.84.1):
+ - hermes-engine
+ - RCTDeprecation
+ - React-Core-prebuilt
+ - React-Core/Default (= 0.84.1)
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsinspectorcdp
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactNativeDependencies
+ - Yoga
+ - React-Core-prebuilt (0.84.1):
+ - ReactNativeDependencies
+ - React-Core/CoreModulesHeaders (0.84.1):
+ - hermes-engine
+ - RCTDeprecation
+ - React-Core-prebuilt
+ - React-Core/Default
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsinspectorcdp
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactNativeDependencies
+ - Yoga
+ - React-Core/Default (0.84.1):
+ - hermes-engine
+ - RCTDeprecation
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsinspectorcdp
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactNativeDependencies
+ - Yoga
+ - React-Core/DevSupport (0.84.1):
+ - hermes-engine
+ - RCTDeprecation
+ - React-Core-prebuilt
+ - React-Core/Default (= 0.84.1)
+ - React-Core/RCTWebSocket (= 0.84.1)
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsinspectorcdp
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactNativeDependencies
+ - Yoga
+ - React-Core/RCTActionSheetHeaders (0.84.1):
+ - hermes-engine
+ - RCTDeprecation
+ - React-Core-prebuilt
+ - React-Core/Default
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsinspectorcdp
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactNativeDependencies
+ - Yoga
+ - React-Core/RCTAnimationHeaders (0.84.1):
+ - hermes-engine
+ - RCTDeprecation
+ - React-Core-prebuilt
+ - React-Core/Default
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsinspectorcdp
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactNativeDependencies
+ - Yoga
+ - React-Core/RCTBlobHeaders (0.84.1):
+ - hermes-engine
+ - RCTDeprecation
+ - React-Core-prebuilt
+ - React-Core/Default
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsinspectorcdp
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactNativeDependencies
+ - Yoga
+ - React-Core/RCTImageHeaders (0.84.1):
+ - hermes-engine
+ - RCTDeprecation
+ - React-Core-prebuilt
+ - React-Core/Default
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsinspectorcdp
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactNativeDependencies
+ - Yoga
+ - React-Core/RCTLinkingHeaders (0.84.1):
+ - hermes-engine
+ - RCTDeprecation
+ - React-Core-prebuilt
+ - React-Core/Default
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsinspectorcdp
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactNativeDependencies
+ - Yoga
+ - React-Core/RCTNetworkHeaders (0.84.1):
+ - hermes-engine
+ - RCTDeprecation
+ - React-Core-prebuilt
+ - React-Core/Default
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsinspectorcdp
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactNativeDependencies
+ - Yoga
+ - React-Core/RCTSettingsHeaders (0.84.1):
+ - hermes-engine
+ - RCTDeprecation
+ - React-Core-prebuilt
+ - React-Core/Default
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsinspectorcdp
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactNativeDependencies
+ - Yoga
+ - React-Core/RCTTextHeaders (0.84.1):
+ - hermes-engine
+ - RCTDeprecation
+ - React-Core-prebuilt
+ - React-Core/Default
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsinspectorcdp
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactNativeDependencies
+ - Yoga
+ - React-Core/RCTVibrationHeaders (0.84.1):
+ - hermes-engine
+ - RCTDeprecation
+ - React-Core-prebuilt
+ - React-Core/Default
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsinspectorcdp
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactNativeDependencies
+ - Yoga
+ - React-Core/RCTWebSocket (0.84.1):
+ - hermes-engine
+ - RCTDeprecation
+ - React-Core-prebuilt
+ - React-Core/Default (= 0.84.1)
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsinspectorcdp
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactNativeDependencies
+ - Yoga
+ - React-CoreModules (0.84.1):
+ - RCTTypeSafety (= 0.84.1)
+ - React-Core-prebuilt
+ - React-Core/CoreModulesHeaders (= 0.84.1)
+ - React-debug
+ - React-jsi (= 0.84.1)
+ - React-jsinspector
+ - React-jsinspectorcdp
+ - React-jsinspectortracing
+ - React-NativeModulesApple
+ - React-RCTBlob
+ - React-RCTFBReactNativeSpec
+ - React-RCTImage (= 0.84.1)
+ - React-runtimeexecutor
+ - React-utils
+ - ReactCommon
+ - ReactNativeDependencies
+ - React-cxxreact (0.84.1):
+ - hermes-engine
+ - React-callinvoker (= 0.84.1)
+ - React-Core-prebuilt
+ - React-debug (= 0.84.1)
+ - React-jsi (= 0.84.1)
+ - React-jsinspector
+ - React-jsinspectorcdp
+ - React-jsinspectortracing
+ - React-logger (= 0.84.1)
+ - React-perflogger (= 0.84.1)
+ - React-runtimeexecutor
+ - React-timing (= 0.84.1)
+ - React-utils
+ - ReactNativeDependencies
+ - React-debug (0.84.1)
+ - React-defaultsnativemodule (0.84.1):
+ - hermes-engine
+ - React-Core-prebuilt
+ - React-domnativemodule
+ - React-featureflags
+ - React-featureflagsnativemodule
+ - React-idlecallbacksnativemodule
+ - React-intersectionobservernativemodule
+ - React-jsi
+ - React-jsiexecutor
+ - React-microtasksnativemodule
+ - React-RCTFBReactNativeSpec
+ - React-webperformancenativemodule
+ - ReactNativeDependencies
+ - Yoga
+ - React-domnativemodule (0.84.1):
+ - hermes-engine
+ - React-Core-prebuilt
+ - React-Fabric
+ - React-Fabric/bridging
+ - React-FabricComponents
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-RCTFBReactNativeSpec
+ - React-runtimeexecutor
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - React-Fabric (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-Fabric/animated (= 0.84.1)
+ - React-Fabric/animationbackend (= 0.84.1)
+ - React-Fabric/animations (= 0.84.1)
+ - React-Fabric/attributedstring (= 0.84.1)
+ - React-Fabric/bridging (= 0.84.1)
+ - React-Fabric/componentregistry (= 0.84.1)
+ - React-Fabric/componentregistrynative (= 0.84.1)
+ - React-Fabric/components (= 0.84.1)
+ - React-Fabric/consistency (= 0.84.1)
+ - React-Fabric/core (= 0.84.1)
+ - React-Fabric/dom (= 0.84.1)
+ - React-Fabric/imagemanager (= 0.84.1)
+ - React-Fabric/leakchecker (= 0.84.1)
+ - React-Fabric/mounting (= 0.84.1)
+ - React-Fabric/observers (= 0.84.1)
+ - React-Fabric/scheduler (= 0.84.1)
+ - React-Fabric/telemetry (= 0.84.1)
+ - React-Fabric/templateprocessor (= 0.84.1)
+ - React-Fabric/uimanager (= 0.84.1)
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-Fabric/animated (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-Fabric/animationbackend (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-Fabric/animations (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-Fabric/attributedstring (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-Fabric/bridging (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-Fabric/componentregistry (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-Fabric/componentregistrynative (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-Fabric/components (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-Fabric/components/legacyviewmanagerinterop (= 0.84.1)
+ - React-Fabric/components/root (= 0.84.1)
+ - React-Fabric/components/scrollview (= 0.84.1)
+ - React-Fabric/components/view (= 0.84.1)
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-Fabric/components/legacyviewmanagerinterop (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-Fabric/components/root (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-Fabric/components/scrollview (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-Fabric/components/view (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-renderercss
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - React-Fabric/consistency (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-Fabric/core (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-Fabric/dom (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-Fabric/imagemanager (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-Fabric/leakchecker (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-Fabric/mounting (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-Fabric/observers (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-Fabric/observers/events (= 0.84.1)
+ - React-Fabric/observers/intersection (= 0.84.1)
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-Fabric/observers/events (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-Fabric/observers/intersection (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-Fabric/scheduler (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-Fabric/observers/events
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-performancecdpmetrics
+ - React-performancetimeline
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-Fabric/telemetry (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-Fabric/templateprocessor (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-Fabric/uimanager (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-Fabric/uimanager/consistency (= 0.84.1)
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererconsistency
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-Fabric/uimanager/consistency (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererconsistency
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-FabricComponents (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-FabricComponents/components (= 0.84.1)
+ - React-FabricComponents/textlayoutmanager (= 0.84.1)
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-RCTFBReactNativeSpec
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - React-FabricComponents/components (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-FabricComponents/components/inputaccessory (= 0.84.1)
+ - React-FabricComponents/components/iostextinput (= 0.84.1)
+ - React-FabricComponents/components/modal (= 0.84.1)
+ - React-FabricComponents/components/rncore (= 0.84.1)
+ - React-FabricComponents/components/safeareaview (= 0.84.1)
+ - React-FabricComponents/components/scrollview (= 0.84.1)
+ - React-FabricComponents/components/switch (= 0.84.1)
+ - React-FabricComponents/components/text (= 0.84.1)
+ - React-FabricComponents/components/textinput (= 0.84.1)
+ - React-FabricComponents/components/unimplementedview (= 0.84.1)
+ - React-FabricComponents/components/virtualview (= 0.84.1)
+ - React-FabricComponents/components/virtualviewexperimental (= 0.84.1)
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-RCTFBReactNativeSpec
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - React-FabricComponents/components/inputaccessory (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-RCTFBReactNativeSpec
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - React-FabricComponents/components/iostextinput (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-RCTFBReactNativeSpec
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - React-FabricComponents/components/modal (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-RCTFBReactNativeSpec
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - React-FabricComponents/components/rncore (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-RCTFBReactNativeSpec
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - React-FabricComponents/components/safeareaview (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-RCTFBReactNativeSpec
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - React-FabricComponents/components/scrollview (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-RCTFBReactNativeSpec
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - React-FabricComponents/components/switch (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-RCTFBReactNativeSpec
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - React-FabricComponents/components/text (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-RCTFBReactNativeSpec
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - React-FabricComponents/components/textinput (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-RCTFBReactNativeSpec
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - React-FabricComponents/components/unimplementedview (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-RCTFBReactNativeSpec
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - React-FabricComponents/components/virtualview (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-RCTFBReactNativeSpec
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - React-FabricComponents/components/virtualviewexperimental (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-RCTFBReactNativeSpec
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - React-FabricComponents/textlayoutmanager (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-RCTFBReactNativeSpec
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - React-FabricImage (0.84.1):
+ - hermes-engine
+ - RCTRequired (= 0.84.1)
+ - RCTTypeSafety (= 0.84.1)
+ - React-Core-prebuilt
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-jsi
+ - React-jsiexecutor (= 0.84.1)
+ - React-logger
+ - React-rendererdebug
+ - React-utils
+ - ReactCommon
+ - ReactNativeDependencies
+ - Yoga
+ - React-featureflags (0.84.1):
+ - React-Core-prebuilt
+ - ReactNativeDependencies
+ - React-featureflagsnativemodule (0.84.1):
+ - hermes-engine
+ - React-Core-prebuilt
+ - React-featureflags
+ - React-jsi
+ - React-jsiexecutor
+ - React-RCTFBReactNativeSpec
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-graphics (0.84.1):
+ - hermes-engine
+ - React-Core-prebuilt
+ - React-jsi
+ - React-jsiexecutor
+ - React-utils
+ - ReactNativeDependencies
+ - React-hermes (0.84.1):
+ - hermes-engine
+ - React-Core-prebuilt
+ - React-cxxreact (= 0.84.1)
+ - React-jsi
+ - React-jsiexecutor (= 0.84.1)
+ - React-jsinspector
+ - React-jsinspectorcdp
+ - React-jsinspectortracing
+ - React-jsitooling
+ - React-oscompat
+ - React-perflogger (= 0.84.1)
+ - React-runtimeexecutor
+ - ReactNativeDependencies
+ - React-idlecallbacksnativemodule (0.84.1):
+ - hermes-engine
+ - React-Core-prebuilt
+ - React-jsi
+ - React-jsiexecutor
+ - React-RCTFBReactNativeSpec
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-ImageManager (0.84.1):
+ - React-Core-prebuilt
+ - React-Core/Default
+ - React-debug
+ - React-Fabric
+ - React-graphics
+ - React-rendererdebug
+ - React-utils
+ - ReactNativeDependencies
+ - React-intersectionobservernativemodule (0.84.1):
+ - hermes-engine
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-Fabric
+ - React-Fabric/bridging
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-RCTFBReactNativeSpec
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - React-jserrorhandler (0.84.1):
+ - hermes-engine
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-jsi
+ - ReactCommon/turbomodule/bridging
+ - ReactNativeDependencies
+ - React-jsi (0.84.1):
+ - hermes-engine
+ - React-Core-prebuilt
+ - ReactNativeDependencies
+ - React-jsiexecutor (0.84.1):
+ - hermes-engine
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-jserrorhandler
+ - React-jsi
+ - React-jsinspector
+ - React-jsinspectorcdp
+ - React-jsinspectortracing
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimeexecutor
+ - React-utils
+ - ReactNativeDependencies
+ - React-jsinspector (0.84.1):
+ - hermes-engine
+ - React-Core-prebuilt
+ - React-featureflags
+ - React-jsi
+ - React-jsinspectorcdp
+ - React-jsinspectornetwork
+ - React-jsinspectortracing
+ - React-oscompat
+ - React-perflogger (= 0.84.1)
+ - React-runtimeexecutor
+ - React-utils
+ - ReactNativeDependencies
+ - React-jsinspectorcdp (0.84.1):
+ - React-Core-prebuilt
+ - ReactNativeDependencies
+ - React-jsinspectornetwork (0.84.1):
+ - React-Core-prebuilt
+ - React-jsinspectorcdp
+ - ReactNativeDependencies
+ - React-jsinspectortracing (0.84.1):
+ - hermes-engine
+ - React-Core-prebuilt
+ - React-jsi
+ - React-jsinspectornetwork
+ - React-oscompat
+ - React-timing
+ - ReactNativeDependencies
+ - React-jsitooling (0.84.1):
+ - hermes-engine
+ - React-Core-prebuilt
+ - React-cxxreact (= 0.84.1)
+ - React-debug
+ - React-jsi (= 0.84.1)
+ - React-jsinspector
+ - React-jsinspectorcdp
+ - React-jsinspectortracing
+ - React-runtimeexecutor
+ - React-utils
+ - ReactNativeDependencies
+ - React-jsitracing (0.84.1):
+ - React-jsi
+ - React-logger (0.84.1):
+ - React-Core-prebuilt
+ - ReactNativeDependencies
+ - React-Mapbuffer (0.84.1):
+ - React-Core-prebuilt
+ - React-debug
+ - ReactNativeDependencies
+ - React-microtasksnativemodule (0.84.1):
+ - hermes-engine
+ - React-Core-prebuilt
+ - React-jsi
+ - React-jsiexecutor
+ - React-RCTFBReactNativeSpec
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - react-native-config (1.6.1):
+ - react-native-config/App (= 1.6.1)
+ - react-native-config/App (1.6.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - react-native-safe-area-context (5.7.0):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-jsi
+ - react-native-safe-area-context/common (= 5.7.0)
+ - react-native-safe-area-context/fabric (= 5.7.0)
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - react-native-safe-area-context/common (5.7.0):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - react-native-safe-area-context/fabric (5.7.0):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-jsi
+ - react-native-safe-area-context/common
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - react-native-webrtc (124.0.7):
+ - JitsiWebRTC (~> 124.0.0)
+ - React-Core
+ - React-NativeModulesApple (0.84.1):
+ - hermes-engine
+ - React-callinvoker
+ - React-Core
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-jsi
+ - React-jsinspector
+ - React-jsinspectorcdp
+ - React-runtimeexecutor
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - React-networking (0.84.1):
+ - React-Core-prebuilt
+ - React-jsinspectornetwork
+ - React-jsinspectortracing
+ - React-performancetimeline
+ - React-timing
+ - ReactNativeDependencies
+ - React-oscompat (0.84.1)
+ - React-perflogger (0.84.1):
+ - React-Core-prebuilt
+ - ReactNativeDependencies
+ - React-performancecdpmetrics (0.84.1):
+ - hermes-engine
+ - React-Core-prebuilt
+ - React-jsi
+ - React-performancetimeline
+ - React-runtimeexecutor
+ - React-timing
+ - ReactNativeDependencies
+ - React-performancetimeline (0.84.1):
+ - React-Core-prebuilt
+ - React-featureflags
+ - React-jsinspector
+ - React-jsinspectortracing
+ - React-perflogger
+ - React-timing
+ - ReactNativeDependencies
+ - React-RCTActionSheet (0.84.1):
+ - React-Core/RCTActionSheetHeaders (= 0.84.1)
+ - React-RCTAnimation (0.84.1):
+ - RCTTypeSafety
+ - React-Core-prebuilt
+ - React-Core/RCTAnimationHeaders
+ - React-debug
+ - React-featureflags
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFBReactNativeSpec
+ - ReactCommon
+ - ReactNativeDependencies
+ - React-RCTAppDelegate (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-CoreModules
+ - React-debug
+ - React-defaultsnativemodule
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsitooling
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-RCTFBReactNativeSpec
+ - React-RCTImage
+ - React-RCTNetwork
+ - React-RCTRuntime
+ - React-rendererdebug
+ - React-RuntimeApple
+ - React-RuntimeCore
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon
+ - ReactNativeDependencies
+ - React-RCTBlob (0.84.1):
+ - hermes-engine
+ - React-Core-prebuilt
+ - React-Core/RCTBlobHeaders
+ - React-Core/RCTWebSocket
+ - React-jsi
+ - React-jsinspector
+ - React-jsinspectorcdp
+ - React-NativeModulesApple
+ - React-RCTFBReactNativeSpec
+ - React-RCTNetwork
+ - ReactCommon
+ - ReactNativeDependencies
+ - React-RCTFabric (0.84.1):
+ - hermes-engine
+ - RCTSwiftUIWrapper
+ - React-Core
+ - React-Core-prebuilt
+ - React-debug
+ - React-Fabric
+ - React-FabricComponents
+ - React-FabricImage
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-jsi
+ - React-jsinspector
+ - React-jsinspectorcdp
+ - React-jsinspectortracing
+ - React-networking
+ - React-performancecdpmetrics
+ - React-performancetimeline
+ - React-RCTAnimation
+ - React-RCTFBReactNativeSpec
+ - React-RCTImage
+ - React-RCTText
+ - React-rendererconsistency
+ - React-renderercss
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactNativeDependencies
+ - Yoga
+ - React-RCTFBReactNativeSpec (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFBReactNativeSpec/components (= 0.84.1)
+ - ReactCommon
+ - ReactNativeDependencies
+ - React-RCTFBReactNativeSpec/components (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-NativeModulesApple
+ - React-rendererdebug
+ - React-utils
+ - ReactCommon
+ - ReactNativeDependencies
+ - Yoga
+ - React-RCTImage (0.84.1):
+ - RCTTypeSafety
+ - React-Core-prebuilt
+ - React-Core/RCTImageHeaders
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFBReactNativeSpec
+ - React-RCTNetwork
+ - ReactCommon
+ - ReactNativeDependencies
+ - React-RCTLinking (0.84.1):
+ - React-Core/RCTLinkingHeaders (= 0.84.1)
+ - React-jsi (= 0.84.1)
+ - React-NativeModulesApple
+ - React-RCTFBReactNativeSpec
+ - ReactCommon
+ - ReactCommon/turbomodule/core (= 0.84.1)
+ - React-RCTNetwork (0.84.1):
+ - RCTTypeSafety
+ - React-Core-prebuilt
+ - React-Core/RCTNetworkHeaders
+ - React-debug
+ - React-featureflags
+ - React-jsi
+ - React-jsinspectorcdp
+ - React-jsinspectornetwork
+ - React-NativeModulesApple
+ - React-networking
+ - React-RCTFBReactNativeSpec
+ - ReactCommon
+ - ReactNativeDependencies
+ - React-RCTRuntime (0.84.1):
+ - hermes-engine
+ - React-Core
+ - React-Core-prebuilt
+ - React-debug
+ - React-jsi
+ - React-jsinspector
+ - React-jsinspectorcdp
+ - React-jsinspectortracing
+ - React-jsitooling
+ - React-RuntimeApple
+ - React-RuntimeCore
+ - React-runtimeexecutor
+ - React-RuntimeHermes
+ - React-utils
+ - ReactNativeDependencies
+ - React-RCTSettings (0.84.1):
+ - RCTTypeSafety
+ - React-Core-prebuilt
+ - React-Core/RCTSettingsHeaders
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFBReactNativeSpec
+ - ReactCommon
+ - ReactNativeDependencies
+ - React-RCTText (0.84.1):
+ - React-Core/RCTTextHeaders (= 0.84.1)
+ - Yoga
+ - React-RCTVibration (0.84.1):
+ - React-Core-prebuilt
+ - React-Core/RCTVibrationHeaders
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFBReactNativeSpec
+ - ReactCommon
+ - ReactNativeDependencies
+ - React-rendererconsistency (0.84.1)
+ - React-renderercss (0.84.1):
+ - React-debug
+ - React-utils
+ - React-rendererdebug (0.84.1):
+ - React-Core-prebuilt
+ - React-debug
+ - ReactNativeDependencies
+ - React-RuntimeApple (0.84.1):
+ - hermes-engine
+ - React-callinvoker
+ - React-Core-prebuilt
+ - React-Core/Default
+ - React-CoreModules
+ - React-cxxreact
+ - React-featureflags
+ - React-jserrorhandler
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsitooling
+ - React-Mapbuffer
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-RCTFBReactNativeSpec
+ - React-RuntimeCore
+ - React-runtimeexecutor
+ - React-RuntimeHermes
+ - React-runtimescheduler
+ - React-utils
+ - ReactNativeDependencies
+ - React-RuntimeCore (0.84.1):
+ - hermes-engine
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-Fabric
+ - React-featureflags
+ - React-jserrorhandler
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsitooling
+ - React-performancetimeline
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - ReactNativeDependencies
+ - React-runtimeexecutor (0.84.1):
+ - React-Core-prebuilt
+ - React-debug
+ - React-featureflags
+ - React-jsi (= 0.84.1)
+ - React-utils
+ - ReactNativeDependencies
+ - React-RuntimeHermes (0.84.1):
+ - hermes-engine
+ - React-Core-prebuilt
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsinspector
+ - React-jsinspectorcdp
+ - React-jsinspectortracing
+ - React-jsitooling
+ - React-jsitracing
+ - React-RuntimeCore
+ - React-runtimeexecutor
+ - React-utils
+ - ReactNativeDependencies
+ - React-runtimescheduler (0.84.1):
+ - hermes-engine
+ - React-callinvoker
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-jsi
+ - React-jsinspectortracing
+ - React-performancetimeline
+ - React-rendererconsistency
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-timing
+ - React-utils
+ - ReactNativeDependencies
+ - React-timing (0.84.1):
+ - React-debug
+ - React-utils (0.84.1):
+ - hermes-engine
+ - React-Core-prebuilt
+ - React-debug
+ - React-jsi (= 0.84.1)
+ - ReactNativeDependencies
+ - React-webperformancenativemodule (0.84.1):
+ - hermes-engine
+ - React-Core-prebuilt
+ - React-cxxreact
+ - React-jsi
+ - React-jsiexecutor
+ - React-performancetimeline
+ - React-RCTFBReactNativeSpec
+ - React-runtimeexecutor
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - ReactAppDependencyProvider (0.84.1):
+ - ReactCodegen
+ - ReactCodegen (0.84.1):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-debug
+ - React-Fabric
+ - React-FabricImage
+ - React-featureflags
+ - React-graphics
+ - React-jsi
+ - React-jsiexecutor
+ - React-NativeModulesApple
+ - React-RCTAppDelegate
+ - React-rendererdebug
+ - React-utils
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - ReactCommon (0.84.1):
+ - React-Core-prebuilt
+ - ReactCommon/turbomodule (= 0.84.1)
+ - ReactNativeDependencies
+ - ReactCommon/turbomodule (0.84.1):
+ - hermes-engine
+ - React-callinvoker (= 0.84.1)
+ - React-Core-prebuilt
+ - React-cxxreact (= 0.84.1)
+ - React-jsi (= 0.84.1)
+ - React-logger (= 0.84.1)
+ - React-perflogger (= 0.84.1)
+ - ReactCommon/turbomodule/bridging (= 0.84.1)
+ - ReactCommon/turbomodule/core (= 0.84.1)
+ - ReactNativeDependencies
+ - ReactCommon/turbomodule/bridging (0.84.1):
+ - hermes-engine
+ - React-callinvoker (= 0.84.1)
+ - React-Core-prebuilt
+ - React-cxxreact (= 0.84.1)
+ - React-jsi (= 0.84.1)
+ - React-logger (= 0.84.1)
+ - React-perflogger (= 0.84.1)
+ - ReactNativeDependencies
+ - ReactCommon/turbomodule/core (0.84.1):
+ - hermes-engine
+ - React-callinvoker (= 0.84.1)
+ - React-Core-prebuilt
+ - React-cxxreact (= 0.84.1)
+ - React-debug (= 0.84.1)
+ - React-featureflags (= 0.84.1)
+ - React-jsi (= 0.84.1)
+ - React-logger (= 0.84.1)
+ - React-perflogger (= 0.84.1)
+ - React-utils (= 0.84.1)
+ - ReactNativeDependencies
+ - ReactNativeDependencies (0.84.1)
+ - ReactNativeIncallManager (4.2.1):
+ - React-Core
+ - RNCallKeep (4.3.16):
+ - React
+ - RNGestureHandler (2.30.0):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - RNPermissions (5.4.4):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - RNScreens (4.24.0):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-RCTImage
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - RNScreens/common (= 4.24.0)
+ - Yoga
+ - RNScreens/common (4.24.0):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-RCTImage
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - RNSVG (15.15.3):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - RNSVG/common (= 15.15.3)
+ - Yoga
+ - RNSVG/common (15.15.3):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
+ - RNVoipPushNotification (3.3.3):
+ - React-Core
+ - Yoga (0.0.0)
+
+DEPENDENCIES:
+ - "AsyncStorage (from `../../../node_modules/@react-native-async-storage/async-storage`)"
+ - FBLazyVector (from `../../../node_modules/react-native/Libraries/FBLazyVector`)
+ - hermes-engine (from `../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
+ - RCTDeprecation (from `../../../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
+ - RCTRequired (from `../../../node_modules/react-native/Libraries/Required`)
+ - RCTSwiftUI (from `../../../node_modules/react-native/ReactApple/RCTSwiftUI`)
+ - RCTSwiftUIWrapper (from `../../../node_modules/react-native/ReactApple/RCTSwiftUIWrapper`)
+ - RCTTypeSafety (from `../../../node_modules/react-native/Libraries/TypeSafety`)
+ - React (from `../../../node_modules/react-native/`)
+ - React-callinvoker (from `../../../node_modules/react-native/ReactCommon/callinvoker`)
+ - React-Core (from `../../../node_modules/react-native/`)
+ - React-Core-prebuilt (from `../../../node_modules/react-native/React-Core-prebuilt.podspec`)
+ - React-Core/RCTWebSocket (from `../../../node_modules/react-native/`)
+ - React-CoreModules (from `../../../node_modules/react-native/React/CoreModules`)
+ - React-cxxreact (from `../../../node_modules/react-native/ReactCommon/cxxreact`)
+ - React-debug (from `../../../node_modules/react-native/ReactCommon/react/debug`)
+ - React-defaultsnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/defaults`)
+ - React-domnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/dom`)
+ - React-Fabric (from `../../../node_modules/react-native/ReactCommon`)
+ - React-FabricComponents (from `../../../node_modules/react-native/ReactCommon`)
+ - React-FabricImage (from `../../../node_modules/react-native/ReactCommon`)
+ - React-featureflags (from `../../../node_modules/react-native/ReactCommon/react/featureflags`)
+ - React-featureflagsnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`)
+ - React-graphics (from `../../../node_modules/react-native/ReactCommon/react/renderer/graphics`)
+ - React-hermes (from `../../../node_modules/react-native/ReactCommon/hermes`)
+ - React-idlecallbacksnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`)
+ - React-ImageManager (from `../../../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`)
+ - React-intersectionobservernativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/intersectionobserver`)
+ - React-jserrorhandler (from `../../../node_modules/react-native/ReactCommon/jserrorhandler`)
+ - React-jsi (from `../../../node_modules/react-native/ReactCommon/jsi`)
+ - React-jsiexecutor (from `../../../node_modules/react-native/ReactCommon/jsiexecutor`)
+ - React-jsinspector (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern`)
+ - React-jsinspectorcdp (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/cdp`)
+ - React-jsinspectornetwork (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/network`)
+ - React-jsinspectortracing (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`)
+ - React-jsitooling (from `../../../node_modules/react-native/ReactCommon/jsitooling`)
+ - React-jsitracing (from `../../../node_modules/react-native/ReactCommon/hermes/executor/`)
+ - React-logger (from `../../../node_modules/react-native/ReactCommon/logger`)
+ - React-Mapbuffer (from `../../../node_modules/react-native/ReactCommon`)
+ - React-microtasksnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
+ - react-native-config (from `../../../node_modules/react-native-config`)
+ - react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`)
+ - react-native-webrtc (from `../../../node_modules/react-native-webrtc`)
+ - React-NativeModulesApple (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
+ - React-networking (from `../../../node_modules/react-native/ReactCommon/react/networking`)
+ - React-oscompat (from `../../../node_modules/react-native/ReactCommon/oscompat`)
+ - React-perflogger (from `../../../node_modules/react-native/ReactCommon/reactperflogger`)
+ - React-performancecdpmetrics (from `../../../node_modules/react-native/ReactCommon/react/performance/cdpmetrics`)
+ - React-performancetimeline (from `../../../node_modules/react-native/ReactCommon/react/performance/timeline`)
+ - React-RCTActionSheet (from `../../../node_modules/react-native/Libraries/ActionSheetIOS`)
+ - React-RCTAnimation (from `../../../node_modules/react-native/Libraries/NativeAnimation`)
+ - React-RCTAppDelegate (from `../../../node_modules/react-native/Libraries/AppDelegate`)
+ - React-RCTBlob (from `../../../node_modules/react-native/Libraries/Blob`)
+ - React-RCTFabric (from `../../../node_modules/react-native/React`)
+ - React-RCTFBReactNativeSpec (from `../../../node_modules/react-native/React`)
+ - React-RCTImage (from `../../../node_modules/react-native/Libraries/Image`)
+ - React-RCTLinking (from `../../../node_modules/react-native/Libraries/LinkingIOS`)
+ - React-RCTNetwork (from `../../../node_modules/react-native/Libraries/Network`)
+ - React-RCTRuntime (from `../../../node_modules/react-native/React/Runtime`)
+ - React-RCTSettings (from `../../../node_modules/react-native/Libraries/Settings`)
+ - React-RCTText (from `../../../node_modules/react-native/Libraries/Text`)
+ - React-RCTVibration (from `../../../node_modules/react-native/Libraries/Vibration`)
+ - React-rendererconsistency (from `../../../node_modules/react-native/ReactCommon/react/renderer/consistency`)
+ - React-renderercss (from `../../../node_modules/react-native/ReactCommon/react/renderer/css`)
+ - React-rendererdebug (from `../../../node_modules/react-native/ReactCommon/react/renderer/debug`)
+ - React-RuntimeApple (from `../../../node_modules/react-native/ReactCommon/react/runtime/platform/ios`)
+ - React-RuntimeCore (from `../../../node_modules/react-native/ReactCommon/react/runtime`)
+ - React-runtimeexecutor (from `../../../node_modules/react-native/ReactCommon/runtimeexecutor`)
+ - React-RuntimeHermes (from `../../../node_modules/react-native/ReactCommon/react/runtime`)
+ - React-runtimescheduler (from `../../../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`)
+ - React-timing (from `../../../node_modules/react-native/ReactCommon/react/timing`)
+ - React-utils (from `../../../node_modules/react-native/ReactCommon/react/utils`)
+ - React-webperformancenativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/webperformance`)
+ - ReactAppDependencyProvider (from `build/generated/ios/ReactAppDependencyProvider`)
+ - ReactCodegen (from `build/generated/ios/ReactCodegen`)
+ - ReactCommon/turbomodule/core (from `../../../node_modules/react-native/ReactCommon`)
+ - ReactNativeDependencies (from `../../../node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec`)
+ - ReactNativeIncallManager (from `../../../node_modules/react-native-incall-manager`)
+ - RNCallKeep (from `../../../node_modules/react-native-callkeep`)
+ - RNGestureHandler (from `../../../node_modules/react-native-gesture-handler`)
+ - RNPermissions (from `../../../node_modules/react-native-permissions`)
+ - RNScreens (from `../../../node_modules/react-native-screens`)
+ - RNSVG (from `../../../node_modules/react-native-svg`)
+ - RNVoipPushNotification (from `../../../node_modules/react-native-voip-push-notification`)
+ - Yoga (from `../../../node_modules/react-native/ReactCommon/yoga`)
+
+SPEC REPOS:
+ trunk:
+ - JitsiWebRTC
+
+EXTERNAL SOURCES:
+ AsyncStorage:
+ :path: "../../../node_modules/@react-native-async-storage/async-storage"
+ FBLazyVector:
+ :path: "../../../node_modules/react-native/Libraries/FBLazyVector"
+ hermes-engine:
+ :podspec: "../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
+ :tag: hermes-v250829098.0.9
+ RCTDeprecation:
+ :path: "../../../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation"
+ RCTRequired:
+ :path: "../../../node_modules/react-native/Libraries/Required"
+ RCTSwiftUI:
+ :path: "../../../node_modules/react-native/ReactApple/RCTSwiftUI"
+ RCTSwiftUIWrapper:
+ :path: "../../../node_modules/react-native/ReactApple/RCTSwiftUIWrapper"
+ RCTTypeSafety:
+ :path: "../../../node_modules/react-native/Libraries/TypeSafety"
+ React:
+ :path: "../../../node_modules/react-native/"
+ React-callinvoker:
+ :path: "../../../node_modules/react-native/ReactCommon/callinvoker"
+ React-Core:
+ :path: "../../../node_modules/react-native/"
+ React-Core-prebuilt:
+ :podspec: "../../../node_modules/react-native/React-Core-prebuilt.podspec"
+ React-CoreModules:
+ :path: "../../../node_modules/react-native/React/CoreModules"
+ React-cxxreact:
+ :path: "../../../node_modules/react-native/ReactCommon/cxxreact"
+ React-debug:
+ :path: "../../../node_modules/react-native/ReactCommon/react/debug"
+ React-defaultsnativemodule:
+ :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/defaults"
+ React-domnativemodule:
+ :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/dom"
+ React-Fabric:
+ :path: "../../../node_modules/react-native/ReactCommon"
+ React-FabricComponents:
+ :path: "../../../node_modules/react-native/ReactCommon"
+ React-FabricImage:
+ :path: "../../../node_modules/react-native/ReactCommon"
+ React-featureflags:
+ :path: "../../../node_modules/react-native/ReactCommon/react/featureflags"
+ React-featureflagsnativemodule:
+ :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/featureflags"
+ React-graphics:
+ :path: "../../../node_modules/react-native/ReactCommon/react/renderer/graphics"
+ React-hermes:
+ :path: "../../../node_modules/react-native/ReactCommon/hermes"
+ React-idlecallbacksnativemodule:
+ :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks"
+ React-ImageManager:
+ :path: "../../../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios"
+ React-intersectionobservernativemodule:
+ :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/intersectionobserver"
+ React-jserrorhandler:
+ :path: "../../../node_modules/react-native/ReactCommon/jserrorhandler"
+ React-jsi:
+ :path: "../../../node_modules/react-native/ReactCommon/jsi"
+ React-jsiexecutor:
+ :path: "../../../node_modules/react-native/ReactCommon/jsiexecutor"
+ React-jsinspector:
+ :path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern"
+ React-jsinspectorcdp:
+ :path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/cdp"
+ React-jsinspectornetwork:
+ :path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/network"
+ React-jsinspectortracing:
+ :path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/tracing"
+ React-jsitooling:
+ :path: "../../../node_modules/react-native/ReactCommon/jsitooling"
+ React-jsitracing:
+ :path: "../../../node_modules/react-native/ReactCommon/hermes/executor/"
+ React-logger:
+ :path: "../../../node_modules/react-native/ReactCommon/logger"
+ React-Mapbuffer:
+ :path: "../../../node_modules/react-native/ReactCommon"
+ React-microtasksnativemodule:
+ :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
+ react-native-config:
+ :path: "../../../node_modules/react-native-config"
+ react-native-safe-area-context:
+ :path: "../../../node_modules/react-native-safe-area-context"
+ react-native-webrtc:
+ :path: "../../../node_modules/react-native-webrtc"
+ React-NativeModulesApple:
+ :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios"
+ React-networking:
+ :path: "../../../node_modules/react-native/ReactCommon/react/networking"
+ React-oscompat:
+ :path: "../../../node_modules/react-native/ReactCommon/oscompat"
+ React-perflogger:
+ :path: "../../../node_modules/react-native/ReactCommon/reactperflogger"
+ React-performancecdpmetrics:
+ :path: "../../../node_modules/react-native/ReactCommon/react/performance/cdpmetrics"
+ React-performancetimeline:
+ :path: "../../../node_modules/react-native/ReactCommon/react/performance/timeline"
+ React-RCTActionSheet:
+ :path: "../../../node_modules/react-native/Libraries/ActionSheetIOS"
+ React-RCTAnimation:
+ :path: "../../../node_modules/react-native/Libraries/NativeAnimation"
+ React-RCTAppDelegate:
+ :path: "../../../node_modules/react-native/Libraries/AppDelegate"
+ React-RCTBlob:
+ :path: "../../../node_modules/react-native/Libraries/Blob"
+ React-RCTFabric:
+ :path: "../../../node_modules/react-native/React"
+ React-RCTFBReactNativeSpec:
+ :path: "../../../node_modules/react-native/React"
+ React-RCTImage:
+ :path: "../../../node_modules/react-native/Libraries/Image"
+ React-RCTLinking:
+ :path: "../../../node_modules/react-native/Libraries/LinkingIOS"
+ React-RCTNetwork:
+ :path: "../../../node_modules/react-native/Libraries/Network"
+ React-RCTRuntime:
+ :path: "../../../node_modules/react-native/React/Runtime"
+ React-RCTSettings:
+ :path: "../../../node_modules/react-native/Libraries/Settings"
+ React-RCTText:
+ :path: "../../../node_modules/react-native/Libraries/Text"
+ React-RCTVibration:
+ :path: "../../../node_modules/react-native/Libraries/Vibration"
+ React-rendererconsistency:
+ :path: "../../../node_modules/react-native/ReactCommon/react/renderer/consistency"
+ React-renderercss:
+ :path: "../../../node_modules/react-native/ReactCommon/react/renderer/css"
+ React-rendererdebug:
+ :path: "../../../node_modules/react-native/ReactCommon/react/renderer/debug"
+ React-RuntimeApple:
+ :path: "../../../node_modules/react-native/ReactCommon/react/runtime/platform/ios"
+ React-RuntimeCore:
+ :path: "../../../node_modules/react-native/ReactCommon/react/runtime"
+ React-runtimeexecutor:
+ :path: "../../../node_modules/react-native/ReactCommon/runtimeexecutor"
+ React-RuntimeHermes:
+ :path: "../../../node_modules/react-native/ReactCommon/react/runtime"
+ React-runtimescheduler:
+ :path: "../../../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler"
+ React-timing:
+ :path: "../../../node_modules/react-native/ReactCommon/react/timing"
+ React-utils:
+ :path: "../../../node_modules/react-native/ReactCommon/react/utils"
+ React-webperformancenativemodule:
+ :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/webperformance"
+ ReactAppDependencyProvider:
+ :path: build/generated/ios/ReactAppDependencyProvider
+ ReactCodegen:
+ :path: build/generated/ios/ReactCodegen
+ ReactCommon:
+ :path: "../../../node_modules/react-native/ReactCommon"
+ ReactNativeDependencies:
+ :podspec: "../../../node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec"
+ ReactNativeIncallManager:
+ :path: "../../../node_modules/react-native-incall-manager"
+ RNCallKeep:
+ :path: "../../../node_modules/react-native-callkeep"
+ RNGestureHandler:
+ :path: "../../../node_modules/react-native-gesture-handler"
+ RNPermissions:
+ :path: "../../../node_modules/react-native-permissions"
+ RNScreens:
+ :path: "../../../node_modules/react-native-screens"
+ RNSVG:
+ :path: "../../../node_modules/react-native-svg"
+ RNVoipPushNotification:
+ :path: "../../../node_modules/react-native-voip-push-notification"
+ Yoga:
+ :path: "../../../node_modules/react-native/ReactCommon/yoga"
+
+SPEC CHECKSUMS:
+ AsyncStorage: 0a927dc82ea8eaa0350779b37d73b11d070ea677
+ FBLazyVector: e97c19a5a442429d1988f182a1940fb08df514da
+ hermes-engine: 804c3c2d60b4d0e84c847adbe8006ed6074bcaa2
+ JitsiWebRTC: b47805ab5668be38e7ee60e2258f49badfe8e1d0
+ RCTDeprecation: af44b104091a34482596cd9bd7e8d90c4e9b4bd7
+ RCTRequired: bb77b070f75f53398ce43c0aaaa58337cebe2bf6
+ RCTSwiftUI: afc0a0a635860da1040a0b894bfd529da06d7810
+ RCTSwiftUIWrapper: cbb32eb90f09bd42ea9ed1eecd51fef3294da673
+ RCTTypeSafety: d13e192a37f151ce354641184bf4239844a3be17
+ React: 1ba7d364ade7d883a1ec055bfc3606f35fdee17b
+ React-callinvoker: bc2a26f8d84fb01f003fc6de6c9337b64715f95b
+ React-Core: bdaa87b276ca31877632a982ecf7c36f8c826414
+ React-Core-prebuilt: 67f423ba104169c581889bd3f9a6cdcbe1530b18
+ React-CoreModules: b24989f62d56390ae08ca4f65e6f38fe6802de42
+ React-cxxreact: 1a2dfcbc18a6b610664dba152adf327f063a0d12
+ React-debug: 755200a6e7f5e6e0a40ff8d215493d43cce285fc
+ React-defaultsnativemodule: 027cad46a2847719b5d3d20dd915463b06a5d4d1
+ React-domnativemodule: 5ddfc6b3b73b48a31dfa12f52d6b62527f6f260c
+ React-Fabric: 6ffcc768e2378e84ed428069c7e2d270ee78f2bf
+ React-FabricComponents: ee6614287222dd4f04fdb1263d1ae6eb7fe952c6
+ React-FabricImage: ab05740a08ad9e23e4e1701e9c354e9a9b048063
+ React-featureflags: a8b0c8d9a93b5903f7620408659de160d95e4efe
+ React-featureflagsnativemodule: 0f0fe1a044829f31d7565a4bdfded376fbcfdfc1
+ React-graphics: c497dd295c88729525a4752d524d2d783aa205d4
+ React-hermes: c2bde95033e6df1599b5c1b6d7e45736a8aa5cba
+ React-idlecallbacksnativemodule: 6ceacabe93be052bbe822fb018602f63a8e280e2
+ React-ImageManager: 820fe1d55add59ec053099a0c5abe830ecd6c699
+ React-intersectionobservernativemodule: f84958aaf662f95f837dc4d26cbb5e7dcc4b8f09
+ React-jserrorhandler: 390c6c46e2f639b5ba104385d7fba848396347e8
+ React-jsi: 382de7964299bbf878458006a14f52cb66a36cfc
+ React-jsiexecutor: b781400a9becfb24e36ac063dccb42a52dcb44ca
+ React-jsinspector: 0644f32cc9b09eae2bc845ceb58d03420ae70821
+ React-jsinspectorcdp: 96677569865afe25c737889e02d635db26131d9f
+ React-jsinspectornetwork: 28c7cac2e92b1739561dcffd07f5554e54050a85
+ React-jsinspectortracing: 58ee96f9580a143011f8b914ad6927b5116461a7
+ React-jsitooling: bc79639489d610c35731dd26e8e54c37e078996d
+ React-jsitracing: 1bb9fae4f2ccf891255a419cdfc13372d07ef4a5
+ React-logger: 517377b1d2ba7ac722d47fb2183b98de86632063
+ React-Mapbuffer: 45e088dfb58dc326ae20cca1814d3726553c4cad
+ React-microtasksnativemodule: ab9d1a05fe1f58ea44a97d307ef1b53463f45a3f
+ react-native-config: 0ac243e38516dbb8908a09eb87d76620a3083886
+ react-native-safe-area-context: 29044d05d61f2c60d0828c373bd0ebe17eed58d0
+ react-native-webrtc: b5062b745a26c99835efdf0d6c027c9b2ee7ddbc
+ React-NativeModulesApple: b94faa2dce6d8c0a9d722ed7ee27b996d28b62d1
+ React-networking: e409d8fb062162da6293e98b77f8d80cf4430e07
+ React-oscompat: ff26abf0ae3e3fdbe47b44224571e3fc7226a573
+ React-perflogger: 757c8c725cc20e94eba406885047f03cf83044fb
+ React-performancecdpmetrics: fec7e28b711c95ccb6fc7e3bb16572d88bcf27ae
+ React-performancetimeline: 4c6102f19df01db35c37a3e63a058cfbf1a056d9
+ React-RCTActionSheet: fc1d5d419856868e7f8c13c14591ed63dadef43a
+ React-RCTAnimation: 1ce166ec15ab1f8eca8ebaae7f8f709d9be6958c
+ React-RCTAppDelegate: c752d93f597168a9a4d5678e9354bbb8d84df6d1
+ React-RCTBlob: 147d41ee9f80cf27fe9b2f7adc1d6d24f68ec3fc
+ React-RCTFabric: 712c4ad749a43712609011d178234c90a17cde12
+ React-RCTFBReactNativeSpec: 032ea8783dc27290ec6b9af9d8df5351847539a2
+ React-RCTImage: fd39f1c478f1e43357bc72c2dbdc2454aafe4035
+ React-RCTLinking: 02ca1c83536dab08130f5db4852f293c53885dd6
+ React-RCTNetwork: 85dc64c530e4b0be7436f9a15b03caba24e9a3a1
+ React-RCTRuntime: c75950caa80e6884cbf0417d8738992256890508
+ React-RCTSettings: df5da31865cc1bab7ef5314e65ca18f6b538d71d
+ React-RCTText: 41587e426883c9a83fd8eb0c57fe328aad4ed57a
+ React-RCTVibration: 8ca2f9839c53416dffb584adb94501431ba7f96e
+ React-rendererconsistency: e91aba4bb482dac127ad955dba6333a8af629c5b
+ React-renderercss: 1f15a79f3cc3c9416902b8f70266408116d93bd0
+ React-rendererdebug: 77dcf1490ee5c0ce141d2b1eaceed02aa0996826
+ React-RuntimeApple: 1074835708500a69770b713f718400137f30ce7a
+ React-RuntimeCore: 148db945742d7ce2985cc35b8ddc61edfdb46e6d
+ React-runtimeexecutor: 5742146dac0f8de9c21f5f703993df249c046d0d
+ React-RuntimeHermes: a5bb378bea92d526341a65afa945a38c9bc787b2
+ React-runtimescheduler: 91838dd32460920ed1b4da68590a2684b784aacc
+ React-timing: 9c0e2b1532317148fa0487bbc3833c1f348981a0
+ React-utils: 2f8dd43fed5c6d881ac5971666bbb34cc4a03fa1
+ React-webperformancenativemodule: afbee7a9fd0b5bf92f6765eb41767f865b293bcc
+ ReactAppDependencyProvider: 26bbf1e26768d08dd965a2b5e372e53f67b21fee
+ ReactCodegen: 439eae7164a2e4d8ad6ee5c9ea31ac8f407b750d
+ ReactCommon: 309419492d417c4cbb87af06f67735afa40ecb9d
+ ReactNativeDependencies: 47a8b90a868f04348dfc51b43aee063b5c214eac
+ ReactNativeIncallManager: 65a85aed033c1d9ec66f98a943cca51c61a210e9
+ RNCallKeep: 94bbe46b807ccf58e9f6ec11bc4d6087323a1954
+ RNGestureHandler: 6d378fd1aa991c7ab62a4215ee6cc417895a6954
+ RNPermissions: 0f534e5ffc883b83ba3c3cbe603481854e21130f
+ RNScreens: 088d923c4327c63c9f8c942cae17a9d038f47d97
+ RNSVG: 13970bfde0ea9c9e10e01ab0d7b4a6cde11fca1b
+ RNVoipPushNotification: a6f7c09e1ca7220e2be1c45e9b6b897c9600024b
+ Yoga: 7c1c3b93e408ac46c7ed64b5641ca7161747378d
+
+PODFILE CHECKSUM: 21e4b7007eed8f5a51d4edb11d3bcab58ee54b32
+
+COCOAPODS: 1.15.2
diff --git a/apps/mobile/metro.config.js b/apps/mobile/metro.config.js
index 2a0a21c..ee3227e 100644
--- a/apps/mobile/metro.config.js
+++ b/apps/mobile/metro.config.js
@@ -1,11 +1,22 @@
+const path = require('path');
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
+const monorepoRoot = path.resolve(__dirname, '../..');
+
/**
* Metro configuration
* https://reactnative.dev/docs/metro
*
* @type {import('@react-native/metro-config').MetroConfig}
*/
-const config = {};
+const config = {
+ watchFolders: [monorepoRoot],
+ resolver: {
+ nodeModulesPaths: [
+ path.resolve(__dirname, 'node_modules'),
+ path.resolve(monorepoRoot, 'node_modules'),
+ ],
+ },
+};
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index 417b194..2d09824 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -10,16 +10,25 @@
"test": "jest"
},
"dependencies": {
+ "@react-native-async-storage/async-storage": "^3.0.1",
"@react-native/new-app-screen": "0.84.1",
"@react-navigation/bottom-tabs": "^7.15.2",
"@react-navigation/native": "^7.1.31",
"@react-navigation/native-stack": "^7.14.2",
+ "@supabase/supabase-js": "^2.98.0",
"react": "19.2.3",
"react-native": "0.84.1",
+ "react-native-callkeep": "^4.3.16",
+ "react-native-config": "^1.6.1",
"react-native-gesture-handler": "^2.30.0",
+ "react-native-incall-manager": "^4.2.1",
+ "react-native-permissions": "^5.4.4",
"react-native-safe-area-context": "^5.5.2",
"react-native-screens": "^4.24.0",
- "react-native-svg": "^15.15.3"
+ "react-native-svg": "^15.15.3",
+ "react-native-url-polyfill": "^3.0.0",
+ "react-native-voip-push-notification": "^3.3.3",
+ "react-native-webrtc": "^124.0.7"
},
"devDependencies": {
"@babel/core": "^7.25.2",
diff --git a/apps/mobile/src/navigation/RootNavigator.tsx b/apps/mobile/src/navigation/RootNavigator.tsx
index 1ace3c2..1db3e21 100644
--- a/apps/mobile/src/navigation/RootNavigator.tsx
+++ b/apps/mobile/src/navigation/RootNavigator.tsx
@@ -1,5 +1,7 @@
-import React, {useState} from 'react';
+import React from 'react';
+import {ActivityIndicator, View} from 'react-native';
import {createNativeStackNavigator} from '@react-navigation/native-stack';
+import {useAuth} from '../stores/authStore';
import {MainTabs} from './MainTabs';
import {LoginScreen} from '../screens/auth/LoginScreen';
import {SignupScreen} from '../screens/auth/SignupScreen';
@@ -30,8 +32,15 @@ function AuthNavigator() {
}
export function RootNavigator() {
- // TODO: replace with real auth state from auth service
- const [isAuthenticated] = useState(false);
+ const {user, loading} = useAuth();
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
return (
- {isAuthenticated ? (
+ {user ? (
<>
) {
const insets = useSafeAreaInsets();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
+ const {signIn, loading, error, clearError} = useAuth();
- function handleLogin() {
- // TODO: wire up auth service
+ async function handleLogin() {
+ await signIn(email, password);
}
return (
@@ -55,19 +58,27 @@ export function LoginScreen({navigation}: AuthScreenProps<'Login'>) {
textContentType="password"
/>
+ {error && (
+ {error}
+ )}
+
- Sign in
+ disabled={!email || !password || loading}>
+ {loading ? (
+
+ ) : (
+ Sign in
+ )}
navigation.navigate('Signup')}
+ onPress={() => { clearError(); navigation.navigate('Signup'); }}
activeOpacity={0.7}>
Don't have an account?{' '}
@@ -128,6 +139,11 @@ const styles = StyleSheet.create({
...typography.headline,
color: colors.white,
},
+ errorText: {
+ ...typography.footnote,
+ color: colors.callRed,
+ textAlign: 'center',
+ },
signupLink: {
alignItems: 'center',
paddingVertical: spacing.lg,
diff --git a/apps/mobile/src/screens/auth/SignupScreen.tsx b/apps/mobile/src/screens/auth/SignupScreen.tsx
index 838681f..350d4ad 100644
--- a/apps/mobile/src/screens/auth/SignupScreen.tsx
+++ b/apps/mobile/src/screens/auth/SignupScreen.tsx
@@ -7,8 +7,10 @@ import {
StyleSheet,
KeyboardAvoidingView,
Platform,
+ ActivityIndicator,
} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
+import {useAuth} from '../../stores/authStore';
import {colors} from '../../theme/colors';
import {typography} from '../../theme/typography';
import {spacing} from '../../theme/spacing';
@@ -19,11 +21,12 @@ export function SignupScreen({navigation}: AuthScreenProps<'Signup'>) {
const [displayName, setDisplayName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
+ const {signUp, loading, error, clearError} = useAuth();
const isValid = displayName.trim() && email && password.length >= 8;
- function handleSignup() {
- // TODO: wire up auth service
+ async function handleSignup() {
+ await signUp(email, password, displayName);
}
return (
@@ -69,19 +72,27 @@ export function SignupScreen({navigation}: AuthScreenProps<'Signup'>) {
textContentType="newPassword"
/>
+ {error && (
+ {error}
+ )}
+
- Create account
+ disabled={!isValid || loading}>
+ {loading ? (
+
+ ) : (
+ Create account
+ )}
navigation.navigate('Login')}
+ onPress={() => { clearError(); navigation.navigate('Login'); }}
activeOpacity={0.7}>
Already have an account?{' '}
@@ -141,6 +152,11 @@ const styles = StyleSheet.create({
...typography.headline,
color: colors.white,
},
+ errorText: {
+ ...typography.footnote,
+ color: colors.callRed,
+ textAlign: 'center',
+ },
loginLink: {
alignItems: 'center',
paddingVertical: spacing.lg,
diff --git a/apps/mobile/src/screens/call/ActiveCallScreen.tsx b/apps/mobile/src/screens/call/ActiveCallScreen.tsx
index 0a8374d..d5e0e09 100644
--- a/apps/mobile/src/screens/call/ActiveCallScreen.tsx
+++ b/apps/mobile/src/screens/call/ActiveCallScreen.tsx
@@ -10,6 +10,7 @@ import {
} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {CallControls} from '../../components/CallControls';
+import {useCallContext} from '../../stores/callStore';
import {colors} from '../../theme/colors';
import {typography} from '../../theme/typography';
import {spacing} from '../../theme/spacing';
@@ -24,6 +25,7 @@ export function ActiveCallScreen({
route,
}: RootStackScreenProps<'ActiveCall'>) {
const insets = useSafeAreaInsets();
+ const {callManager, callState} = useCallContext();
const {contactName} = route.params;
const [muted, setMuted] = useState(false);
@@ -86,6 +88,13 @@ export function ActiveCallScreen({
return () => clearInterval(interval);
}, []);
+ useEffect(() => {
+ if (callState.phase === 'ended') {
+ const timer = setTimeout(() => navigation.goBack(), 500);
+ return () => clearTimeout(timer);
+ }
+ }, [callState.phase, navigation]);
+
function showControls() {
setControlsVisible(true);
controlsOpacity.setValue(1);
@@ -107,8 +116,29 @@ export function ActiveCallScreen({
}
}
+ function handleToggleMute() {
+ const next = !muted;
+ if (callManager) {
+ callManager.mediaService.setMicEnabled(!next);
+ }
+ setMuted(next);
+ }
+
+ function handleToggleCamera() {
+ const next = !cameraOff;
+ if (callManager) {
+ callManager.mediaService.setCameraEnabled(!next);
+ }
+ setCameraOff(next);
+ }
+
+ function handleToggleSpeaker() {
+ setSpeakerOn(s => !s);
+ // Speaker routing is handled by react-native-incall-manager
+ }
+
function handleHangup() {
- navigation.goBack();
+ callManager?.hangup();
}
const minutes = Math.floor(elapsed / 60);
@@ -161,9 +191,9 @@ export function ActiveCallScreen({
muted={muted}
cameraOff={cameraOff}
speakerOn={speakerOn}
- onToggleMute={() => setMuted(m => !m)}
- onToggleCamera={() => setCameraOff(c => !c)}
- onToggleSpeaker={() => setSpeakerOn(s => !s)}
+ onToggleMute={handleToggleMute}
+ onToggleCamera={handleToggleCamera}
+ onToggleSpeaker={handleToggleSpeaker}
onHangup={handleHangup}
/>
diff --git a/apps/mobile/src/screens/call/IncomingCallScreen.tsx b/apps/mobile/src/screens/call/IncomingCallScreen.tsx
index d53013f..186ce0a 100644
--- a/apps/mobile/src/screens/call/IncomingCallScreen.tsx
+++ b/apps/mobile/src/screens/call/IncomingCallScreen.tsx
@@ -3,6 +3,7 @@ import {View, Text, TouchableOpacity, Animated, StyleSheet} from 'react-native';
import Svg, {Path} from 'react-native-svg';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {Avatar} from '../../components/Avatar';
+import {useCallContext} from '../../stores/callStore';
import {colors} from '../../theme/colors';
import {typography} from '../../theme/typography';
import {spacing} from '../../theme/spacing';
@@ -41,6 +42,7 @@ export function IncomingCallScreen({
route,
}: RootStackScreenProps<'IncomingCall'>) {
const insets = useSafeAreaInsets();
+ const {callManager} = useCallContext();
const {callerName} = route.params;
const pulse = useRef(new Animated.Value(1)).current;
@@ -56,6 +58,7 @@ export function IncomingCallScreen({
}, [pulse]);
function handleAccept() {
+ callManager?.acceptCall();
navigation.replace('ActiveCall', {
contactId: route.params.callerId,
contactName: callerName,
@@ -63,6 +66,7 @@ export function IncomingCallScreen({
}
function handleDecline() {
+ callManager?.declineCall();
navigation.goBack();
}
diff --git a/apps/mobile/src/screens/call/OutgoingCallScreen.tsx b/apps/mobile/src/screens/call/OutgoingCallScreen.tsx
index 74aac4f..f0951b8 100644
--- a/apps/mobile/src/screens/call/OutgoingCallScreen.tsx
+++ b/apps/mobile/src/screens/call/OutgoingCallScreen.tsx
@@ -2,6 +2,7 @@ import React, {useEffect, useRef} from 'react';
import {View, Text, TouchableOpacity, Animated, StyleSheet} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {Avatar} from '../../components/Avatar';
+import {useCallContext} from '../../stores/callStore';
import {colors} from '../../theme/colors';
import {typography} from '../../theme/typography';
import {spacing} from '../../theme/spacing';
@@ -12,7 +13,8 @@ export function OutgoingCallScreen({
route,
}: RootStackScreenProps<'OutgoingCall'>) {
const insets = useSafeAreaInsets();
- const {contactName} = route.params;
+ const {callManager, callState} = useCallContext();
+ const {contactId, contactName} = route.params;
const dot1 = useRef(new Animated.Value(0.3)).current;
const dot2 = useRef(new Animated.Value(0.3)).current;
@@ -43,7 +45,16 @@ export function OutgoingCallScreen({
};
}, [dot1, dot2, dot3]);
+ useEffect(() => {
+ if (callState.phase === 'connecting' || callState.phase === 'active') {
+ navigation.replace('ActiveCall', {contactId, contactName});
+ } else if (callState.phase === 'ended') {
+ navigation.goBack();
+ }
+ }, [callState.phase, navigation, contactId, contactName]);
+
function handleCancel() {
+ callManager?.cancelCall();
navigation.goBack();
}
diff --git a/apps/mobile/src/screens/contacts/AddContactScreen.tsx b/apps/mobile/src/screens/contacts/AddContactScreen.tsx
index 8619de2..5b0c0ea 100644
--- a/apps/mobile/src/screens/contacts/AddContactScreen.tsx
+++ b/apps/mobile/src/screens/contacts/AddContactScreen.tsx
@@ -1,47 +1,46 @@
import React, {useState} from 'react';
-import {View, Text, TouchableOpacity, FlatList, StyleSheet} from 'react-native';
+import {View, Text, TouchableOpacity, FlatList, StyleSheet, Alert} from 'react-native';
+import {useNavigation} from '@react-navigation/native';
import {SearchBar} from '../../components/SearchBar';
import {Avatar} from '../../components/Avatar';
import {EmptyState} from '../../components/EmptyState';
+import {UserService, type UserProfile} from '../../services/user/UserService';
+import {useContacts} from '../../stores/contactsStore';
import {colors} from '../../theme/colors';
import {typography} from '../../theme/typography';
import {spacing} from '../../theme/spacing';
-type SearchResult = {
- id: string;
- name: string;
- username: string;
-};
-
-const MOCK_RESULTS: SearchResult[] = [
- {id: '10', name: 'Sarah Lin', username: 'sarahlin'},
- {id: '11', name: 'Omar Hassan', username: 'omarh'},
- {id: '12', name: 'Yuki Tanaka', username: 'yukitan'},
-];
-
export function AddContactScreen() {
+ const navigation = useNavigation();
const [query, setQuery] = useState('');
- const [results, setResults] = useState([]);
+ const [results, setResults] = useState([]);
const [searched, setSearched] = useState(false);
+ const {addContact} = useContacts();
- function handleSearch(text: string) {
+ async function handleSearch(text: string) {
setQuery(text);
if (text.trim().length >= 2) {
- const q = text.toLowerCase();
- setResults(
- MOCK_RESULTS.filter(
- r => r.name.toLowerCase().includes(q) || r.username.toLowerCase().includes(q),
- ),
- );
- setSearched(true);
+ try {
+ const users = await UserService.searchUsers(text);
+ setResults(users);
+ setSearched(true);
+ } catch {
+ setResults([]);
+ setSearched(true);
+ }
} else {
setResults([]);
setSearched(false);
}
}
- function handleAdd(_user: SearchResult) {
- // TODO: wire up contact service
+ async function handleAdd(user: UserProfile) {
+ try {
+ await addContact(user.id);
+ navigation.goBack();
+ } catch (e: unknown) {
+ Alert.alert('Error', e instanceof Error ? e.message : 'Failed to add contact');
+ }
}
return (
@@ -50,14 +49,14 @@ export function AddContactScreen() {
{!searched ? (
) : results.length === 0 ? (
item.id}
renderItem={({item}) => (
-
+
- {item.name}
- @{item.username}
+ {item.display_name}
) {
const insets = useSafeAreaInsets();
const {contactId, name} = route.params;
- const [isFavorite, setIsFavorite] = useState(false);
+ const {startCall} = useCallContext();
+ const {contacts, removeContact, toggleFavorite} = useContacts();
+
+ const contact = contacts.find(c => c.contact_user_id === contactId);
+ const isFavorite = contact?.is_favorite ?? false;
function handleCall() {
- navigation.navigate('OutgoingCall', {contactId, contactName: name});
+ startCall(contactId, name);
}
function handleRemove() {
@@ -44,8 +50,8 @@ export function ContactDetailScreen({
{
text: 'Remove',
style: 'destructive',
- onPress: () => {
- // TODO: wire up contact service
+ onPress: async () => {
+ await removeContact(contactId);
navigation.goBack();
},
},
@@ -63,14 +69,13 @@ export function ContactDetailScreen({
{name}
- @{name.toLowerCase().replace(/\s/g, '')}
setIsFavorite(f => !f)}
+ onPress={() => toggleFavorite(contactId)}
activeOpacity={0.7}>
@@ -107,10 +112,6 @@ const styles = StyleSheet.create({
color: colors.text,
marginTop: spacing.md,
},
- username: {
- ...typography.body,
- color: colors.textSecondary,
- },
actions: {
flexDirection: 'row',
alignItems: 'center',
diff --git a/apps/mobile/src/screens/main/ContactsScreen.tsx b/apps/mobile/src/screens/main/ContactsScreen.tsx
index 8bdf09b..6783b25 100644
--- a/apps/mobile/src/screens/main/ContactsScreen.tsx
+++ b/apps/mobile/src/screens/main/ContactsScreen.tsx
@@ -1,33 +1,17 @@
-import React, {useMemo, useState} from 'react';
+import React, {useEffect, useMemo, useState} from 'react';
import {View, Text, SectionList, TouchableOpacity, StyleSheet} from 'react-native';
import Svg, {Path} from 'react-native-svg';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {ContactRow} from '../../components/ContactRow';
import {SearchBar} from '../../components/SearchBar';
import {EmptyState} from '../../components/EmptyState';
+import {useContacts} from '../../stores/contactsStore';
+import type {Contact} from '../../services/user/ContactsService';
import {colors} from '../../theme/colors';
import {typography} from '../../theme/typography';
import {spacing} from '../../theme/spacing';
import type {MainTabScreenProps} from '../../navigation/types';
-type Contact = {
- id: string;
- name: string;
- username: string;
- online: boolean;
-};
-
-const MOCK_CONTACTS: Contact[] = [
- {id: '1', name: 'Alice Chen', username: 'alice', online: true},
- {id: '2', name: 'Ben Torres', username: 'bentorres', online: false},
- {id: '3', name: 'Clara Reyes', username: 'clarareyes', online: true},
- {id: '4', name: 'David Kim', username: 'dkim', online: false},
- {id: '5', name: 'Emma Wilson', username: 'emmaw', online: false},
- {id: '6', name: 'James Ko', username: 'jamesko', online: false},
- {id: '7', name: 'Marcus Wright', username: 'marcusw', online: true},
- {id: '8', name: 'Priya Sharma', username: 'priya', online: true},
-];
-
function PlusIcon() {
return (
+ );
+ }}
/>
);
diff --git a/apps/mobile/src/screens/main/SettingsScreen.tsx b/apps/mobile/src/screens/main/SettingsScreen.tsx
index b96af18..db28c15 100644
--- a/apps/mobile/src/screens/main/SettingsScreen.tsx
+++ b/apps/mobile/src/screens/main/SettingsScreen.tsx
@@ -1,7 +1,9 @@
-import React from 'react';
-import {View, Text, ScrollView, TouchableOpacity, Switch, StyleSheet} from 'react-native';
+import React, {useEffect, useState, useCallback} from 'react';
+import {View, Text, ScrollView, TouchableOpacity, Switch, StyleSheet, Alert, TextInput} from 'react-native';
import Svg, {Path} from 'react-native-svg';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
+import {useAuth} from '../../stores/authStore';
+import {UserService, type UserProfile} from '../../services/user/UserService';
import {colors} from '../../theme/colors';
import {typography} from '../../theme/typography';
import {spacing} from '../../theme/spacing';
@@ -54,6 +56,43 @@ function SectionHeader({title}: {title: string}) {
export function SettingsScreen() {
const insets = useSafeAreaInsets();
+ const {user, signOut} = useAuth();
+ const [profile, setProfile] = useState(null);
+ const [editingName, setEditingName] = useState(false);
+ const [nameInput, setNameInput] = useState('');
+
+ useEffect(() => {
+ if (user?.id) {
+ UserService.getProfile(user.id).then(setProfile).catch(() => {});
+ }
+ }, [user?.id]);
+
+ const handleSignOut = useCallback(() => {
+ Alert.alert('Sign out', 'Are you sure?', [
+ {text: 'Cancel', style: 'cancel'},
+ {text: 'Sign out', style: 'destructive', onPress: () => signOut()},
+ ]);
+ }, [signOut]);
+
+ const handleEditName = useCallback(() => {
+ setNameInput(profile?.display_name ?? '');
+ setEditingName(true);
+ }, [profile?.display_name]);
+
+ const handleSaveName = useCallback(async () => {
+ const trimmed = nameInput.trim();
+ if (!trimmed || trimmed === profile?.display_name) {
+ setEditingName(false);
+ return;
+ }
+ try {
+ const updated = await UserService.updateProfile({display_name: trimmed});
+ setProfile(updated);
+ } catch (e: unknown) {
+ Alert.alert('Error', e instanceof Error ? e.message : 'Failed to update name');
+ }
+ setEditingName(false);
+ }, [nameInput, profile?.display_name]);
return (
- {}} />
+
} />
@@ -74,24 +113,43 @@ export function SettingsScreen() {
} />
- {}} />
+
- {}} />
- {}} />
- {}} />
+ {editingName ? (
+
+ Display name
+
+
+ ) : (
+
+ )}
+
- {}} />
- {}} />
-
+
Sign out
@@ -156,4 +214,12 @@ const styles = StyleSheet.create({
color: colors.callRed,
fontWeight: '600',
},
+ nameInput: {
+ ...typography.body,
+ color: colors.text,
+ textAlign: 'right',
+ flex: 1,
+ marginLeft: spacing.base,
+ padding: 0,
+ },
});
diff --git a/apps/mobile/src/services/auth/AuthService.ts b/apps/mobile/src/services/auth/AuthService.ts
index 2c4e4ee..6f10675 100644
--- a/apps/mobile/src/services/auth/AuthService.ts
+++ b/apps/mobile/src/services/auth/AuthService.ts
@@ -1,30 +1,5 @@
-/*
- Supabase schema:
-
- users
- id uuid primary key (matches auth.users.id)
- display_name text not null
- avatar_url text
- created_at timestamptz default now()
- updated_at timestamptz default now()
-
- contacts
- user_id uuid references users(id)
- contact_user_id uuid references users(id)
- is_favorite boolean default false
- added_at timestamptz default now()
- primary key (user_id, contact_user_id)
-
- push_tokens
- user_id uuid references users(id)
- token text not null
- platform text not null -- 'ios' | 'android'
- voip_token text
- updated_at timestamptz default now()
-*/
-
-import {supabase, type SupabaseSession} from '../supabase/client';
-import {SessionManager} from './SessionManager';
+import {supabase} from '../supabase/client';
+import type {Session} from '@supabase/supabase-js';
export type AuthUser = {
id: string;
@@ -33,7 +8,7 @@ export type AuthUser = {
export type AuthState = {
user: AuthUser | null;
- session: SupabaseSession | null;
+ session: Session | null;
};
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -70,20 +45,24 @@ export const AuthService = {
const {data, error} = await supabase.auth.signUp({
email: cleanEmail,
password,
+ options: {
+ data: {display_name: name},
+ },
});
if (error) throw new Error(error.message);
if (!data.user) throw new Error('Sign up failed');
- // Create user profile row
+ // Update the user profile row created by the DB trigger
+ // The trigger creates a row with email prefix as display_name,
+ // so we update it with the user's chosen name
const {error: profileError} = await supabase
.from('users')
- .insert({id: data.user.id, display_name: name});
+ .update({display_name: name})
+ .eq('id', data.user.id);
- if (profileError) throw new Error(profileError.message);
-
- if (data.session) {
- await SessionManager.persistSession(data.session);
+ if (profileError) {
+ console.warn('Failed to update display name:', profileError.message);
}
return {
@@ -104,8 +83,6 @@ export const AuthService = {
if (error) throw new Error(error.message);
if (!data.session) throw new Error('Sign in failed');
- await SessionManager.persistSession(data.session);
-
return {
user: {id: data.session.user.id, email: cleanEmail},
session: data.session,
@@ -113,8 +90,8 @@ export const AuthService = {
},
async signOut(): Promise {
- await supabase.auth.signOut();
- await SessionManager.clearSession();
+ const {error} = await supabase.auth.signOut();
+ if (error) throw new Error(error.message);
},
async resetPassword(email: string): Promise {
@@ -128,38 +105,21 @@ export const AuthService = {
const userId = data.session?.user.id;
if (!userId) throw new Error('Not authenticated');
- // Remove user data in order: push_tokens, contacts, users profile
- await supabase.from('push_tokens').delete().eq('user_id', userId);
- await supabase
- .from('contacts')
- .delete()
- .or(`user_id.eq.${userId},contact_user_id.eq.${userId}`);
- await supabase.from('users').delete().eq('id', userId);
-
- // Delete auth account — requires service-role key on server in production
- await supabase.auth.admin.deleteUser(userId);
- await SessionManager.clearSession();
- },
-
- async getSession(): Promise {
- const {data, error} = await supabase.auth.getSession();
+ // Remove user data — cascade will handle contacts and push_tokens
+ const {error} = await supabase.from('users').delete().eq('id', userId);
if (error) throw new Error(error.message);
- return data.session;
+
+ await supabase.auth.signOut();
},
- async refreshToken(): Promise {
- const {data, error} = await supabase.auth.refreshSession();
+ async getSession(): Promise {
+ const {data, error} = await supabase.auth.getSession();
if (error) throw new Error(error.message);
-
- if (data.session) {
- await SessionManager.persistSession(data.session);
- }
-
return data.session;
},
onAuthStateChange(
- callback: (event: string, session: SupabaseSession | null) => void,
+ callback: (event: string, session: Session | null) => void,
): {unsubscribe: () => void} {
const {data} = supabase.auth.onAuthStateChange(callback);
return data.subscription;
diff --git a/apps/mobile/src/services/auth/SessionManager.ts b/apps/mobile/src/services/auth/SessionManager.ts
index 7343c76..4226330 100644
--- a/apps/mobile/src/services/auth/SessionManager.ts
+++ b/apps/mobile/src/services/auth/SessionManager.ts
@@ -1,40 +1,8 @@
-import AsyncStorage from '@react-native-async-storage/async-storage';
-import type {SupabaseSession} from '../supabase/client';
-
-const SESSION_KEY = '@farscry/session';
-const EXPIRY_BUFFER_MS = 60_000; // refresh 1 minute before expiry
+import type {Session} from '@supabase/supabase-js';
export const SessionManager = {
- async persistSession(session: SupabaseSession): Promise {
- await AsyncStorage.setItem(SESSION_KEY, JSON.stringify(session));
- },
-
- async loadSession(): Promise {
- const raw = await AsyncStorage.getItem(SESSION_KEY);
- if (!raw) return null;
-
- try {
- const session: SupabaseSession = JSON.parse(raw);
- return this.isSessionValid(session) ? session : null;
- } catch {
- await this.clearSession();
- return null;
- }
- },
-
- async clearSession(): Promise {
- await AsyncStorage.removeItem(SESSION_KEY);
- },
-
- isSessionValid(session: SupabaseSession): boolean {
- if (!session.access_token || !session.refresh_token) return false;
- if (!session.expires_at) return false;
- // Expired sessions can still be refreshed, so only reject
- // if we have no refresh token to work with
- return true;
- },
-
- isExpiringSoon(session: SupabaseSession): boolean {
- return session.expires_at * 1000 - Date.now() < EXPIRY_BUFFER_MS;
+ isExpiringSoon(session: Session): boolean {
+ const EXPIRY_BUFFER_MS = 60_000;
+ return (session.expires_at ?? 0) * 1000 - Date.now() < EXPIRY_BUFFER_MS;
},
};
diff --git a/apps/mobile/src/services/native/AudioRouteService.ts b/apps/mobile/src/services/native/AudioRouteService.ts
index af068d6..9164cb6 100644
--- a/apps/mobile/src/services/native/AudioRouteService.ts
+++ b/apps/mobile/src/services/native/AudioRouteService.ts
@@ -1,5 +1,5 @@
import { NativeModules, NativeEventEmitter, Platform } from 'react-native';
-import { InCallManager } from 'react-native-incall-manager';
+import InCallManager from 'react-native-incall-manager';
export type AudioRoute = 'earpiece' | 'speaker' | 'bluetooth' | 'wired';
diff --git a/apps/mobile/src/services/native/CallKeepService.ts b/apps/mobile/src/services/native/CallKeepService.ts
index aa06080..7f53058 100644
--- a/apps/mobile/src/services/native/CallKeepService.ts
+++ b/apps/mobile/src/services/native/CallKeepService.ts
@@ -1,11 +1,5 @@
import { Platform } from 'react-native';
-import RNCallKeep, {
- type SetupOptions,
- type AnswerCallPayload,
- type EndCallPayload,
- type MuteCallPayload,
- type HoldCallPayload,
-} from 'react-native-callkeep';
+import RNCallKeep, { CONSTANTS } from 'react-native-callkeep';
export interface CallKeepEventHandlers {
onAnswerCall: (callUUID: string) => void;
@@ -16,12 +10,12 @@ export interface CallKeepEventHandlers {
onIncomingCallDisplayed: (callUUID: string) => void;
}
-const SETUP_CONFIG: SetupOptions = {
+const SETUP_CONFIG = {
ios: {
appName: 'Farscry',
supportsVideo: true,
- maximumCallGroups: 1,
- maximumCallsPerCallGroup: 1,
+ maximumCallGroups: '1',
+ maximumCallsPerCallGroup: '1',
includesCallsInRecents: true,
},
android: {
@@ -29,6 +23,7 @@ const SETUP_CONFIG: SetupOptions = {
alertDescription: 'Farscry needs phone account access to manage calls',
cancelButton: 'Cancel',
okButton: 'OK',
+ additionalPermissions: [],
selfManaged: true,
foregroundService: {
channelId: 'farscry-call',
@@ -49,7 +44,7 @@ class CallKeepServiceImpl {
await RNCallKeep.setup(SETUP_CONFIG);
if (Platform.OS === 'android') {
- RNCallKeep.registerPhoneAccount();
+ RNCallKeep.registerPhoneAccount(SETUP_CONFIG);
RNCallKeep.registerAndroidEvents();
}
@@ -64,19 +59,19 @@ class CallKeepServiceImpl {
registerEventHandlers(handlers: CallKeepEventHandlers) {
this.handlers = handlers;
- RNCallKeep.addEventListener('answerCall', ({ callUUID }: AnswerCallPayload) => {
+ RNCallKeep.addEventListener('answerCall', ({ callUUID }) => {
this.handlers?.onAnswerCall(callUUID);
});
- RNCallKeep.addEventListener('endCall', ({ callUUID }: EndCallPayload) => {
+ RNCallKeep.addEventListener('endCall', ({ callUUID }) => {
this.handlers?.onEndCall(callUUID);
});
- RNCallKeep.addEventListener('didPerformSetMutedCallAction', ({ callUUID, muted }: MuteCallPayload) => {
+ RNCallKeep.addEventListener('didPerformSetMutedCallAction', ({ callUUID, muted }) => {
this.handlers?.onMuteToggled(callUUID, muted);
});
- RNCallKeep.addEventListener('didToggleHoldCallAction', ({ callUUID, hold }: HoldCallPayload) => {
+ RNCallKeep.addEventListener('didToggleHoldCallAction', ({ callUUID, hold }) => {
this.handlers?.onHoldToggled(callUUID, hold);
});
@@ -84,7 +79,7 @@ class CallKeepServiceImpl {
this.handlers?.onAudioSessionActivated();
});
- RNCallKeep.addEventListener('didDisplayIncomingCall', ({ callUUID }: { callUUID: string }) => {
+ RNCallKeep.addEventListener('didDisplayIncomingCall', ({ callUUID }) => {
this.handlers?.onIncomingCallDisplayed(callUUID);
});
}
@@ -127,10 +122,10 @@ class CallKeepServiceImpl {
reportEndCall(callUUID: string, reason?: 'failed' | 'remote' | 'unanswered' | 'declined') {
const reasonMap = {
- failed: RNCallKeep.END_CALL_REASONS.FAILED,
- remote: RNCallKeep.END_CALL_REASONS.REMOTE_ENDED,
- unanswered: RNCallKeep.END_CALL_REASONS.UNANSWERED,
- declined: RNCallKeep.END_CALL_REASONS.DECLINED_ELSEWHERE,
+ failed: CONSTANTS.END_CALL_REASONS.FAILED,
+ remote: CONSTANTS.END_CALL_REASONS.REMOTE_ENDED,
+ unanswered: CONSTANTS.END_CALL_REASONS.UNANSWERED,
+ declined: CONSTANTS.END_CALL_REASONS.DECLINED_ELSEWHERE,
};
if (reason) {
diff --git a/apps/mobile/src/services/native/PermissionsService.ts b/apps/mobile/src/services/native/PermissionsService.ts
index 131af0b..4b47b40 100644
--- a/apps/mobile/src/services/native/PermissionsService.ts
+++ b/apps/mobile/src/services/native/PermissionsService.ts
@@ -2,6 +2,8 @@ import { Platform, Linking, Alert } from 'react-native';
import {
check,
request,
+ checkNotifications,
+ requestNotifications,
PERMISSIONS,
RESULTS,
type Permission,
@@ -13,7 +15,12 @@ export type PermissionType = 'camera' | 'microphone' | 'notifications';
export type PermissionState = 'granted' | 'denied' | 'blocked' | 'unavailable' | 'undetermined';
-const PERMISSION_MAP: Record = {
+/**
+ * Notifications are not a standard permission in react-native-permissions.
+ * They use checkNotifications/requestNotifications instead.
+ * Only camera and microphone are in this map.
+ */
+const PERMISSION_MAP: Record<'camera' | 'microphone', { ios: Permission; android: Permission }> = {
camera: {
ios: PERMISSIONS.IOS.CAMERA,
android: PERMISSIONS.ANDROID.CAMERA,
@@ -22,11 +29,6 @@ const PERMISSION_MAP: Record {
+ if (type === 'notifications') {
+ const { status } = await checkNotifications();
+ return mapStatus(status);
+ }
const permission = getPlatformPermission(type);
const status = await check(permission);
return mapStatus(status);
}
async requestPermission(type: PermissionType): Promise {
+ if (type === 'notifications') {
+ const { status } = await requestNotifications(['alert', 'sound', 'badge']);
+ return mapStatus(status);
+ }
const permission = getPlatformPermission(type);
const status = await request(permission);
return mapStatus(status);
@@ -109,9 +119,9 @@ class PermissionsServiceImpl {
const message = rationale || `Farscry needs ${labels[type].toLowerCase()} access. Please enable it in Settings.`;
- return new Promise((resolve) => {
+ return new Promise((resolve) => {
Alert.alert(`${labels[type]} Access Required`, message, [
- { text: 'Not Now', style: 'cancel', onPress: resolve },
+ { text: 'Not Now', style: 'cancel', onPress: () => resolve() },
{
text: 'Open Settings',
onPress: () => {
diff --git a/apps/mobile/src/services/native/PushService.ts b/apps/mobile/src/services/native/PushService.ts
index 523fcc4..7361b03 100644
--- a/apps/mobile/src/services/native/PushService.ts
+++ b/apps/mobile/src/services/native/PushService.ts
@@ -85,8 +85,9 @@ class PushServiceImpl {
this.handleVoipPush(notification as IncomingCallPayload);
});
+ // registerVoipToken() registers for VoIP pushes — on iOS, VoIP push
+ // permissions are implicitly granted when registering for PushKit.
VoipPushNotification.registerVoipToken();
- VoipPushNotification.requestPermissions();
}
/**
diff --git a/apps/mobile/src/services/supabase/client.ts b/apps/mobile/src/services/supabase/client.ts
index 2c3cdcf..7b49f94 100644
--- a/apps/mobile/src/services/supabase/client.ts
+++ b/apps/mobile/src/services/supabase/client.ts
@@ -1,111 +1,32 @@
-// Placeholder Supabase client — replace URL and anon key with real values
-// after installing @supabase/supabase-js.
-
-const SUPABASE_URL = 'https://your-project.supabase.co';
-const SUPABASE_ANON_KEY = 'your-anon-key';
-
-export type SupabaseSession = {
- access_token: string;
- refresh_token: string;
- expires_at: number;
- user: {
- id: string;
- email?: string;
- };
-};
-
-export type AuthChangeEvent =
- | 'SIGNED_IN'
- | 'SIGNED_OUT'
- | 'TOKEN_REFRESHED'
- | 'USER_UPDATED'
- | 'USER_DELETED';
-
-type AuthChangeCallback = (
- event: AuthChangeEvent,
- session: SupabaseSession | null,
-) => void;
-
-type Unsubscribe = { unsubscribe: () => void };
-
-type QueryBuilder> = {
- select: (columns?: string) => QueryBuilder;
- insert: (data: Partial | Partial[]) => QueryBuilder;
- update: (data: Partial) => QueryBuilder;
- delete: () => QueryBuilder;
- eq: (column: string, value: unknown) => QueryBuilder;
- neq: (column: string, value: unknown) => QueryBuilder;
- ilike: (column: string, value: string) => QueryBuilder;
- or: (filters: string) => QueryBuilder;
- order: (column: string, options?: { ascending?: boolean }) => QueryBuilder;
- limit: (count: number) => QueryBuilder;
- single: () => Promise<{ data: T | null; error: SupabaseError | null }>;
- maybeSingle: () => Promise<{ data: T | null; error: SupabaseError | null }>;
- then: Promise<{ data: T[] | null; error: SupabaseError | null }>['then'];
-};
-
-export type SupabaseError = {
- message: string;
- code?: string;
- status?: number;
-};
-
-type SupabaseAuth = {
- signUp: (credentials: {
- email: string;
- password: string;
- }) => Promise<{ data: { user: { id: string } | null; session: SupabaseSession | null }; error: SupabaseError | null }>;
-
- signInWithPassword: (credentials: {
- email: string;
- password: string;
- }) => Promise<{ data: { session: SupabaseSession | null }; error: SupabaseError | null }>;
-
- signOut: () => Promise<{ error: SupabaseError | null }>;
-
- getSession: () => Promise<{
- data: { session: SupabaseSession | null };
- error: SupabaseError | null;
- }>;
-
- refreshSession: () => Promise<{
- data: { session: SupabaseSession | null };
- error: SupabaseError | null;
- }>;
-
- resetPasswordForEmail: (
- email: string,
- options?: { redirectTo?: string },
- ) => Promise<{ error: SupabaseError | null }>;
-
- onAuthStateChange: (callback: AuthChangeCallback) => {
- data: { subscription: Unsubscribe };
- };
-
- admin: {
- deleteUser: (userId: string) => Promise<{ error: SupabaseError | null }>;
- };
-};
-
-export type SupabaseClient = {
- auth: SupabaseAuth;
- from: >(table: string) => QueryBuilder;
- rpc: (
- fn: string,
- params?: Record,
- ) => Promise<{ data: T | null; error: SupabaseError | null }>;
-};
-
-// Stub — will throw at runtime until supabase-js is installed
-function createClient(_url: string, _key: string): SupabaseClient {
- throw new Error(
- 'Supabase client not configured. Install @supabase/supabase-js and update this file.',
+import 'react-native-url-polyfill/auto';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import {createClient} from '@supabase/supabase-js';
+import {AppState} from 'react-native';
+import Config from 'react-native-config';
+
+const supabaseUrl = Config.SUPABASE_URL ?? '';
+const supabaseAnonKey = Config.SUPABASE_ANON_KEY ?? '';
+
+if (!supabaseUrl || !supabaseAnonKey) {
+ console.warn(
+ 'Supabase credentials missing. Create apps/mobile/.env with SUPABASE_URL and SUPABASE_ANON_KEY.',
);
}
-export const supabase: SupabaseClient = createClient(
- SUPABASE_URL,
- SUPABASE_ANON_KEY,
-);
-
-export { SUPABASE_URL };
+export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
+ auth: {
+ storage: AsyncStorage,
+ autoRefreshToken: true,
+ persistSession: true,
+ detectSessionInUrl: false,
+ },
+});
+
+// Auto-refresh tokens when app comes to foreground
+AppState.addEventListener('change', state => {
+ if (state === 'active') {
+ supabase.auth.startAutoRefresh();
+ } else {
+ supabase.auth.stopAutoRefresh();
+ }
+});
diff --git a/apps/mobile/src/services/user/ContactsService.ts b/apps/mobile/src/services/user/ContactsService.ts
index 3d5f0d1..d34036b 100644
--- a/apps/mobile/src/services/user/ContactsService.ts
+++ b/apps/mobile/src/services/user/ContactsService.ts
@@ -16,13 +16,13 @@ export const ContactsService = {
if (!userId) throw new Error('Not authenticated');
const {data, error} = await supabase
- .from('contacts')
+ .from('contacts')
.select('*, profile:users!contact_user_id(*)')
.eq('user_id', userId)
.order('added_at', {ascending: false});
if (error) throw new Error(error.message);
- return data ?? [];
+ return (data ?? []) as Contact[];
},
async addContact(contactUserId: string): Promise {
@@ -35,8 +35,9 @@ export const ContactsService = {
}
const {data, error} = await supabase
- .from('contacts')
+ .from('contacts')
.insert({user_id: userId, contact_user_id: contactUserId})
+ .select('*, profile:users!contact_user_id(*)')
.single();
if (error) {
@@ -45,8 +46,7 @@ export const ContactsService = {
}
throw new Error(error.message);
}
- if (!data) throw new Error('Failed to add contact');
- return data;
+ return data as Contact;
},
async removeContact(contactUserId: string): Promise {
@@ -68,9 +68,8 @@ export const ContactsService = {
const userId = sessionData.session?.user.id;
if (!userId) throw new Error('Not authenticated');
- // Fetch current state
const {data: existing, error: fetchError} = await supabase
- .from('contacts')
+ .from('contacts')
.select('is_favorite')
.eq('user_id', userId)
.eq('contact_user_id', contactUserId)
diff --git a/apps/mobile/src/services/user/UserService.ts b/apps/mobile/src/services/user/UserService.ts
index d446a92..2aed736 100644
--- a/apps/mobile/src/services/user/UserService.ts
+++ b/apps/mobile/src/services/user/UserService.ts
@@ -16,14 +16,13 @@ export type ProfileUpdate = {
export const UserService = {
async getProfile(userId: string): Promise {
const {data, error} = await supabase
- .from('users')
+ .from('users')
.select('*')
.eq('id', userId)
.single();
if (error) throw new Error(error.message);
- if (!data) throw new Error('User not found');
- return data;
+ return data as UserProfile;
},
async updateProfile(updates: ProfileUpdate): Promise {
@@ -38,14 +37,14 @@ export const UserService = {
}
const {data, error} = await supabase
- .from('users')
+ .from('users')
.update({...updates, updated_at: new Date().toISOString()})
.eq('id', userId)
+ .select()
.single();
if (error) throw new Error(error.message);
- if (!data) throw new Error('Profile update failed');
- return data;
+ return data as UserProfile;
},
async searchUsers(query: string): Promise {
@@ -53,23 +52,23 @@ export const UserService = {
if (!trimmed) return [];
const {data, error} = await supabase
- .from('users')
+ .from('users')
.select('*')
.ilike('display_name', `%${trimmed}%`)
.limit(20);
if (error) throw new Error(error.message);
- return data ?? [];
+ return (data ?? []) as UserProfile[];
},
async getUserById(id: string): Promise {
const {data, error} = await supabase
- .from('users')
+ .from('users')
.select('*')
.eq('id', id)
.maybeSingle();
if (error) throw new Error(error.message);
- return data;
+ return data as UserProfile | null;
},
};
diff --git a/apps/mobile/src/stores/authStore.ts b/apps/mobile/src/stores/authStore.ts
index 8dba260..f37ce1a 100644
--- a/apps/mobile/src/stores/authStore.ts
+++ b/apps/mobile/src/stores/authStore.ts
@@ -1,18 +1,18 @@
import React, {createContext, useContext, useEffect, useReducer, useCallback} from 'react';
import {AuthService, type AuthUser} from '../services/auth/AuthService';
-import {SessionManager} from '../services/auth/SessionManager';
-import type {SupabaseSession} from '../services/supabase/client';
+import {supabase} from '../services/supabase/client';
+import type {Session} from '@supabase/supabase-js';
type AuthState = {
user: AuthUser | null;
- session: SupabaseSession | null;
+ session: Session | null;
loading: boolean;
error: string | null;
};
type AuthAction =
| {type: 'LOADING'}
- | {type: 'SIGNED_IN'; user: AuthUser; session: SupabaseSession}
+ | {type: 'SIGNED_IN'; user: AuthUser; session: Session}
| {type: 'SIGNED_OUT'}
| {type: 'ERROR'; error: string}
| {type: 'CLEAR_ERROR'};
@@ -51,41 +51,10 @@ const AuthContext = createContext(null);
export function AuthProvider({children}: {children: React.ReactNode}) {
const [state, dispatch] = useReducer(authReducer, initialState);
- // Restore session on mount
+ // Restore session on mount + listen for auth changes
useEffect(() => {
- let cancelled = false;
-
- async function restore() {
- try {
- const session = await SessionManager.loadSession();
- if (cancelled) return;
-
- if (session && SessionManager.isSessionValid(session)) {
- // Refresh if expiring soon
- let activeSession = session;
- if (SessionManager.isExpiringSoon(session)) {
- const refreshed = await AuthService.refreshToken();
- if (refreshed) activeSession = refreshed;
- }
-
- dispatch({
- type: 'SIGNED_IN',
- user: {id: activeSession.user.id, email: activeSession.user.email},
- session: activeSession,
- });
- } else {
- dispatch({type: 'SIGNED_OUT'});
- }
- } catch {
- if (!cancelled) dispatch({type: 'SIGNED_OUT'});
- }
- }
-
- restore();
-
- // Listen for external auth changes
- const sub = AuthService.onAuthStateChange((_event, session) => {
- if (cancelled) return;
+ // Get initial session from SDK (auto-persisted in AsyncStorage)
+ supabase.auth.getSession().then(({data: {session}}) => {
if (session) {
dispatch({
type: 'SIGNED_IN',
@@ -97,10 +66,22 @@ export function AuthProvider({children}: {children: React.ReactNode}) {
}
});
- return () => {
- cancelled = true;
- sub.unsubscribe();
- };
+ // Listen for auth state changes
+ const {data: {subscription}} = supabase.auth.onAuthStateChange(
+ (_event, session) => {
+ if (session) {
+ dispatch({
+ type: 'SIGNED_IN',
+ user: {id: session.user.id, email: session.user.email},
+ session,
+ });
+ } else {
+ dispatch({type: 'SIGNED_OUT'});
+ }
+ },
+ );
+
+ return () => subscription.unsubscribe();
}, []);
const signUp = useCallback(
@@ -108,14 +89,14 @@ export function AuthProvider({children}: {children: React.ReactNode}) {
dispatch({type: 'LOADING'});
try {
const result = await AuthService.signUp(email, password, displayName);
- if (result.session) {
+ if (result.session && result.user) {
dispatch({
type: 'SIGNED_IN',
- user: result.user!,
+ user: result.user,
session: result.session,
});
} else {
- // Email confirmation required
+ // Email confirmation required (shouldn't happen if disabled)
dispatch({type: 'SIGNED_OUT'});
}
} catch (e: unknown) {
@@ -129,10 +110,11 @@ export function AuthProvider({children}: {children: React.ReactNode}) {
dispatch({type: 'LOADING'});
try {
const result = await AuthService.signIn(email, password);
+ if (!result.user || !result.session) throw new Error('Sign in failed');
dispatch({
type: 'SIGNED_IN',
- user: result.user!,
- session: result.session!,
+ user: result.user,
+ session: result.session,
});
} catch (e: unknown) {
dispatch({type: 'ERROR', error: e instanceof Error ? e.message : 'Sign in failed'});
diff --git a/apps/mobile/src/stores/callStore.ts b/apps/mobile/src/stores/callStore.ts
new file mode 100644
index 0000000..07c222b
--- /dev/null
+++ b/apps/mobile/src/stores/callStore.ts
@@ -0,0 +1,125 @@
+import React, {createContext, useContext, useEffect, useRef, useState, useCallback} from 'react';
+import {useNavigation} from '@react-navigation/native';
+import type {NativeStackNavigationProp} from '@react-navigation/native-stack';
+import Config from 'react-native-config';
+import {SignalingClient, type ConnectionState} from '../services/signaling/SignalingClient';
+import {CallManager} from '../services/call/CallManager';
+import {type CallStateValue, createIdleState} from '../services/call/CallState';
+import {PermissionsService} from '../services/native/PermissionsService';
+import {useAuth} from './authStore';
+import type {RootStackParamList} from '../navigation/types';
+import type {ServerMessage} from '@farscry/shared';
+
+const SIGNALING_URL = Config.SIGNALING_URL ?? 'ws://localhost:8080';
+
+type CallContextValue = {
+ callManager: CallManager | null;
+ signalingState: ConnectionState;
+ callState: CallStateValue;
+ startCall: (remoteUserId: string, remoteName: string) => Promise;
+};
+
+const CallContext = createContext(null);
+
+export function CallProvider({children}: {children: React.ReactNode}) {
+ const {user, session} = useAuth();
+ const navigation = useNavigation>();
+
+ const signalingRef = useRef(null);
+ const callManagerRef = useRef(null);
+
+ const [signalingState, setSignalingState] = useState('disconnected');
+ const [callState, setCallState] = useState(createIdleState());
+
+ // Connect to signaling server when authenticated
+ useEffect(() => {
+ if (!user || !session?.access_token) {
+ // Not authenticated — tear down if exists
+ if (signalingRef.current) {
+ signalingRef.current.disconnect();
+ signalingRef.current = null;
+ }
+ if (callManagerRef.current) {
+ callManagerRef.current.destroy();
+ callManagerRef.current = null;
+ }
+ setSignalingState('disconnected');
+ setCallState(createIdleState());
+ return;
+ }
+
+ // Create signaling client and call manager
+ const signaling = new SignalingClient(SIGNALING_URL);
+ const manager = new CallManager(signaling);
+
+ signalingRef.current = signaling;
+ callManagerRef.current = manager;
+
+ // Track signaling connection state
+ const unsubState = signaling.onStateChange(setSignalingState);
+
+ // Track call state
+ const unsubCall = manager.onStateChange(setCallState);
+
+ // Listen for incoming calls to navigate
+ const unsubMessage = signaling.onMessage((message: ServerMessage) => {
+ if (message.type === 'call:incoming') {
+ navigation.navigate('IncomingCall', {
+ callerId: message.callerId,
+ callerName: message.callerName,
+ });
+ }
+ });
+
+ // Connect with auth
+ signaling.connect(user.id, session.access_token);
+
+ return () => {
+ unsubState();
+ unsubCall();
+ unsubMessage();
+ signaling.disconnect();
+ manager.destroy();
+ signalingRef.current = null;
+ callManagerRef.current = null;
+ };
+ }, [user?.id, session?.access_token, navigation]);
+
+ const startCall = useCallback(
+ async (remoteUserId: string, remoteName: string) => {
+ if (!callManagerRef.current) {
+ throw new Error('Not connected to signaling server');
+ }
+
+ // Request permissions before starting call
+ const perms = await PermissionsService.requestCallPermissions();
+ if (perms.microphone !== 'granted') {
+ throw new Error('Microphone permission is required for calls');
+ }
+
+ await callManagerRef.current.startCall(remoteUserId);
+ navigation.navigate('OutgoingCall', {
+ contactId: remoteUserId,
+ contactName: remoteName,
+ });
+ },
+ [navigation],
+ );
+
+ const value: CallContextValue = {
+ callManager: callManagerRef.current,
+ signalingState,
+ callState,
+ startCall,
+ };
+
+ return React.createElement(CallContext.Provider, {value}, children);
+}
+
+export function useCallContext(): CallContextValue {
+ const ctx = useContext(CallContext);
+ if (!ctx) {
+ throw new Error('useCallContext must be used within CallProvider');
+ }
+ return ctx;
+}
diff --git a/apps/mobile/src/types/react-native-callkeep.d.ts b/apps/mobile/src/types/react-native-callkeep.d.ts
deleted file mode 100644
index 0d8ab14..0000000
--- a/apps/mobile/src/types/react-native-callkeep.d.ts
+++ /dev/null
@@ -1,143 +0,0 @@
-declare module 'react-native-callkeep' {
- export interface SetupOptions {
- ios: {
- appName: string;
- imageName?: string;
- supportsVideo?: boolean;
- maximumCallGroups?: number;
- maximumCallsPerCallGroup?: number;
- includesCallsInRecents?: boolean;
- ringtoneSound?: string;
- };
- android: {
- alertTitle: string;
- alertDescription: string;
- cancelButton: string;
- okButton: string;
- additionalPermissions?: string[];
- selfManaged?: boolean;
- foregroundService?: {
- channelId: string;
- channelName: string;
- notificationTitle: string;
- notificationIcon?: string;
- };
- };
- }
-
- export type CallKeepEventType =
- | 'answerCall'
- | 'endCall'
- | 'didActivateAudioSession'
- | 'didDeactivateAudioSession'
- | 'didDisplayIncomingCall'
- | 'didPerformSetMutedCallAction'
- | 'didToggleHoldCallAction'
- | 'didPerformDTMFAction'
- | 'showIncomingCallUi'
- | 'silenceIncomingCall'
- | 'checkReachability'
- | 'didLoadWithEvents';
-
- export interface AnswerCallPayload {
- callUUID: string;
- }
-
- export interface EndCallPayload {
- callUUID: string;
- }
-
- export interface MuteCallPayload {
- callUUID: string;
- muted: boolean;
- }
-
- export interface HoldCallPayload {
- callUUID: string;
- hold: boolean;
- }
-
- export interface DTMFPayload {
- callUUID: string;
- digits: string;
- }
-
- export interface DisplayIncomingCallPayload {
- callUUID: string;
- handle: string;
- localizedCallerName: string;
- hasVideo: string;
- fromPushKit: string;
- payload: Record;
- }
-
- const RNCallKeep: {
- setup(options: SetupOptions): Promise;
- hasDefaultPhoneAccount(): boolean;
-
- displayIncomingCall(
- uuid: string,
- handle: string,
- localizedCallerName?: string,
- handleType?: string,
- hasVideo?: boolean,
- options?: Record,
- ): void;
-
- startCall(
- uuid: string,
- handle: string,
- contactIdentifier?: string,
- handleType?: string,
- hasVideo?: boolean,
- ): void;
-
- reportConnectingOutgoingCallWithUUID(uuid: string): void;
- reportConnectedOutgoingCallWithUUID(uuid: string): void;
- reportEndCallWithUUID(uuid: string, reason: number): void;
- endCall(uuid: string): void;
- endAllCalls(): void;
-
- setMutedCall(uuid: string, muted: boolean): void;
- setOnHold(uuid: string, hold: boolean): void;
-
- checkIfBusy(): Promise;
- checkSpeaker(): Promise;
-
- setAvailable(available: boolean): void;
- setForegroundServiceSettings(settings: Record): void;
- setCurrentCallActive(uuid: string): void;
-
- updateDisplay(
- uuid: string,
- displayName: string,
- handle: string,
- options?: Record,
- ): void;
-
- addEventListener(
- event: CallKeepEventType,
- handler: (data: Record) => void,
- ): void;
- removeEventListener(event: CallKeepEventType): void;
-
- backToForeground(): void;
-
- // iOS CXCallDirectoryProvider support
- hasPhoneAccount(): Promise;
- registerPhoneAccount(): void;
- registerAndroidEvents(): void;
-
- // End call reasons (maps to CXCallEndedReason)
- END_CALL_REASONS: {
- FAILED: 1;
- REMOTE_ENDED: 2;
- UNANSWERED: 3;
- ANSWERED_ELSEWHERE: 4;
- DECLINED_ELSEWHERE: 5;
- MISSED: 6;
- };
- };
-
- export default RNCallKeep;
-}
diff --git a/apps/mobile/src/types/react-native-voip-push-notification.d.ts b/apps/mobile/src/types/react-native-voip-push-notification.d.ts
deleted file mode 100644
index 7653de7..0000000
--- a/apps/mobile/src/types/react-native-voip-push-notification.d.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-declare module 'react-native-voip-push-notification' {
- export type VoIPPushEventType =
- | 'register'
- | 'notification'
- | 'didLoadWithEvents';
-
- export interface VoIPPushPayload {
- [key: string]: unknown;
- callId?: string;
- callerId?: string;
- callerName?: string;
- hasVideo?: boolean;
- uuid?: string;
- }
-
- const VoipPushNotification: {
- requestPermissions(): void;
- registerVoipToken(): void;
-
- addEventListener(
- event: 'register',
- handler: (token: string) => void,
- ): void;
- addEventListener(
- event: 'notification',
- handler: (notification: VoIPPushPayload) => void,
- ): void;
- addEventListener(
- event: 'didLoadWithEvents',
- handler: (events: Array<{ name: string; data: unknown }>) => void,
- ): void;
-
- removeEventListener(event: VoIPPushEventType): void;
-
- onVoipNotificationCompleted(uuid: string): void;
- };
-
- export default VoipPushNotification;
-}
diff --git a/docs/plans/2026-03-01-call-infrastructure-design.md b/docs/plans/2026-03-01-call-infrastructure-design.md
new file mode 100644
index 0000000..73d01cc
--- /dev/null
+++ b/docs/plans/2026-03-01-call-infrastructure-design.md
@@ -0,0 +1,77 @@
+# Call Infrastructure Setup for Family Testing
+
+**Date:** 2026-03-01
+**Goal:** Install native dependencies, configure iOS, wire signaling + call management into the app so two authenticated users on the same WiFi can make a real call.
+**Prerequisites:** Supabase auth (being implemented separately).
+**Out of scope:** TURN server, VoIP push notifications, Android-specific push (FCM). Both users will have the app foregrounded.
+
+## Native Dependencies
+
+| Package | Purpose |
+|---------|---------|
+| `react-native-webrtc` | RTCPeerConnection, MediaStream, getUserMedia |
+| `react-native-callkeep` | CallKit (iOS) / ConnectionService (Android) native call UI |
+| `react-native-incall-manager` | Earpiece/speaker routing, proximity sensor |
+| `react-native-permissions` | Runtime mic + camera permission requests |
+| `react-native-voip-push-notification` | iOS VoIP push token registration (installed now, wired later) |
+
+## iOS Configuration
+
+**Info.plist:**
+- `NSMicrophoneUsageDescription` — "Farscry needs microphone access for voice and video calls"
+- `NSCameraUsageDescription` — "Farscry needs camera access for video calls"
+- `UIBackgroundModes` — `voip`, `audio`
+
+No entitlements changes needed yet.
+
+## Signaling URL
+
+Add `SIGNALING_URL` to `.env` and `.env.example`. For local testing: `ws://:8080`. The signaling server already listens on port 8080.
+
+## CallProvider (Approach A — Context Provider)
+
+New file: `src/stores/callStore.ts`, matching existing `authStore.ts` / `contactsStore.ts` pattern.
+
+### Lifecycle
+
+- When auth session is available: create `SignalingClient(SIGNALING_URL)`, create `CallManager(signalingClient)`, call `signalingClient.connect(userId, accessToken)`
+- When auth session is gone: disconnect signaling, destroy CallManager
+- On incoming call message: navigate to `IncomingCallScreen`
+
+### Context Shape
+
+```typescript
+{
+ callManager: CallManager | null;
+ signalingState: 'disconnected' | 'connecting' | 'connected';
+ callState: CallStateValue;
+ startCall: (remoteUserId: string) => Promise;
+}
+```
+
+## App.tsx Wiring
+
+```
+AuthProvider
+ ContactsProvider
+ NavigationContainer
+ CallProvider ← new, inside NavigationContainer for navigation access
+ RootNavigator
+```
+
+## Screen Updates
+
+Call screens (`IncomingCallScreen`, `OutgoingCallScreen`, `ActiveCallScreen`) will use `useCallContext()` to wire buttons to `acceptCall()`, `declineCall()`, `hangup()`, etc.
+
+Contact screens that initiate calls will use `useCallContext()` to call `startCall(userId)`.
+
+## Network
+
+Same WiFi only for initial testing. STUN servers (Google) are sufficient — no TURN needed.
+
+## Unchanged
+
+- Signaling server code (already functional)
+- WebRTC/media service implementations (already written, just need the dependency installed)
+- Call state machine
+- Screen UI layouts
diff --git a/docs/plans/2026-03-01-call-infrastructure.md b/docs/plans/2026-03-01-call-infrastructure.md
new file mode 100644
index 0000000..8829eda
--- /dev/null
+++ b/docs/plans/2026-03-01-call-infrastructure.md
@@ -0,0 +1,572 @@
+# Call Infrastructure Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Install native dependencies, configure iOS, and wire signaling + call management into the app so two authenticated users on the same WiFi can make a real video call.
+
+**Architecture:** Add a `CallProvider` (React context) that creates `SignalingClient` + `CallManager` when the user is authenticated, connects to the signaling server with the Supabase JWT, and navigates to call screens on incoming calls. Update existing call screens to use `useCallContext()` for real call actions.
+
+**Tech Stack:** `react-native-webrtc`, `react-native-callkeep`, `react-native-incall-manager`, `react-native-permissions`, `react-native-voip-push-notification`, React Context
+
+---
+
+## Task 1: Install Native Dependencies
+
+**Files:**
+- Modify: `apps/mobile/package.json`
+
+**Step 1: Install npm packages**
+
+```bash
+cd /Users/gav/Programming/personal/farscry
+npm install --workspace=com.farscry.app react-native-webrtc react-native-callkeep react-native-incall-manager react-native-permissions react-native-voip-push-notification
+```
+
+**Step 2: Install iOS pods**
+
+```bash
+cd /Users/gav/Programming/personal/farscry/apps/mobile/ios && pod install
+```
+
+If `pod install` fails with version conflicts, try:
+
+```bash
+cd /Users/gav/Programming/personal/farscry/apps/mobile/ios && pod install --repo-update
+```
+
+**Step 3: Verify**
+
+Check that `package.json` now lists all 5 packages in `dependencies`. Check that `Podfile.lock` has entries for the new pods.
+
+**Step 4: Commit**
+
+```bash
+cd /Users/gav/Programming/personal/farscry
+git add apps/mobile/package.json package-lock.json apps/mobile/ios/Podfile.lock apps/mobile/ios/Pods
+git commit -m "Add native call dependencies (WebRTC, CallKeep, InCallManager, permissions, VoIP push)"
+```
+
+---
+
+## Task 2: iOS Configuration (Info.plist)
+
+**Files:**
+- Modify: `apps/mobile/ios/Farscry/Info.plist`
+
+**Step 1: Add permission strings and background modes**
+
+Add these entries to `Info.plist` (inside the top-level ``):
+
+```xml
+NSMicrophoneUsageDescription
+Farscry needs microphone access for voice and video calls
+NSCameraUsageDescription
+Farscry needs camera access for video calls
+UIBackgroundModes
+
+ voip
+ audio
+
+```
+
+**Step 2: Verify**
+
+Open `Info.plist` and confirm:
+- `NSMicrophoneUsageDescription` is present
+- `NSCameraUsageDescription` is present
+- `UIBackgroundModes` contains `voip` and `audio`
+- The old empty `NSLocationWhenInUseUsageDescription` can optionally be removed (it's not needed)
+
+**Step 3: Commit**
+
+```bash
+cd /Users/gav/Programming/personal/farscry
+git add apps/mobile/ios/Farscry/Info.plist
+git commit -m "Add camera/mic permissions and VoIP background modes to Info.plist"
+```
+
+---
+
+## Task 3: Add SIGNALING_URL to Environment Config
+
+**Files:**
+- Modify: `apps/mobile/.env`
+- Modify: `apps/mobile/.env.example`
+
+**Step 1: Add SIGNALING_URL to .env.example**
+
+Append to `apps/mobile/.env.example`:
+
+```
+SIGNALING_URL=ws://localhost:8080
+```
+
+**Step 2: Add SIGNALING_URL to .env**
+
+Append to `apps/mobile/.env`:
+
+```
+SIGNALING_URL=ws://localhost:8080
+```
+
+Note: For testing on physical devices, replace `localhost` with the Mac's LAN IP (e.g., `ws://192.168.1.42:8080`). The `NSAllowsLocalNetworking` key in `Info.plist` is already set to `true`, so local WebSocket connections will work.
+
+**Step 3: Commit**
+
+```bash
+cd /Users/gav/Programming/personal/farscry
+git add apps/mobile/.env.example
+git commit -m "Add SIGNALING_URL to environment config"
+```
+
+Do NOT commit `.env` — it should be in `.gitignore`.
+
+---
+
+## Task 4: Create CallProvider (callStore.ts)
+
+**Files:**
+- Create: `apps/mobile/src/stores/callStore.ts`
+
+**Step 1: Create the CallProvider**
+
+This follows the exact same pattern as `authStore.ts` and `contactsStore.ts`. It:
+- Reads auth state from `useAuth()`
+- Creates `SignalingClient` and `CallManager` when session is available
+- Connects to signaling server with user ID and access token
+- Listens for incoming calls and navigates to `IncomingCallScreen`
+- Disconnects on sign-out or unmount
+- Exposes `callManager`, `signalingState`, and `callState` via context
+
+```typescript
+import React, { createContext, useContext, useEffect, useRef, useState, useCallback } from 'react';
+import { useNavigation } from '@react-navigation/native';
+import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
+import Config from 'react-native-config';
+import { SignalingClient, type ConnectionState } from '../services/signaling/SignalingClient';
+import { CallManager } from '../services/call/CallManager';
+import { type CallStateValue, createIdleState } from '../services/call/CallState';
+import { PermissionsService } from '../services/native/PermissionsService';
+import { useAuth } from './authStore';
+import type { RootStackParamList } from '../navigation/types';
+import type { ServerMessage } from '@farscry/shared';
+
+const SIGNALING_URL = Config.SIGNALING_URL ?? 'ws://localhost:8080';
+
+type CallContextValue = {
+ callManager: CallManager | null;
+ signalingState: ConnectionState;
+ callState: CallStateValue;
+ startCall: (remoteUserId: string, remoteName: string) => Promise;
+};
+
+const CallContext = createContext(null);
+
+export function CallProvider({ children }: { children: React.ReactNode }) {
+ const { user, session } = useAuth();
+ const navigation = useNavigation>();
+
+ const signalingRef = useRef(null);
+ const callManagerRef = useRef(null);
+
+ const [signalingState, setSignalingState] = useState('disconnected');
+ const [callState, setCallState] = useState(createIdleState());
+
+ // Connect to signaling server when authenticated
+ useEffect(() => {
+ if (!user || !session?.access_token) {
+ // Not authenticated — tear down if exists
+ if (signalingRef.current) {
+ signalingRef.current.disconnect();
+ signalingRef.current = null;
+ }
+ if (callManagerRef.current) {
+ callManagerRef.current.destroy();
+ callManagerRef.current = null;
+ }
+ setSignalingState('disconnected');
+ setCallState(createIdleState());
+ return;
+ }
+
+ // Create signaling client and call manager
+ const signaling = new SignalingClient(SIGNALING_URL);
+ const manager = new CallManager(signaling);
+
+ signalingRef.current = signaling;
+ callManagerRef.current = manager;
+
+ // Track signaling connection state
+ const unsubState = signaling.onStateChange(setSignalingState);
+
+ // Track call state
+ const unsubCall = manager.onStateChange(setCallState);
+
+ // Listen for incoming calls to navigate
+ const unsubMessage = signaling.onMessage((message: ServerMessage) => {
+ if (message.type === 'call:incoming') {
+ navigation.navigate('IncomingCall', {
+ callerId: message.callerId,
+ callerName: message.callerName,
+ });
+ }
+ });
+
+ // Connect with auth
+ signaling.connect(user.id, session.access_token);
+
+ return () => {
+ unsubState();
+ unsubCall();
+ unsubMessage();
+ signaling.disconnect();
+ manager.destroy();
+ signalingRef.current = null;
+ callManagerRef.current = null;
+ };
+ }, [user?.id, session?.access_token, navigation]);
+
+ const startCall = useCallback(async (remoteUserId: string, remoteName: string) => {
+ if (!callManagerRef.current) {
+ throw new Error('Not connected to signaling server');
+ }
+
+ // Request permissions before starting call
+ const perms = await PermissionsService.requestCallPermissions();
+ if (perms.microphone !== 'granted') {
+ throw new Error('Microphone permission is required for calls');
+ }
+
+ await callManagerRef.current.startCall(remoteUserId);
+ navigation.navigate('OutgoingCall', {
+ contactId: remoteUserId,
+ contactName: remoteName,
+ });
+ }, [navigation]);
+
+ const value: CallContextValue = {
+ callManager: callManagerRef.current,
+ signalingState,
+ callState,
+ startCall,
+ };
+
+ return React.createElement(CallContext.Provider, { value }, children);
+}
+
+export function useCallContext(): CallContextValue {
+ const ctx = useContext(CallContext);
+ if (!ctx) {
+ throw new Error('useCallContext must be used within CallProvider');
+ }
+ return ctx;
+}
+```
+
+**Step 2: Verify**
+
+Run TypeScript type checking:
+
+```bash
+cd /Users/gav/Programming/personal/farscry/apps/mobile && npx tsc --noEmit
+```
+
+Fix any type errors. Note: there may be type errors from other files that import uninstalled packages — focus only on errors in `callStore.ts`.
+
+**Step 3: Commit**
+
+```bash
+cd /Users/gav/Programming/personal/farscry
+git add apps/mobile/src/stores/callStore.ts
+git commit -m "Add CallProvider context for signaling and call management"
+```
+
+---
+
+## Task 5: Wire CallProvider into App.tsx
+
+**Files:**
+- Modify: `apps/mobile/App.tsx`
+
+**Step 1: Add CallProvider inside NavigationContainer**
+
+The current `App.tsx` structure is:
+
+```
+GestureHandlerRootView
+ AuthProvider
+ ContactsProvider
+ SafeAreaProvider
+ StatusBar
+ NavigationContainer
+ RootNavigator
+```
+
+Change it to:
+
+```
+GestureHandlerRootView
+ AuthProvider
+ ContactsProvider
+ SafeAreaProvider
+ StatusBar
+ NavigationContainer
+ CallProvider ← new
+ RootNavigator
+```
+
+The diff: import `CallProvider` from `./src/stores/callStore` and wrap `` with ``.
+
+```typescript
+import {CallProvider} from './src/stores/callStore';
+
+// ... in the render:
+
+
+
+
+
+```
+
+**Step 2: Verify**
+
+Run TypeScript type checking:
+
+```bash
+cd /Users/gav/Programming/personal/farscry/apps/mobile && npx tsc --noEmit
+```
+
+**Step 3: Commit**
+
+```bash
+cd /Users/gav/Programming/personal/farscry
+git add apps/mobile/App.tsx
+git commit -m "Wire CallProvider into app component tree"
+```
+
+---
+
+## Task 6: Wire IncomingCallScreen to CallManager
+
+**Files:**
+- Modify: `apps/mobile/src/screens/call/IncomingCallScreen.tsx`
+
+**Step 1: Connect accept/decline buttons to real call actions**
+
+Currently the screen just navigates on accept and goes back on decline. Update it to:
+- Import `useCallContext` from `../../stores/callStore`
+- On accept: call `callManager.acceptCall()`, then navigate to `ActiveCall`
+- On decline: call `callManager.declineCall()`, then go back
+
+Replace `handleAccept` and `handleDecline`:
+
+```typescript
+import { useCallContext } from '../../stores/callStore';
+
+// Inside the component:
+const { callManager } = useCallContext();
+
+function handleAccept() {
+ callManager?.acceptCall();
+ navigation.replace('ActiveCall', {
+ contactId: route.params.callerId,
+ contactName: callerName,
+ });
+}
+
+function handleDecline() {
+ callManager?.declineCall();
+ navigation.goBack();
+}
+```
+
+**Step 2: Commit**
+
+```bash
+cd /Users/gav/Programming/personal/farscry
+git add apps/mobile/src/screens/call/IncomingCallScreen.tsx
+git commit -m "Wire IncomingCallScreen to real CallManager actions"
+```
+
+---
+
+## Task 7: Wire OutgoingCallScreen to CallManager
+
+**Files:**
+- Modify: `apps/mobile/src/screens/call/OutgoingCallScreen.tsx`
+
+**Step 1: Connect cancel button and listen for call state changes**
+
+The outgoing screen needs to:
+- Call `callManager.cancelCall()` on cancel
+- Listen for call state changes — when the call transitions to `connecting` or `active`, navigate to `ActiveCall`
+- When the call ends (declined, timeout), go back
+
+```typescript
+import { useCallContext } from '../../stores/callStore';
+
+// Inside the component:
+const { callManager, callState } = useCallContext();
+
+// Navigate on state transitions
+useEffect(() => {
+ if (callState.phase === 'connecting' || callState.phase === 'active') {
+ navigation.replace('ActiveCall', {
+ contactId: route.params.contactId,
+ contactName: route.params.contactName,
+ });
+ }
+ if (callState.phase === 'ended') {
+ navigation.goBack();
+ }
+}, [callState.phase, navigation, route.params]);
+
+function handleCancel() {
+ callManager?.cancelCall();
+ navigation.goBack();
+}
+```
+
+**Step 2: Commit**
+
+```bash
+cd /Users/gav/Programming/personal/farscry
+git add apps/mobile/src/screens/call/OutgoingCallScreen.tsx
+git commit -m "Wire OutgoingCallScreen to real CallManager actions"
+```
+
+---
+
+## Task 8: Wire ActiveCallScreen to CallManager
+
+**Files:**
+- Modify: `apps/mobile/src/screens/call/ActiveCallScreen.tsx`
+
+**Step 1: Connect controls to real media and call actions**
+
+The active call screen needs to:
+- Use `useCallContext()` to get `callManager`
+- Use `useCallControls(callManager.mediaService)` for mute/camera/speaker
+- Call `callManager.hangup()` on hangup
+- Listen for call state `ended` to navigate back
+
+Replace the local state controls with real ones:
+
+```typescript
+import { useCallContext } from '../../stores/callStore';
+import { useCallControls } from '../../hooks/useCallControls';
+
+// Inside the component:
+const { callManager, callState } = useCallContext();
+const controls = callManager
+ ? useCallControls(callManager.mediaService)
+ : { isMuted: false, isCameraOff: false, isSpeakerOn: false, toggleMute: () => {}, toggleCamera: () => {}, toggleSpeaker: () => {} };
+
+// Navigate back when call ends
+useEffect(() => {
+ if (callState.phase === 'ended' || callState.phase === 'idle') {
+ navigation.goBack();
+ }
+}, [callState.phase, navigation]);
+
+function handleHangup() {
+ callManager?.hangup();
+}
+
+// In the render, replace the local state variables:
+// muted → controls.isMuted
+// cameraOff → controls.isCameraOff
+// speakerOn → controls.isSpeakerOn
+// onToggleMute → controls.toggleMute
+// onToggleCamera → controls.toggleCamera
+// onToggleSpeaker → controls.toggleSpeaker
+```
+
+Remove the old `useState` for `muted`, `cameraOff`, `speakerOn` — they're replaced by the hook.
+
+**Step 2: Commit**
+
+```bash
+cd /Users/gav/Programming/personal/farscry
+git add apps/mobile/src/screens/call/ActiveCallScreen.tsx
+git commit -m "Wire ActiveCallScreen to real CallManager and media controls"
+```
+
+---
+
+## Task 9: Add Call Button to Contact Screens
+
+**Files:**
+- Modify: `apps/mobile/src/screens/contacts/ContactDetailScreen.tsx`
+
+**Step 1: Read the existing ContactDetailScreen**
+
+Check what UI exists. If there's already a "Call" button that navigates to `OutgoingCall`, update it to use `useCallContext().startCall()` instead of raw navigation. If there's no call button, add one.
+
+The `startCall` from `useCallContext` handles:
+1. Requesting mic/camera permissions
+2. Starting the call via `CallManager`
+3. Navigating to `OutgoingCallScreen`
+
+```typescript
+import { useCallContext } from '../../stores/callStore';
+
+// Inside the component:
+const { startCall } = useCallContext();
+
+async function handleCall() {
+ try {
+ await startCall(contactId, contactName);
+ } catch (err) {
+ // Show error (permission denied, not connected, etc.)
+ console.error('Failed to start call:', err);
+ }
+}
+```
+
+**Step 2: Commit**
+
+```bash
+cd /Users/gav/Programming/personal/farscry
+git add apps/mobile/src/screens/contacts/ContactDetailScreen.tsx
+git commit -m "Wire contact detail call button to real call flow"
+```
+
+---
+
+## Task 10: Verify End-to-End Build
+
+**Step 1: TypeScript check**
+
+```bash
+cd /Users/gav/Programming/personal/farscry/apps/mobile && npx tsc --noEmit
+```
+
+Fix any type errors.
+
+**Step 2: Build iOS**
+
+```bash
+cd /Users/gav/Programming/personal/farscry/apps/mobile && npx react-native run-ios
+```
+
+The app should build and launch in the simulator. Verify:
+- App launches without crashes
+- Auth screens render
+- No red screen errors
+
+Note: WebRTC camera/mic won't work in the simulator — that's expected. This step just verifies the build succeeds and the provider wiring doesn't crash.
+
+**Step 3: Build signaling server**
+
+```bash
+cd /Users/gav/Programming/personal/farscry/packages/signaling && npm run build
+```
+
+**Step 4: Commit any remaining fixes**
+
+```bash
+cd /Users/gav/Programming/personal/farscry
+git add -A
+git commit -m "Fix build issues from call infrastructure integration"
+```
diff --git a/docs/plans/2026-03-01-supabase-setup-design.md b/docs/plans/2026-03-01-supabase-setup-design.md
new file mode 100644
index 0000000..4c2dde9
--- /dev/null
+++ b/docs/plans/2026-03-01-supabase-setup-design.md
@@ -0,0 +1,79 @@
+# Supabase Setup for Family Testing
+
+**Date:** 2026-03-01
+**Goal:** Connect existing stub services to a real Supabase backend so the app can be tested with real accounts.
+**Out of scope:** Subscriptions, payments, entitlement gates. Everyone who signs up gets full access.
+
+## Architecture
+
+No structural changes. The existing service layer (AuthService, UserService, ContactsService) already models Supabase patterns. Replace stubs with real SDK calls and create the database schema.
+
+## Auth Method
+
+Email + password via Supabase Auth. Simple, works immediately.
+
+## Config
+
+`react-native-config` with `.env` at mobile app root:
+
+```
+SUPABASE_URL=https://your-project.supabase.co
+SUPABASE_ANON_KEY=your-anon-key
+```
+
+`.env` in `.gitignore`, `.env.example` committed with placeholder values.
+
+## Database Schema
+
+### users
+- `id` uuid PRIMARY KEY (synced from auth.users via trigger)
+- `display_name` text NOT NULL
+- `avatar_url` text
+- `created_at` timestamptz DEFAULT now()
+- `updated_at` timestamptz DEFAULT now()
+
+### contacts
+- `user_id` uuid REFERENCES users(id) ON DELETE CASCADE
+- `contact_user_id` uuid REFERENCES users(id) ON DELETE CASCADE
+- `is_favorite` boolean DEFAULT false
+- `added_at` timestamptz DEFAULT now()
+- PRIMARY KEY (user_id, contact_user_id)
+- CHECK (user_id != contact_user_id)
+
+### push_tokens
+- `user_id` uuid REFERENCES users(id) ON DELETE CASCADE
+- `token` text NOT NULL
+- `platform` text NOT NULL CHECK (platform IN ('ios', 'android'))
+- `voip_token` text
+- `updated_at` timestamptz DEFAULT now()
+- PRIMARY KEY (user_id, platform)
+
+### Trigger
+Auto-create `users` row when someone signs up via Supabase Auth, using email prefix as initial display_name.
+
+### RLS Policies
+- users: read own profile, read others' profiles (for contact search), update own profile only
+- contacts: read/write own contacts, read contacts where you are the contact_user_id
+- push_tokens: read/write own tokens only
+
+## Changes
+
+| Area | Change |
+|------|--------|
+| New deps | `@supabase/supabase-js`, `react-native-config`, `@react-native-async-storage/async-storage` |
+| `supabase/client.ts` | Replace stub with real createClient() |
+| `AuthService.ts` | Rewrite to use real supabase.auth.* methods |
+| `SessionManager.ts` | Simplify — SDK handles token persistence |
+| `UserService.ts` | Rewrite to use real supabase.from('users').* |
+| `ContactsService.ts` | Rewrite with real queries + joins |
+| `authStore.ts` | Use supabase.auth.onAuthStateChange() |
+| `RootNavigator.tsx` | Wire isAuthenticated to auth state |
+| SQL migration | Script for tables + RLS + triggers |
+| signaling auth.ts | Real JWT verification with Supabase JWT secret |
+
+## Unchanged
+- Screen UI
+- Navigation structure
+- WebRTC/signaling infrastructure
+- Call flow
+- Native integrations (CallKit, push)
diff --git a/docs/plans/2026-03-01-supabase-setup.md b/docs/plans/2026-03-01-supabase-setup.md
new file mode 100644
index 0000000..ddd5ab1
--- /dev/null
+++ b/docs/plans/2026-03-01-supabase-setup.md
@@ -0,0 +1,1380 @@
+# Supabase Setup Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Connect the existing stub Supabase services to a real Supabase backend so the app can be tested with real user accounts and contacts.
+
+**Architecture:** Replace the stub `supabase/client.ts` with a real `@supabase/supabase-js` client configured with AsyncStorage for session persistence. The existing service layer (AuthService, UserService, ContactsService) already follows Supabase's API patterns — update them for real SDK types. Create database tables + RLS policies via SQL migration script.
+
+**Tech Stack:** `@supabase/supabase-js` v2, `react-native-config`, `@react-native-async-storage/async-storage`, Supabase Auth (email+password)
+
+---
+
+## Task 1: Install Dependencies
+
+**Files:**
+- Modify: `apps/mobile/package.json`
+
+**Step 1: Install npm packages**
+
+Run from project root:
+
+```bash
+cd /Users/gav/Programming/personal/farscry
+npm install --workspace=com.farscry.app @supabase/supabase-js @react-native-async-storage/async-storage react-native-config
+```
+
+**Step 2: Install iOS pods**
+
+```bash
+cd /Users/gav/Programming/personal/farscry/apps/mobile/ios && bundle exec pod install
+```
+
+If `bundle` is not set up, use:
+
+```bash
+cd /Users/gav/Programming/personal/farscry/apps/mobile/ios && pod install
+```
+
+**Step 3: Commit**
+
+```bash
+git add apps/mobile/package.json package-lock.json apps/mobile/ios/Podfile.lock
+git commit -m "Add Supabase JS SDK, AsyncStorage, and react-native-config"
+```
+
+---
+
+## Task 2: Environment Config
+
+**Files:**
+- Create: `apps/mobile/.env.example`
+- Create: `apps/mobile/.env` (gitignored)
+- Modify: `apps/mobile/ios/Farscry/Info.plist` (for react-native-config if needed)
+
+**Step 1: Create .env.example**
+
+```
+SUPABASE_URL=https://your-project.supabase.co
+SUPABASE_ANON_KEY=your-anon-key
+```
+
+**Step 2: Create .env with real credentials**
+
+Ask the user for their Supabase project URL and anon key from their Supabase dashboard (Settings → API). Create `apps/mobile/.env` with the real values.
+
+**Step 3: Verify .env is gitignored**
+
+The root `.gitignore` already has `.env` and `.env.local` entries. Verify with:
+
+```bash
+git check-ignore apps/mobile/.env
+```
+
+Expected: `apps/mobile/.env`
+
+**Step 4: Commit**
+
+```bash
+git add apps/mobile/.env.example
+git commit -m "Add .env.example for Supabase config"
+```
+
+---
+
+## Task 3: Replace Supabase Client Stub
+
+**Files:**
+- Rewrite: `apps/mobile/src/services/supabase/client.ts`
+
+**Step 1: Rewrite client.ts**
+
+Replace the entire file with:
+
+```typescript
+import 'react-native-url-polyfill/polyfill';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import {createClient} from '@supabase/supabase-js';
+import {AppState} from 'react-native';
+import Config from 'react-native-config';
+
+const supabaseUrl = Config.SUPABASE_URL ?? '';
+const supabaseAnonKey = Config.SUPABASE_ANON_KEY ?? '';
+
+if (!supabaseUrl || !supabaseAnonKey) {
+ console.warn(
+ 'Supabase credentials missing. Create apps/mobile/.env with SUPABASE_URL and SUPABASE_ANON_KEY.',
+ );
+}
+
+export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
+ auth: {
+ storage: AsyncStorage,
+ autoRefreshToken: true,
+ persistSession: true,
+ detectSessionInUrl: false,
+ },
+});
+
+// Auto-refresh tokens when app comes to foreground
+AppState.addEventListener('change', state => {
+ if (state === 'active') {
+ supabase.auth.startAutoRefresh();
+ } else {
+ supabase.auth.stopAutoRefresh();
+ }
+});
+```
+
+Note: Check if `react-native-url-polyfill` is needed. If the project targets React Native 0.84+ with Hermes, the URL API may be built in. If the import fails, remove it — the polyfill may not be necessary.
+
+**Step 2: Check if react-native-url-polyfill is needed**
+
+Run:
+
+```bash
+cd /Users/gav/Programming/personal/farscry && node -e "const {URL} = require('url'); console.log(new URL('https://example.com').hostname)"
+```
+
+If `react-native-url-polyfill` is needed (Supabase SDK requires URL API):
+
+```bash
+npm install --workspace=com.farscry.app react-native-url-polyfill
+```
+
+If NOT needed (RN 0.84+ with Hermes has URL built in), remove the `import 'react-native-url-polyfill/polyfill'` line from client.ts.
+
+**Step 3: Commit**
+
+```bash
+git add apps/mobile/src/services/supabase/client.ts
+git commit -m "Replace Supabase stub client with real SDK"
+```
+
+---
+
+## Task 4: Update AuthService for Real SDK
+
+**Files:**
+- Modify: `apps/mobile/src/services/auth/AuthService.ts`
+- Modify: `apps/mobile/src/services/auth/SessionManager.ts`
+
+**Step 1: Rewrite AuthService.ts**
+
+The existing AuthService already calls the correct Supabase methods (`supabase.auth.signUp`, etc.). The changes needed:
+
+1. Remove import of `SupabaseSession` type — use `Session` from `@supabase/supabase-js`
+2. Remove manual `SessionManager.persistSession` calls — the SDK auto-persists via AsyncStorage
+3. Remove the profile insert from `signUp` — the database trigger handles it (see Task 6)
+4. Remove `admin.deleteUser` call — anon key can't call admin endpoints
+
+Replace the entire file:
+
+```typescript
+import {supabase} from '../supabase/client';
+import type {Session} from '@supabase/supabase-js';
+
+export type AuthUser = {
+ id: string;
+ email?: string;
+};
+
+export type AuthState = {
+ user: AuthUser | null;
+ session: Session | null;
+};
+
+const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+const MIN_PASSWORD_LENGTH = 8;
+
+function validateEmail(email: string): string {
+ const trimmed = email.trim().toLowerCase();
+ if (!EMAIL_RE.test(trimmed)) {
+ throw new Error('Invalid email address');
+ }
+ return trimmed;
+}
+
+function validatePassword(password: string): void {
+ if (password.length < MIN_PASSWORD_LENGTH) {
+ throw new Error(`Password must be at least ${MIN_PASSWORD_LENGTH} characters`);
+ }
+}
+
+export const AuthService = {
+ async signUp(
+ email: string,
+ password: string,
+ displayName: string,
+ ): Promise {
+ const cleanEmail = validateEmail(email);
+ validatePassword(password);
+
+ const name = displayName.trim();
+ if (!name) {
+ throw new Error('Display name is required');
+ }
+
+ const {data, error} = await supabase.auth.signUp({
+ email: cleanEmail,
+ password,
+ options: {
+ data: {display_name: name},
+ },
+ });
+
+ if (error) throw new Error(error.message);
+ if (!data.user) throw new Error('Sign up failed');
+
+ // Update the user profile row created by the DB trigger
+ // The trigger creates a row with email prefix as display_name,
+ // so we update it with the user's chosen name
+ const {error: profileError} = await supabase
+ .from('users')
+ .update({display_name: name})
+ .eq('id', data.user.id);
+
+ if (profileError) {
+ console.warn('Failed to update display name:', profileError.message);
+ }
+
+ return {
+ user: {id: data.user.id, email: cleanEmail},
+ session: data.session,
+ };
+ },
+
+ async signIn(email: string, password: string): Promise {
+ const cleanEmail = validateEmail(email);
+ if (!password) throw new Error('Password is required');
+
+ const {data, error} = await supabase.auth.signInWithPassword({
+ email: cleanEmail,
+ password,
+ });
+
+ if (error) throw new Error(error.message);
+ if (!data.session) throw new Error('Sign in failed');
+
+ return {
+ user: {id: data.session.user.id, email: cleanEmail},
+ session: data.session,
+ };
+ },
+
+ async signOut(): Promise {
+ const {error} = await supabase.auth.signOut();
+ if (error) throw new Error(error.message);
+ },
+
+ async resetPassword(email: string): Promise {
+ const cleanEmail = validateEmail(email);
+ const {error} = await supabase.auth.resetPasswordForEmail(cleanEmail);
+ if (error) throw new Error(error.message);
+ },
+
+ async deleteAccount(): Promise {
+ const {data} = await supabase.auth.getSession();
+ const userId = data.session?.user.id;
+ if (!userId) throw new Error('Not authenticated');
+
+ // Remove user data — cascade will handle contacts and push_tokens
+ const {error} = await supabase.from('users').delete().eq('id', userId);
+ if (error) throw new Error(error.message);
+
+ await supabase.auth.signOut();
+ },
+
+ async getSession(): Promise {
+ const {data, error} = await supabase.auth.getSession();
+ if (error) throw new Error(error.message);
+ return data.session;
+ },
+
+ onAuthStateChange(
+ callback: (event: string, session: Session | null) => void,
+ ): {unsubscribe: () => void} {
+ const {data} = supabase.auth.onAuthStateChange(callback);
+ return data.subscription;
+ },
+};
+```
+
+**Step 2: Simplify SessionManager.ts**
+
+The SDK now handles session persistence via AsyncStorage. SessionManager is no longer needed for persist/load but keep it as a thin utility for any manual session checks. Replace the file:
+
+```typescript
+import type {Session} from '@supabase/supabase-js';
+
+export const SessionManager = {
+ isExpiringSoon(session: Session): boolean {
+ const EXPIRY_BUFFER_MS = 60_000;
+ return (session.expires_at ?? 0) * 1000 - Date.now() < EXPIRY_BUFFER_MS;
+ },
+};
+```
+
+**Step 3: Commit**
+
+```bash
+git add apps/mobile/src/services/auth/AuthService.ts apps/mobile/src/services/auth/SessionManager.ts
+git commit -m "Update auth services to use real Supabase SDK"
+```
+
+---
+
+## Task 5: Update UserService and ContactsService
+
+**Files:**
+- Modify: `apps/mobile/src/services/user/UserService.ts`
+- Modify: `apps/mobile/src/services/user/ContactsService.ts`
+
+**Step 1: Rewrite UserService.ts**
+
+Remove the `from()` generic (real SDK doesn't support it on `from()`). Add `.select()` after mutations to return data. Replace the entire file:
+
+```typescript
+import {supabase} from '../supabase/client';
+
+export type UserProfile = {
+ id: string;
+ display_name: string;
+ avatar_url: string | null;
+ created_at: string;
+ updated_at: string;
+};
+
+export type ProfileUpdate = {
+ display_name?: string;
+ avatar_url?: string | null;
+};
+
+export const UserService = {
+ async getProfile(userId: string): Promise {
+ const {data, error} = await supabase
+ .from('users')
+ .select('*')
+ .eq('id', userId)
+ .single();
+
+ if (error) throw new Error(error.message);
+ return data as UserProfile;
+ },
+
+ async updateProfile(updates: ProfileUpdate): Promise {
+ const {data: sessionData} = await supabase.auth.getSession();
+ const userId = sessionData.session?.user.id;
+ if (!userId) throw new Error('Not authenticated');
+
+ if (updates.display_name !== undefined) {
+ const name = updates.display_name.trim();
+ if (!name) throw new Error('Display name cannot be empty');
+ updates.display_name = name;
+ }
+
+ const {data, error} = await supabase
+ .from('users')
+ .update({...updates, updated_at: new Date().toISOString()})
+ .eq('id', userId)
+ .select()
+ .single();
+
+ if (error) throw new Error(error.message);
+ return data as UserProfile;
+ },
+
+ async searchUsers(query: string): Promise {
+ const trimmed = query.trim();
+ if (!trimmed) return [];
+
+ const {data, error} = await supabase
+ .from('users')
+ .select('*')
+ .ilike('display_name', `%${trimmed}%`)
+ .limit(20);
+
+ if (error) throw new Error(error.message);
+ return (data ?? []) as UserProfile[];
+ },
+
+ async getUserById(id: string): Promise {
+ const {data, error} = await supabase
+ .from('users')
+ .select('*')
+ .eq('id', id)
+ .maybeSingle();
+
+ if (error) throw new Error(error.message);
+ return data as UserProfile | null;
+ },
+};
+```
+
+**Step 2: Rewrite ContactsService.ts**
+
+Same changes — remove `from()` generic, add `.select()` after `.insert()`:
+
+```typescript
+import {supabase} from '../supabase/client';
+import type {UserProfile} from './UserService';
+
+export type Contact = {
+ user_id: string;
+ contact_user_id: string;
+ is_favorite: boolean;
+ added_at: string;
+ profile?: UserProfile;
+};
+
+export const ContactsService = {
+ async getContacts(): Promise {
+ const {data: sessionData} = await supabase.auth.getSession();
+ const userId = sessionData.session?.user.id;
+ if (!userId) throw new Error('Not authenticated');
+
+ const {data, error} = await supabase
+ .from('contacts')
+ .select('*, profile:users!contact_user_id(*)')
+ .eq('user_id', userId)
+ .order('added_at', {ascending: false});
+
+ if (error) throw new Error(error.message);
+ return (data ?? []) as Contact[];
+ },
+
+ async addContact(contactUserId: string): Promise {
+ const {data: sessionData} = await supabase.auth.getSession();
+ const userId = sessionData.session?.user.id;
+ if (!userId) throw new Error('Not authenticated');
+
+ if (contactUserId === userId) {
+ throw new Error('Cannot add yourself as a contact');
+ }
+
+ const {data, error} = await supabase
+ .from('contacts')
+ .insert({user_id: userId, contact_user_id: contactUserId})
+ .select('*, profile:users!contact_user_id(*)')
+ .single();
+
+ if (error) {
+ if (error.code === '23505') {
+ throw new Error('Contact already added');
+ }
+ throw new Error(error.message);
+ }
+ return data as Contact;
+ },
+
+ async removeContact(contactUserId: string): Promise {
+ const {data: sessionData} = await supabase.auth.getSession();
+ const userId = sessionData.session?.user.id;
+ if (!userId) throw new Error('Not authenticated');
+
+ const {error} = await supabase
+ .from('contacts')
+ .delete()
+ .eq('user_id', userId)
+ .eq('contact_user_id', contactUserId);
+
+ if (error) throw new Error(error.message);
+ },
+
+ async toggleFavorite(contactUserId: string): Promise {
+ const {data: sessionData} = await supabase.auth.getSession();
+ const userId = sessionData.session?.user.id;
+ if (!userId) throw new Error('Not authenticated');
+
+ const {data: existing, error: fetchError} = await supabase
+ .from('contacts')
+ .select('is_favorite')
+ .eq('user_id', userId)
+ .eq('contact_user_id', contactUserId)
+ .single();
+
+ if (fetchError) throw new Error(fetchError.message);
+ if (!existing) throw new Error('Contact not found');
+
+ const newValue = !existing.is_favorite;
+
+ const {error} = await supabase
+ .from('contacts')
+ .update({is_favorite: newValue})
+ .eq('user_id', userId)
+ .eq('contact_user_id', contactUserId);
+
+ if (error) throw new Error(error.message);
+ return newValue;
+ },
+};
+```
+
+**Step 3: Commit**
+
+```bash
+git add apps/mobile/src/services/user/UserService.ts apps/mobile/src/services/user/ContactsService.ts
+git commit -m "Update UserService and ContactsService for real Supabase SDK"
+```
+
+---
+
+## Task 6: Create SQL Migration Script
+
+**Files:**
+- Create: `supabase/migrations/001_initial_schema.sql`
+
+**Step 1: Create migration directory**
+
+```bash
+mkdir -p /Users/gav/Programming/personal/farscry/supabase/migrations
+```
+
+**Step 2: Write the SQL migration**
+
+Create `supabase/migrations/001_initial_schema.sql`:
+
+```sql
+-- Farscry initial schema
+-- Run this in the Supabase SQL Editor (Dashboard → SQL Editor → New query)
+
+-- ============================================
+-- TABLES
+-- ============================================
+
+-- User profiles (synced from auth.users)
+create table if not exists public.users (
+ id uuid primary key references auth.users(id) on delete cascade,
+ display_name text not null,
+ avatar_url text,
+ created_at timestamptz not null default now(),
+ updated_at timestamptz not null default now()
+);
+
+-- Contacts
+create table if not exists public.contacts (
+ user_id uuid not null references public.users(id) on delete cascade,
+ contact_user_id uuid not null references public.users(id) on delete cascade,
+ is_favorite boolean not null default false,
+ added_at timestamptz not null default now(),
+ primary key (user_id, contact_user_id),
+ constraint no_self_contact check (user_id != contact_user_id)
+);
+
+-- Push notification tokens
+create table if not exists public.push_tokens (
+ user_id uuid not null references public.users(id) on delete cascade,
+ token text not null,
+ platform text not null check (platform in ('ios', 'android')),
+ voip_token text,
+ updated_at timestamptz not null default now(),
+ primary key (user_id, platform)
+);
+
+-- ============================================
+-- INDEXES
+-- ============================================
+
+create index if not exists idx_contacts_contact_user on public.contacts(contact_user_id);
+create index if not exists idx_contacts_user on public.contacts(user_id);
+create index if not exists idx_users_display_name on public.users using gin (display_name gin_trgm_ops);
+
+-- Enable the trigram extension for fuzzy display_name search
+create extension if not exists pg_trgm;
+
+-- ============================================
+-- TRIGGER: Auto-create user profile on signup
+-- ============================================
+
+create or replace function public.handle_new_user()
+returns trigger
+language plpgsql
+security definer set search_path = ''
+as $$
+begin
+ insert into public.users (id, display_name)
+ values (
+ new.id,
+ coalesce(
+ new.raw_user_meta_data ->> 'display_name',
+ split_part(new.email, '@', 1)
+ )
+ );
+ return new;
+end;
+$$;
+
+-- Drop trigger if it exists, then create
+drop trigger if exists on_auth_user_created on auth.users;
+create trigger on_auth_user_created
+ after insert on auth.users
+ for each row execute function public.handle_new_user();
+
+-- ============================================
+-- TRIGGER: Auto-update updated_at
+-- ============================================
+
+create or replace function public.update_updated_at()
+returns trigger
+language plpgsql
+as $$
+begin
+ new.updated_at = now();
+ return new;
+end;
+$$;
+
+drop trigger if exists users_updated_at on public.users;
+create trigger users_updated_at
+ before update on public.users
+ for each row execute function public.update_updated_at();
+
+-- ============================================
+-- ROW LEVEL SECURITY
+-- ============================================
+
+alter table public.users enable row level security;
+alter table public.contacts enable row level security;
+alter table public.push_tokens enable row level security;
+
+-- Users: anyone authenticated can read profiles (for search/contacts)
+create policy "Users can read all profiles"
+ on public.users for select
+ to authenticated
+ using (true);
+
+-- Users: can only update own profile
+create policy "Users can update own profile"
+ on public.users for update
+ to authenticated
+ using (auth.uid() = id)
+ with check (auth.uid() = id);
+
+-- Users: can delete own profile
+create policy "Users can delete own profile"
+ on public.users for delete
+ to authenticated
+ using (auth.uid() = id);
+
+-- Contacts: can read own contacts
+create policy "Users can read own contacts"
+ on public.contacts for select
+ to authenticated
+ using (auth.uid() = user_id);
+
+-- Contacts: can insert own contacts
+create policy "Users can add contacts"
+ on public.contacts for insert
+ to authenticated
+ with check (auth.uid() = user_id);
+
+-- Contacts: can update own contacts (favorite toggle)
+create policy "Users can update own contacts"
+ on public.contacts for update
+ to authenticated
+ using (auth.uid() = user_id)
+ with check (auth.uid() = user_id);
+
+-- Contacts: can delete own contacts
+create policy "Users can remove contacts"
+ on public.contacts for delete
+ to authenticated
+ using (auth.uid() = user_id);
+
+-- Push tokens: full access to own tokens only
+create policy "Users can manage own push tokens"
+ on public.push_tokens for all
+ to authenticated
+ using (auth.uid() = user_id)
+ with check (auth.uid() = user_id);
+```
+
+**Step 3: Run the migration**
+
+The user must run this SQL in their Supabase dashboard:
+1. Go to Supabase Dashboard → SQL Editor
+2. Click "New query"
+3. Paste the entire contents of `001_initial_schema.sql`
+4. Click "Run"
+
+**Step 4: Disable email confirmation for testing**
+
+In Supabase Dashboard:
+1. Go to Authentication → Providers → Email
+2. Turn OFF "Confirm email" (so users can sign in immediately after signup)
+3. This is for testing only — re-enable before production
+
+**Step 5: Commit**
+
+```bash
+git add supabase/migrations/001_initial_schema.sql
+git commit -m "Add initial database schema migration for users, contacts, push_tokens"
+```
+
+---
+
+## Task 7: Update Auth Store
+
+**Files:**
+- Modify: `apps/mobile/src/stores/authStore.ts`
+
+**Step 1: Rewrite authStore.ts**
+
+Update to use real SDK types. The SDK handles session persistence, so the restore logic simplifies to just calling `supabase.auth.getSession()`:
+
+```typescript
+import React, {createContext, useContext, useEffect, useReducer, useCallback} from 'react';
+import {AuthService, type AuthUser} from '../services/auth/AuthService';
+import {supabase} from '../services/supabase/client';
+import type {Session} from '@supabase/supabase-js';
+
+type AuthState = {
+ user: AuthUser | null;
+ session: Session | null;
+ loading: boolean;
+ error: string | null;
+};
+
+type AuthAction =
+ | {type: 'LOADING'}
+ | {type: 'SIGNED_IN'; user: AuthUser; session: Session}
+ | {type: 'SIGNED_OUT'}
+ | {type: 'ERROR'; error: string}
+ | {type: 'CLEAR_ERROR'};
+
+const initialState: AuthState = {
+ user: null,
+ session: null,
+ loading: true,
+ error: null,
+};
+
+function authReducer(state: AuthState, action: AuthAction): AuthState {
+ switch (action.type) {
+ case 'LOADING':
+ return {...state, loading: true, error: null};
+ case 'SIGNED_IN':
+ return {user: action.user, session: action.session, loading: false, error: null};
+ case 'SIGNED_OUT':
+ return {user: null, session: null, loading: false, error: null};
+ case 'ERROR':
+ return {...state, loading: false, error: action.error};
+ case 'CLEAR_ERROR':
+ return {...state, error: null};
+ }
+}
+
+type AuthContextValue = AuthState & {
+ signUp: (email: string, password: string, displayName: string) => Promise;
+ signIn: (email: string, password: string) => Promise;
+ signOut: () => Promise;
+ clearError: () => void;
+};
+
+const AuthContext = createContext(null);
+
+export function AuthProvider({children}: {children: React.ReactNode}) {
+ const [state, dispatch] = useReducer(authReducer, initialState);
+
+ // Restore session on mount + listen for auth changes
+ useEffect(() => {
+ // Get initial session from SDK (auto-persisted in AsyncStorage)
+ supabase.auth.getSession().then(({data: {session}}) => {
+ if (session) {
+ dispatch({
+ type: 'SIGNED_IN',
+ user: {id: session.user.id, email: session.user.email},
+ session,
+ });
+ } else {
+ dispatch({type: 'SIGNED_OUT'});
+ }
+ });
+
+ // Listen for auth state changes
+ const {data: {subscription}} = supabase.auth.onAuthStateChange(
+ (_event, session) => {
+ if (session) {
+ dispatch({
+ type: 'SIGNED_IN',
+ user: {id: session.user.id, email: session.user.email},
+ session,
+ });
+ } else {
+ dispatch({type: 'SIGNED_OUT'});
+ }
+ },
+ );
+
+ return () => subscription.unsubscribe();
+ }, []);
+
+ const signUp = useCallback(
+ async (email: string, password: string, displayName: string) => {
+ dispatch({type: 'LOADING'});
+ try {
+ const result = await AuthService.signUp(email, password, displayName);
+ if (result.session) {
+ dispatch({
+ type: 'SIGNED_IN',
+ user: result.user,
+ session: result.session,
+ });
+ } else {
+ // Email confirmation required (shouldn't happen if disabled)
+ dispatch({type: 'SIGNED_OUT'});
+ }
+ } catch (e: unknown) {
+ dispatch({type: 'ERROR', error: e instanceof Error ? e.message : 'Sign up failed'});
+ }
+ },
+ [],
+ );
+
+ const signIn = useCallback(async (email: string, password: string) => {
+ dispatch({type: 'LOADING'});
+ try {
+ const result = await AuthService.signIn(email, password);
+ dispatch({
+ type: 'SIGNED_IN',
+ user: result.user,
+ session: result.session!,
+ });
+ } catch (e: unknown) {
+ dispatch({type: 'ERROR', error: e instanceof Error ? e.message : 'Sign in failed'});
+ }
+ }, []);
+
+ const signOut = useCallback(async () => {
+ dispatch({type: 'LOADING'});
+ try {
+ await AuthService.signOut();
+ dispatch({type: 'SIGNED_OUT'});
+ } catch (e: unknown) {
+ dispatch({type: 'ERROR', error: e instanceof Error ? e.message : 'Sign out failed'});
+ }
+ }, []);
+
+ const clearError = useCallback(() => dispatch({type: 'CLEAR_ERROR'}), []);
+
+ const value: AuthContextValue = {
+ ...state,
+ signUp,
+ signIn,
+ signOut,
+ clearError,
+ };
+
+ return React.createElement(AuthContext.Provider, {value}, children);
+}
+
+export function useAuth(): AuthContextValue {
+ const ctx = useContext(AuthContext);
+ if (!ctx) {
+ throw new Error('useAuth must be used within AuthProvider');
+ }
+ return ctx;
+}
+```
+
+**Step 2: Commit**
+
+```bash
+git add apps/mobile/src/stores/authStore.ts
+git commit -m "Update authStore to use real Supabase session management"
+```
+
+---
+
+## Task 8: Wire App Providers and Navigation
+
+**Files:**
+- Modify: `apps/mobile/App.tsx`
+- Modify: `apps/mobile/src/navigation/RootNavigator.tsx`
+
+**Step 1: Wrap App.tsx with AuthProvider and ContactsProvider**
+
+```typescript
+import React from 'react';
+import {StatusBar} from 'react-native';
+import {SafeAreaProvider} from 'react-native-safe-area-context';
+import {NavigationContainer} from '@react-navigation/native';
+import {GestureHandlerRootView} from 'react-native-gesture-handler';
+import {AuthProvider} from './src/stores/authStore';
+import {ContactsProvider} from './src/stores/contactsStore';
+import {RootNavigator} from './src/navigation/RootNavigator';
+import {colors} from './src/theme/colors';
+
+const navTheme = {
+ dark: true,
+ colors: {
+ primary: colors.accent,
+ background: colors.background,
+ card: colors.surface,
+ text: colors.text,
+ border: colors.border,
+ notification: colors.accent,
+ },
+ fonts: {
+ regular: {fontFamily: 'System', fontWeight: '400' as const},
+ medium: {fontFamily: 'System', fontWeight: '500' as const},
+ bold: {fontFamily: 'System', fontWeight: '700' as const},
+ heavy: {fontFamily: 'System', fontWeight: '900' as const},
+ },
+};
+
+export default function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+```
+
+**Step 2: Wire RootNavigator to real auth state**
+
+Replace the `useState(false)` with `useAuth()`:
+
+```typescript
+import React from 'react';
+import {ActivityIndicator, View} from 'react-native';
+import {createNativeStackNavigator} from '@react-navigation/native-stack';
+import {useAuth} from '../stores/authStore';
+import {MainTabs} from './MainTabs';
+import {LoginScreen} from '../screens/auth/LoginScreen';
+import {SignupScreen} from '../screens/auth/SignupScreen';
+import {OnboardingScreen} from '../screens/auth/OnboardingScreen';
+import {IncomingCallScreen} from '../screens/call/IncomingCallScreen';
+import {OutgoingCallScreen} from '../screens/call/OutgoingCallScreen';
+import {ActiveCallScreen} from '../screens/call/ActiveCallScreen';
+import {AddContactScreen} from '../screens/contacts/AddContactScreen';
+import {ContactDetailScreen} from '../screens/contacts/ContactDetailScreen';
+import {colors} from '../theme/colors';
+import type {RootStackParamList, AuthStackParamList} from './types';
+
+const RootStack = createNativeStackNavigator();
+const AuthStack = createNativeStackNavigator();
+
+function AuthNavigator() {
+ return (
+
+
+
+
+
+ );
+}
+
+export function RootNavigator() {
+ const {user, loading} = useAuth();
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {user ? (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+ ) : (
+
+ )}
+
+ );
+}
+```
+
+**Step 3: Commit**
+
+```bash
+git add apps/mobile/App.tsx apps/mobile/src/navigation/RootNavigator.tsx
+git commit -m "Wire AuthProvider, ContactsProvider, and auth-gated navigation"
+```
+
+---
+
+## Task 9: Wire Auth Screens
+
+**Files:**
+- Modify: `apps/mobile/src/screens/auth/LoginScreen.tsx`
+- Modify: `apps/mobile/src/screens/auth/SignupScreen.tsx`
+
+**Step 1: Wire LoginScreen**
+
+Add `useAuth()` hook and connect `handleLogin`. Add loading and error display:
+
+In `LoginScreen.tsx`, make these changes:
+
+1. Add imports: `import {useAuth} from '../../stores/authStore';` and `import {ActivityIndicator} from 'react-native';`
+2. Inside the component, add: `const {signIn, loading, error, clearError} = useAuth();`
+3. Replace `handleLogin`:
+```typescript
+async function handleLogin() {
+ await signIn(email, password);
+}
+```
+4. Add error display after the form inputs (before the button):
+```tsx
+{error && (
+ {error}
+)}
+```
+5. Update the button to show loading state:
+```tsx
+
+ {loading ? (
+
+ ) : (
+ Sign in
+ )}
+
+```
+6. Add to styles:
+```typescript
+errorText: {
+ ...typography.footnote,
+ color: colors.callRed,
+ textAlign: 'center',
+},
+```
+7. Clear error when navigating away — add `onPress` to signup link:
+```typescript
+onPress={() => { clearError(); navigation.navigate('Signup'); }}
+```
+
+**Step 2: Wire SignupScreen**
+
+Same pattern. In `SignupScreen.tsx`:
+
+1. Add imports: `import {useAuth} from '../../stores/authStore';` and `import {ActivityIndicator} from 'react-native';`
+2. Inside the component: `const {signUp, loading, error, clearError} = useAuth();`
+3. Replace `handleSignup`:
+```typescript
+async function handleSignup() {
+ await signUp(email, password, displayName);
+}
+```
+4. Add error display (same as login)
+5. Update button with loading state (same pattern, disabled when `!isValid || loading`)
+6. Add `errorText` style (same)
+7. Clear error on navigate: `onPress={() => { clearError(); navigation.navigate('Login'); }}`
+
+**Step 3: Commit**
+
+```bash
+git add apps/mobile/src/screens/auth/LoginScreen.tsx apps/mobile/src/screens/auth/SignupScreen.tsx
+git commit -m "Wire login and signup screens to auth service"
+```
+
+---
+
+## Task 10: Wire Contacts and Favorites Screens
+
+**Files:**
+- Modify: `apps/mobile/src/screens/main/ContactsScreen.tsx`
+- Modify: `apps/mobile/src/screens/main/FavoritesScreen.tsx`
+- Modify: `apps/mobile/src/screens/contacts/AddContactScreen.tsx`
+- Modify: `apps/mobile/src/screens/contacts/ContactDetailScreen.tsx`
+
+**Step 1: Wire ContactsScreen to real data**
+
+Replace mock data with `useContacts()`:
+
+1. Add import: `import {useContacts} from '../../stores/contactsStore';`
+2. Remove `MOCK_CONTACTS` array and the local `Contact` type
+3. Inside component, add:
+```typescript
+const {contacts, fetchContacts, loading} = useContacts();
+```
+4. Add useEffect to fetch contacts on mount:
+```typescript
+useEffect(() => { fetchContacts(); }, [fetchContacts]);
+```
+5. Update the `filtered` memo to use `contacts` instead of `MOCK_CONTACTS`:
+```typescript
+const filtered = useMemo(() => {
+ if (!search.trim()) return contacts;
+ const q = search.toLowerCase();
+ return contacts.filter(c => {
+ const name = c.profile?.display_name ?? '';
+ return name.toLowerCase().includes(q);
+ });
+}, [search, contacts]);
+```
+6. Update `buildSections` to work with Contact type:
+```typescript
+function buildSections(items: Contact[]): Section[] {
+ const map = new Map();
+ for (const c of items) {
+ const name = c.profile?.display_name ?? '?';
+ const letter = name.charAt(0).toUpperCase();
+ const group = map.get(letter) ?? [];
+ group.push(c);
+ map.set(letter, group);
+ }
+ return Array.from(map.entries())
+ .sort(([a], [b]) => a.localeCompare(b))
+ .map(([title, data]) => ({title, data}));
+}
+```
+7. Update `Section` type to use `Contact` from contactsStore
+8. Update `renderItem` to use `item.profile?.display_name` and `item.contact_user_id`
+
+**Step 2: Wire FavoritesScreen to real data**
+
+1. Add import: `import {useContacts} from '../../stores/contactsStore';`
+2. Remove `MOCK_FAVORITES` and local `FavoriteContact` type
+3. Inside component: `const {favorites, fetchContacts} = useContacts();`
+4. Add useEffect: `useEffect(() => { fetchContacts(); }, [fetchContacts]);`
+5. Update references from `MOCK_FAVORITES` to `favorites`
+6. Update renderItem to use `item.profile?.display_name` and `item.contact_user_id`
+
+**Step 3: Wire AddContactScreen to real services**
+
+1. Add imports:
+```typescript
+import {UserService, type UserProfile} from '../../services/user/UserService';
+import {useContacts} from '../../stores/contactsStore';
+```
+2. Replace `MOCK_RESULTS` with real search using `UserService.searchUsers()`
+3. Inside component: `const {addContact} = useContacts();`
+4. Replace `handleSearch` with debounced search:
+```typescript
+async function handleSearch(text: string) {
+ setQuery(text);
+ if (text.trim().length >= 2) {
+ try {
+ const users = await UserService.searchUsers(text);
+ setResults(users);
+ setSearched(true);
+ } catch {
+ setResults([]);
+ setSearched(true);
+ }
+ } else {
+ setResults([]);
+ setSearched(false);
+ }
+}
+```
+5. Replace `handleAdd`:
+```typescript
+async function handleAdd(user: UserProfile) {
+ try {
+ await addContact(user.id);
+ navigation.goBack();
+ } catch (e: unknown) {
+ Alert.alert('Error', e instanceof Error ? e.message : 'Failed to add contact');
+ }
+}
+```
+6. Update `SearchResult` type references to `UserProfile`
+7. Update renderItem to use `item.display_name` instead of `item.name`/`item.username`
+
+**Step 4: Wire ContactDetailScreen to real services**
+
+1. Add import: `import {useContacts} from '../../stores/contactsStore';`
+2. Inside component: `const {contacts, removeContact, toggleFavorite} = useContacts();`
+3. Derive `isFavorite` from real data:
+```typescript
+const contact = contacts.find(c => c.contact_user_id === contactId);
+const isFavorite = contact?.is_favorite ?? false;
+```
+4. Wire `handleRemove`:
+```typescript
+onPress: async () => {
+ await removeContact(contactId);
+ navigation.goBack();
+},
+```
+5. Wire favorite toggle:
+```typescript
+onPress={() => toggleFavorite(contactId)}
+```
+
+**Step 5: Commit**
+
+```bash
+git add apps/mobile/src/screens/main/ContactsScreen.tsx apps/mobile/src/screens/main/FavoritesScreen.tsx apps/mobile/src/screens/contacts/AddContactScreen.tsx apps/mobile/src/screens/contacts/ContactDetailScreen.tsx
+git commit -m "Wire contacts, favorites, and search screens to real Supabase data"
+```
+
+---
+
+## Task 11: Update Signaling Server JWT Verification
+
+**Files:**
+- Modify: `packages/signaling/package.json`
+- Modify: `packages/signaling/src/auth.ts`
+
+**Step 1: Install jose in signaling package**
+
+```bash
+cd /Users/gav/Programming/personal/farscry
+npm install --workspace=@farscry/signaling jose
+```
+
+**Step 2: Update auth.ts with real JWT verification**
+
+Replace the file:
+
+```typescript
+import {jwtVerify} from 'jose';
+import {logger} from './logger.js';
+
+export interface AuthResult {
+ valid: boolean;
+ userId?: string;
+ error?: string;
+}
+
+const SUPABASE_JWT_SECRET = process.env.SUPABASE_JWT_SECRET ?? '';
+
+let secretKey: Uint8Array | null = null;
+function getSecret(): Uint8Array {
+ if (!secretKey) {
+ if (!SUPABASE_JWT_SECRET) {
+ throw new Error('SUPABASE_JWT_SECRET environment variable is not set');
+ }
+ secretKey = new TextEncoder().encode(SUPABASE_JWT_SECRET);
+ }
+ return secretKey;
+}
+
+export async function validateToken(token: string): Promise {
+ try {
+ if (!token || typeof token !== 'string') {
+ return {valid: false, error: 'missing token'};
+ }
+
+ const {payload} = await jwtVerify(token, getSecret(), {
+ audience: 'authenticated',
+ });
+
+ if (!payload.sub || typeof payload.sub !== 'string') {
+ return {valid: false, error: 'missing subject claim'};
+ }
+
+ return {valid: true, userId: payload.sub};
+ } catch (err) {
+ const message = err instanceof Error ? err.message : 'invalid token';
+ logger.warn(`Token validation failed: ${message}`);
+ return {valid: false, error: message};
+ }
+}
+```
+
+Note: `validateToken` is now `async`. All callers in `server.ts` that call `validateToken()` must `await` it. Check `server.ts` for call sites and add `await`.
+
+**Step 3: Update server.ts call sites**
+
+Search for `validateToken` usage in `server.ts` and ensure all calls use `await`. The function signature changed from sync to async.
+
+**Step 4: Commit**
+
+```bash
+git add packages/signaling/package.json package-lock.json packages/signaling/src/auth.ts packages/signaling/src/server.ts
+git commit -m "Add real JWT verification to signaling server using jose"
+```
+
+---
+
+## Task 12: Verify and Test
+
+**Step 1: TypeScript check**
+
+```bash
+cd /Users/gav/Programming/personal/farscry && npm run typecheck
+```
+
+Fix any type errors.
+
+**Step 2: Build and run iOS**
+
+```bash
+cd /Users/gav/Programming/personal/farscry && npm run mobile:ios
+```
+
+**Step 3: Manual test checklist**
+
+- [ ] App launches and shows onboarding/login screen
+- [ ] Can navigate to signup screen
+- [ ] Can create account with display name, email, password
+- [ ] After signup, navigates to main tabs
+- [ ] Can sign out (from settings)
+- [ ] Can sign back in
+- [ ] Session persists across app restart
+- [ ] Can search for other users by display name
+- [ ] Can add a contact
+- [ ] Contacts appear in contacts list
+- [ ] Can favorite/unfavorite a contact
+- [ ] Favorites appear in favorites tab
+- [ ] Can remove a contact
+
+**Step 4: Final commit**
+
+Fix any issues found during testing, then:
+
+```bash
+git add -A
+git commit -m "Fix issues found during Supabase integration testing"
+```
diff --git a/package-lock.json b/package-lock.json
index cb5ea72..9b064e4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,16 +19,25 @@
"name": "com.farscry.app",
"version": "0.0.1",
"dependencies": {
+ "@react-native-async-storage/async-storage": "^3.0.1",
"@react-native/new-app-screen": "0.84.1",
"@react-navigation/bottom-tabs": "^7.15.2",
"@react-navigation/native": "^7.1.31",
"@react-navigation/native-stack": "^7.14.2",
+ "@supabase/supabase-js": "^2.98.0",
"react": "19.2.3",
"react-native": "0.84.1",
+ "react-native-callkeep": "^4.3.16",
+ "react-native-config": "^1.6.1",
"react-native-gesture-handler": "^2.30.0",
+ "react-native-incall-manager": "^4.2.1",
+ "react-native-permissions": "^5.4.4",
"react-native-safe-area-context": "^5.5.2",
"react-native-screens": "^4.24.0",
- "react-native-svg": "^15.15.3"
+ "react-native-svg": "^15.15.3",
+ "react-native-url-polyfill": "^3.0.0",
+ "react-native-voip-push-notification": "^3.3.3",
+ "react-native-webrtc": "^124.0.7"
},
"devDependencies": {
"@babel/core": "^7.25.2",
@@ -3179,6 +3188,19 @@
"node": ">= 8"
}
},
+ "node_modules/@react-native-async-storage/async-storage": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-3.0.1.tgz",
+ "integrity": "sha512-VHwHb19sMg4Xh3W5M6YmJ/HSm1uh8RYFa6Dozm9o/jVYTYUgz2BmDXqXF7sum3glQaR34/hlwVc94px1sSdC2A==",
+ "license": "MIT",
+ "dependencies": {
+ "idb": "8.0.3"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/@react-native-community/cli": {
"version": "20.1.0",
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-20.1.0.tgz",
@@ -4306,6 +4328,86 @@
"@sinonjs/commons": "^3.0.0"
}
},
+ "node_modules/@supabase/auth-js": {
+ "version": "2.98.0",
+ "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.98.0.tgz",
+ "integrity": "sha512-GBH361T0peHU91AQNzOlIrjUZw9TZbB9YDRiyFgk/3Kvr3/Z1NWUZ2athWTfHhwNNi8IrW00foyFxQD9IO/Trg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/functions-js": {
+ "version": "2.98.0",
+ "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.98.0.tgz",
+ "integrity": "sha512-N/xEyiNU5Org+d+PNCpv+TWniAXRzxIURxDYsS/m2I/sfAB/HcM9aM2Dmf5edj5oWb9GxID1OBaZ8NMmPXL+Lg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/postgrest-js": {
+ "version": "2.98.0",
+ "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.98.0.tgz",
+ "integrity": "sha512-v6e9WeZuJijzUut8HyXu6gMqWFepIbaeaMIm1uKzei4yLg9bC9OtEW9O14LE/9ezqNbSAnSLO5GtOLFdm7Bpkg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/realtime-js": {
+ "version": "2.98.0",
+ "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.98.0.tgz",
+ "integrity": "sha512-rOWt28uGyFipWOSd+n0WVMr9kUXiWaa7J4hvyLCIHjRFqWm1z9CaaKAoYyfYMC1Exn3WT8WePCgiVhlAtWC2yw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/phoenix": "^1.6.6",
+ "@types/ws": "^8.18.1",
+ "tslib": "2.8.1",
+ "ws": "^8.18.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/storage-js": {
+ "version": "2.98.0",
+ "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.98.0.tgz",
+ "integrity": "sha512-tzr2mG+v7ILSAZSfZMSL9OPyIH4z1ikgQ8EcQTKfMRz4EwmlFt3UnJaGzSOxyvF5b+fc9So7qdSUWTqGgeLokQ==",
+ "license": "MIT",
+ "dependencies": {
+ "iceberg-js": "^0.8.1",
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/supabase-js": {
+ "version": "2.98.0",
+ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.98.0.tgz",
+ "integrity": "sha512-Ohc97CtInLwZyiSASz7tT9/Abm/vqnIbO9REp+PivVUII8UZsuI3bngRQnYgJdFoOIwvaEII1fX1qy8x0CyNiw==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/auth-js": "2.98.0",
+ "@supabase/functions-js": "2.98.0",
+ "@supabase/postgrest-js": "2.98.0",
+ "@supabase/realtime-js": "2.98.0",
+ "@supabase/storage-js": "2.98.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -4431,6 +4533,12 @@
"undici-types": "~7.18.0"
}
},
+ "node_modules/@types/phoenix": {
+ "version": "1.6.7",
+ "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz",
+ "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==",
+ "license": "MIT"
+ },
"node_modules/@types/react": {
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
@@ -4468,7 +4576,6 @@
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
@@ -5610,7 +5717,6 @@
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
- "devOptional": true,
"funding": [
{
"type": "github",
@@ -8273,6 +8379,15 @@
"node": ">=10.17.0"
}
},
+ "node_modules/iceberg-js": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
+ "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -8286,11 +8401,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/idb": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz",
+ "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==",
+ "license": "ISC"
+ },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
- "devOptional": true,
"funding": [
{
"type": "github",
@@ -9621,6 +9741,15 @@
"@sideway/pinpoint": "^2.0.0"
}
},
+ "node_modules/jose": {
+ "version": "6.1.3",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz",
+ "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -11370,7 +11499,6 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -11598,6 +11726,31 @@
}
}
},
+ "node_modules/react-native-callkeep": {
+ "version": "4.3.16",
+ "resolved": "https://registry.npmjs.org/react-native-callkeep/-/react-native-callkeep-4.3.16.tgz",
+ "integrity": "sha512-aIxn02T5zW4jNPyzRdFGTWv6xD3Vy/1AkBMB6iYvWZEHWnfmgNGF0hELqg03Vbc2BNUhfqpu17aIydos+5Hurg==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react-native": ">=0.40.0"
+ }
+ },
+ "node_modules/react-native-config": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/react-native-config/-/react-native-config-1.6.1.tgz",
+ "integrity": "sha512-HvKtxr6/Tq3iMdFx5REYZsjCtPi0RxQOMCs15+DqrUPTNFtWHuEuh+zw7fJp+dmuO79YMfdtlsPWIGTHtaXwjg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*",
+ "react-native-windows": ">=0.61"
+ },
+ "peerDependenciesMeta": {
+ "react-native-windows": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-native-gesture-handler": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.30.0.tgz",
@@ -11613,6 +11766,31 @@
"react-native": "*"
}
},
+ "node_modules/react-native-incall-manager": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/react-native-incall-manager/-/react-native-incall-manager-4.2.1.tgz",
+ "integrity": "sha512-HTdtzQ/AswUbuNhcL0gmyZLAXo8VqBO7SIh+BwbeeM1YMXXlR+Q2MvKxhD4yanjJPeyqMfuRhryCQCJhPlsdAw==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react-native": ">=0.40.0"
+ }
+ },
+ "node_modules/react-native-permissions": {
+ "version": "5.4.4",
+ "resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-5.4.4.tgz",
+ "integrity": "sha512-WB5lRCBGXETfuaUhem2vgOceb9+URCeyfKpLGFSwoOffLuyJCA6+NTR3l1KLkrK4Ykxsig37z16/shUVufmt7A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=18.1.0",
+ "react-native": ">=0.70.0",
+ "react-native-windows": ">=0.70.0"
+ },
+ "peerDependenciesMeta": {
+ "react-native-windows": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-native-safe-area-context": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.7.0.tgz",
@@ -11652,6 +11830,76 @@
"react-native": "*"
}
},
+ "node_modules/react-native-url-polyfill": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/react-native-url-polyfill/-/react-native-url-polyfill-3.0.0.tgz",
+ "integrity": "sha512-aA5CiuUCUb/lbrliVCJ6lZ17/RpNJzvTO/C7gC/YmDQhTUoRD5q5HlJfwLWcxz4VgAhHwXKzhxH+wUN24tAdqg==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url-without-unicode": "8.0.0-3"
+ },
+ "peerDependencies": {
+ "react-native": "*"
+ }
+ },
+ "node_modules/react-native-voip-push-notification": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/react-native-voip-push-notification/-/react-native-voip-push-notification-3.3.3.tgz",
+ "integrity": "sha512-cyWuI9//T1IQIq4RPq0QQe0NuEwIpnE0L98H2sUH4MjFsNMD/yNE4EJzEZN4cIwfPMZaASa0gQw6B1a7VwnkMA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react-native": ">=0.60.0"
+ }
+ },
+ "node_modules/react-native-webrtc": {
+ "version": "124.0.7",
+ "resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-124.0.7.tgz",
+ "integrity": "sha512-gnXPdbUS8IkKHq9WNaWptW/yy5s6nMyI6cNn90LXdobPVCgYSk6NA2uUGdT4c4J14BRgaFA95F+cR28tUPkMVA==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "1.5.1",
+ "debug": "4.3.4",
+ "event-target-shim": "6.0.2"
+ },
+ "peerDependencies": {
+ "react-native": ">=0.60.0"
+ }
+ },
+ "node_modules/react-native-webrtc/node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-native-webrtc/node_modules/event-target-shim": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-6.0.2.tgz",
+ "integrity": "sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ }
+ },
+ "node_modules/react-native-webrtc/node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "license": "MIT"
+ },
"node_modules/react-native/node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
@@ -13128,6 +13376,12 @@
"typescript": ">=4.8.4"
}
},
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/tsx": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
@@ -13735,12 +13989,35 @@
"defaults": "^1.0.3"
}
},
+ "node_modules/webidl-conversions": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
+ "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/whatwg-fetch": {
"version": "3.6.20",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
"license": "MIT"
},
+ "node_modules/whatwg-url-without-unicode": {
+ "version": "8.0.0-3",
+ "resolved": "https://registry.npmjs.org/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz",
+ "integrity": "sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.4.3",
+ "punycode": "^2.1.1",
+ "webidl-conversions": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -14041,6 +14318,7 @@
"version": "0.0.1",
"dependencies": {
"@farscry/shared": "*",
+ "jose": "^6.1.3",
"uuid": "^11.1.0",
"ws": "^8.18.0"
},
diff --git a/packages/signaling/.eslintrc.json b/packages/signaling/.eslintrc.json
new file mode 100644
index 0000000..e491050
--- /dev/null
+++ b/packages/signaling/.eslintrc.json
@@ -0,0 +1,13 @@
+{
+ "parser": "@typescript-eslint/parser",
+ "plugins": ["@typescript-eslint"],
+ "extends": [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/recommended"
+ ],
+ "parserOptions": {
+ "ecmaVersion": 2022,
+ "sourceType": "module"
+ },
+ "root": true
+}
diff --git a/packages/signaling/package.json b/packages/signaling/package.json
index 6d31508..034816b 100644
--- a/packages/signaling/package.json
+++ b/packages/signaling/package.json
@@ -11,22 +11,23 @@
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
- "lint": "eslint src/"
+ "lint": "eslint src"
},
"dependencies": {
- "ws": "^8.18.0",
+ "@farscry/shared": "*",
+ "jose": "^6.1.3",
"uuid": "^11.1.0",
- "@farscry/shared": "*"
+ "ws": "^8.18.0"
},
"devDependencies": {
- "@types/ws": "^8.18.0",
"@types/uuid": "^10.0.0",
+ "@types/ws": "^8.18.0",
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
+ "@typescript-eslint/parser": "^8.0.0",
+ "eslint": "^8.19.0",
"tsx": "^4.19.0",
"typescript": "^5.8.3",
- "vitest": "^3.0.0",
- "eslint": "^8.19.0",
- "@typescript-eslint/eslint-plugin": "^8.0.0",
- "@typescript-eslint/parser": "^8.0.0"
+ "vitest": "^3.0.0"
},
"engines": {
"node": ">= 22.11.0"
diff --git a/packages/signaling/src/__tests__/server.test.ts b/packages/signaling/src/__tests__/server.test.ts
index 82cd69b..600d32d 100644
--- a/packages/signaling/src/__tests__/server.test.ts
+++ b/packages/signaling/src/__tests__/server.test.ts
@@ -1,6 +1,25 @@
-import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { createServer, type Server } from 'node:http';
import { WebSocketServer, WebSocket } from 'ws';
+
+// Mock auth module so tests don't need a real JWT secret.
+// vi.mock is hoisted before imports by vitest, so the static import below
+// will receive the mocked version.
+vi.mock('../auth.js', () => ({
+ validateToken: vi.fn(async (token: string) => {
+ if (token === 'not-a-jwt') {
+ return { valid: false, error: 'invalid token' };
+ }
+ try {
+ const parts = token.split('.');
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
+ return { valid: true, userId: payload.sub };
+ } catch {
+ return { valid: false, error: 'invalid token' };
+ }
+ }),
+}));
+
import { SignalingServer } from '../server.js';
function makeToken(sub: string, exp?: number): string {
diff --git a/packages/signaling/src/auth.ts b/packages/signaling/src/auth.ts
index 933bfcf..02d7efb 100644
--- a/packages/signaling/src/auth.ts
+++ b/packages/signaling/src/auth.ts
@@ -1,4 +1,5 @@
-import { logger } from './logger.js';
+import {jwtVerify} from 'jose';
+import {logger} from './logger.js';
export interface AuthResult {
valid: boolean;
@@ -6,57 +7,37 @@ export interface AuthResult {
error?: string;
}
-// TODO: Set from environment variable (Supabase project settings -> JWT Secret)
const SUPABASE_JWT_SECRET = process.env.SUPABASE_JWT_SECRET ?? '';
-/**
- * Validates a Supabase JWT access token.
- *
- * For production, replace this with proper verification using the `jose` library:
- * import { jwtVerify } from 'jose';
- * const secret = new TextEncoder().encode(SUPABASE_JWT_SECRET);
- * const { payload } = await jwtVerify(token, secret);
- *
- * Current implementation: decode-only with basic structural + expiry checks.
- * This is NOT secure for production — install jose and verify the signature.
- */
-export function validateToken(token: string): AuthResult {
- try {
- if (!token || typeof token !== 'string') {
- return { valid: false, error: 'missing token' };
+let secretKey: Uint8Array | null = null;
+function getSecret(): Uint8Array {
+ if (!secretKey) {
+ if (!SUPABASE_JWT_SECRET) {
+ throw new Error('SUPABASE_JWT_SECRET environment variable is not set');
}
+ secretKey = new TextEncoder().encode(SUPABASE_JWT_SECRET);
+ }
+ return secretKey;
+}
- const parts = token.split('.');
- if (parts.length !== 3) {
- return { valid: false, error: 'malformed token' };
+export async function validateToken(token: string): Promise {
+ try {
+ if (!token || typeof token !== 'string') {
+ return {valid: false, error: 'missing token'};
}
- const payload = JSON.parse(
- Buffer.from(parts[1], 'base64url').toString('utf-8')
- );
+ const {payload} = await jwtVerify(token, getSecret(), {
+ audience: 'authenticated',
+ });
if (!payload.sub || typeof payload.sub !== 'string') {
- return { valid: false, error: 'missing subject claim' };
- }
-
- // Verify token hasn't expired
- if (payload.exp && payload.exp * 1000 < Date.now()) {
- return { valid: false, error: 'token expired' };
- }
-
- // Verify this is a Supabase-issued token (aud claim)
- if (payload.aud && payload.aud !== 'authenticated') {
- return { valid: false, error: 'invalid audience' };
- }
-
- // In production, verify signature against SUPABASE_JWT_SECRET using jose
- if (!SUPABASE_JWT_SECRET) {
- logger.warn('SUPABASE_JWT_SECRET not set — skipping signature verification');
+ return {valid: false, error: 'missing subject claim'};
}
- return { valid: true, userId: payload.sub };
- } catch {
- logger.warn('Token validation failed');
- return { valid: false, error: 'invalid token' };
+ return {valid: true, userId: payload.sub};
+ } catch (err) {
+ const message = err instanceof Error ? err.message : 'invalid token';
+ logger.warn(`Token validation failed: ${message}`);
+ return {valid: false, error: message};
}
}
diff --git a/packages/signaling/src/server.ts b/packages/signaling/src/server.ts
index 3a2b782..a7257b0 100644
--- a/packages/signaling/src/server.ts
+++ b/packages/signaling/src/server.ts
@@ -77,7 +77,7 @@ export class SignalingServer {
private handleMessage(conn: ClientConnection, message: ClientMessage): void {
if (message.type === 'register') {
- this.handleRegister(conn, message);
+ void this.handleRegister(conn, message);
return;
}
@@ -113,8 +113,8 @@ export class SignalingServer {
}
}
- private handleRegister(conn: ClientConnection, msg: RegisterMessage): void {
- const authResult = validateToken(msg.token);
+ private async handleRegister(conn: ClientConnection, msg: RegisterMessage): Promise {
+ const authResult = await validateToken(msg.token);
if (!authResult.valid) {
conn.sendError('auth_failed', authResult.error ?? 'Invalid token');
conn.close(WS_CLOSE_AUTH_FAILED, 'auth failed');
diff --git a/supabase/migrations/001_initial_schema.sql b/supabase/migrations/001_initial_schema.sql
new file mode 100644
index 0000000..00856d8
--- /dev/null
+++ b/supabase/migrations/001_initial_schema.sql
@@ -0,0 +1,154 @@
+-- Farscry initial schema
+-- Run this in the Supabase SQL Editor (Dashboard > SQL Editor > New query)
+
+-- Enable the trigram extension for fuzzy display_name search (in extensions schema)
+create schema if not exists extensions;
+create extension if not exists pg_trgm schema extensions;
+
+-- ============================================
+-- TABLES
+-- ============================================
+
+-- User profiles (synced from auth.users)
+create table if not exists public.users (
+ id uuid primary key references auth.users(id) on delete cascade,
+ display_name text not null,
+ avatar_url text,
+ created_at timestamptz not null default now(),
+ updated_at timestamptz not null default now()
+);
+
+-- Contacts
+create table if not exists public.contacts (
+ user_id uuid not null references public.users(id) on delete cascade,
+ contact_user_id uuid not null references public.users(id) on delete cascade,
+ is_favorite boolean not null default false,
+ added_at timestamptz not null default now(),
+ primary key (user_id, contact_user_id),
+ constraint no_self_contact check (user_id != contact_user_id)
+);
+
+-- Push notification tokens
+create table if not exists public.push_tokens (
+ user_id uuid not null references public.users(id) on delete cascade,
+ token text not null,
+ platform text not null check (platform in ('ios', 'android')),
+ voip_token text,
+ updated_at timestamptz not null default now(),
+ primary key (user_id, platform)
+);
+
+-- ============================================
+-- INDEXES
+-- ============================================
+
+create index if not exists idx_contacts_contact_user on public.contacts(contact_user_id);
+create index if not exists idx_contacts_user on public.contacts(user_id);
+create index if not exists idx_users_display_name on public.users using gin (display_name extensions.gin_trgm_ops);
+
+-- ============================================
+-- TRIGGER: Auto-create user profile on signup
+-- ============================================
+
+create or replace function public.handle_new_user()
+returns trigger
+language plpgsql
+security definer set search_path = ''
+as $$
+begin
+ insert into public.users (id, display_name)
+ values (
+ new.id,
+ coalesce(
+ new.raw_user_meta_data ->> 'display_name',
+ split_part(new.email, '@', 1)
+ )
+ );
+ return new;
+end;
+$$;
+
+-- Drop trigger if it exists, then create
+drop trigger if exists on_auth_user_created on auth.users;
+create trigger on_auth_user_created
+ after insert on auth.users
+ for each row execute function public.handle_new_user();
+
+-- ============================================
+-- TRIGGER: Auto-update updated_at
+-- ============================================
+
+create or replace function public.update_updated_at()
+returns trigger
+language plpgsql
+security invoker set search_path = ''
+as $$
+begin
+ new.updated_at = now();
+ return new;
+end;
+$$;
+
+drop trigger if exists users_updated_at on public.users;
+create trigger users_updated_at
+ before update on public.users
+ for each row execute function public.update_updated_at();
+
+-- ============================================
+-- ROW LEVEL SECURITY
+-- ============================================
+
+alter table public.users enable row level security;
+alter table public.contacts enable row level security;
+alter table public.push_tokens enable row level security;
+
+-- Users: anyone authenticated can read profiles (for search/contacts)
+create policy "Users can read all profiles"
+ on public.users for select
+ to authenticated
+ using (true);
+
+-- Users: can only update own profile
+create policy "Users can update own profile"
+ on public.users for update
+ to authenticated
+ using (auth.uid() = id)
+ with check (auth.uid() = id);
+
+-- Users: can delete own profile
+create policy "Users can delete own profile"
+ on public.users for delete
+ to authenticated
+ using (auth.uid() = id);
+
+-- Contacts: can read own contacts
+create policy "Users can read own contacts"
+ on public.contacts for select
+ to authenticated
+ using (auth.uid() = user_id);
+
+-- Contacts: can insert own contacts
+create policy "Users can add contacts"
+ on public.contacts for insert
+ to authenticated
+ with check (auth.uid() = user_id);
+
+-- Contacts: can update own contacts (favorite toggle)
+create policy "Users can update own contacts"
+ on public.contacts for update
+ to authenticated
+ using (auth.uid() = user_id)
+ with check (auth.uid() = user_id);
+
+-- Contacts: can delete own contacts
+create policy "Users can remove contacts"
+ on public.contacts for delete
+ to authenticated
+ using (auth.uid() = user_id);
+
+-- Push tokens: full access to own tokens only
+create policy "Users can manage own push tokens"
+ on public.push_tokens for all
+ to authenticated
+ using (auth.uid() = user_id)
+ with check (auth.uid() = user_id);