From 53538a750373dcfc5848e753f430e79f56c55ea4 Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 13:28:47 -0700 Subject: [PATCH 01/32] Add Supabase setup design doc for family testing phase --- .../plans/2026-03-01-supabase-setup-design.md | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 docs/plans/2026-03-01-supabase-setup-design.md 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) From cd00b6da5a0543d5e0edec72b1a655575df577bd Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 13:33:22 -0700 Subject: [PATCH 02/32] Add Supabase setup implementation plan --- docs/plans/2026-03-01-supabase-setup.md | 1380 +++++++++++++++++++++++ 1 file changed, 1380 insertions(+) create mode 100644 docs/plans/2026-03-01-supabase-setup.md 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" +``` From 32ab62d7bc384fc674060197c05ba51aabab8f2f Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 13:49:41 -0700 Subject: [PATCH 03/32] Add Supabase JS SDK, AsyncStorage, and react-native-config --- apps/mobile/package.json | 3 + package-lock.json | 140 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 142 insertions(+), 1 deletion(-) diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 417b194..424e1f5 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -10,12 +10,15 @@ "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-config": "^1.6.1", "react-native-gesture-handler": "^2.30.0", "react-native-safe-area-context": "^5.5.2", "react-native-screens": "^4.24.0", diff --git a/package-lock.json b/package-lock.json index cb5ea72..479f2b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,12 +19,15 @@ "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-config": "^1.6.1", "react-native-gesture-handler": "^2.30.0", "react-native-safe-area-context": "^5.5.2", "react-native-screens": "^4.24.0", @@ -3179,6 +3182,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 +4322,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 +4527,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 +4570,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": "*" @@ -8273,6 +8374,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,6 +8396,12 @@ "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", @@ -11598,6 +11714,22 @@ } } }, + "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", @@ -13128,6 +13260,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", From 246d829b9f45c0f1d711152867d67a17d06631c0 Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 13:54:25 -0700 Subject: [PATCH 04/32] Add .env.example for Supabase config --- apps/mobile/.env.example | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 apps/mobile/.env.example diff --git a/apps/mobile/.env.example b/apps/mobile/.env.example new file mode 100644 index 0000000..191115e --- /dev/null +++ b/apps/mobile/.env.example @@ -0,0 +1,2 @@ +SUPABASE_URL=https://your-project.supabase.co +SUPABASE_ANON_KEY=your-anon-key From f1a5247c64ec4518620b006e51eef34badae1949 Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 13:54:28 -0700 Subject: [PATCH 05/32] Replace Supabase stub client with real SDK --- apps/mobile/src/services/supabase/client.ts | 132 ++++---------------- 1 file changed, 26 insertions(+), 106 deletions(-) diff --git a/apps/mobile/src/services/supabase/client.ts b/apps/mobile/src/services/supabase/client.ts index 2c3cdcf..d0f86a8 100644 --- a/apps/mobile/src/services/supabase/client.ts +++ b/apps/mobile/src/services/supabase/client.ts @@ -1,111 +1,31 @@ -// Placeholder Supabase client — replace URL and anon key with real values -// after installing @supabase/supabase-js. +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 SUPABASE_URL = 'https://your-project.supabase.co'; -const SUPABASE_ANON_KEY = 'your-anon-key'; +const supabaseUrl = Config.SUPABASE_URL ?? ''; +const supabaseAnonKey = Config.SUPABASE_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.', +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(); + } +}); From bc18a8df47b31ca851ddd1c13c1673c5a2ee54bb Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 13:54:33 -0700 Subject: [PATCH 06/32] Update auth services to use real Supabase SDK --- apps/mobile/src/services/auth/AuthService.ts | 84 +++++-------------- .../src/services/auth/SessionManager.ts | 40 +-------- 2 files changed, 26 insertions(+), 98 deletions(-) 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; }, }; From a9c1e3f45e5ece339e67ba56113a40fd4cce580f Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 13:54:37 -0700 Subject: [PATCH 07/32] Update UserService and ContactsService for real Supabase SDK --- .../src/services/user/ContactsService.ts | 13 ++++++------- apps/mobile/src/services/user/UserService.ts | 19 +++++++++---------- 2 files changed, 15 insertions(+), 17 deletions(-) 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; }, }; From a5e0bf23b58f5fc728d815a97e1e68acffe65769 Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 13:54:41 -0700 Subject: [PATCH 08/32] Add initial database schema migration for users, contacts, push_tokens --- supabase/migrations/001_initial_schema.sql | 152 +++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 supabase/migrations/001_initial_schema.sql diff --git a/supabase/migrations/001_initial_schema.sql b/supabase/migrations/001_initial_schema.sql new file mode 100644 index 0000000..02f22bc --- /dev/null +++ b/supabase/migrations/001_initial_schema.sql @@ -0,0 +1,152 @@ +-- Farscry initial schema +-- Run this in the Supabase SQL Editor (Dashboard > SQL Editor > New query) + +-- Enable the trigram extension for fuzzy display_name search +create extension if not exists pg_trgm; + +-- ============================================ +-- 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); + +-- ============================================ +-- 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); From e33bda64cfd510057562bdc5b7918dd336a6fdba Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 13:54:45 -0700 Subject: [PATCH 09/32] Update authStore to use real Supabase session management --- apps/mobile/src/stores/authStore.ts | 71 +++++++++++------------------ 1 file changed, 26 insertions(+), 45 deletions(-) diff --git a/apps/mobile/src/stores/authStore.ts b/apps/mobile/src/stores/authStore.ts index 8dba260..1f41500 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( @@ -111,11 +92,11 @@ export function AuthProvider({children}: {children: React.ReactNode}) { if (result.session) { 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) { @@ -131,7 +112,7 @@ export function AuthProvider({children}: {children: React.ReactNode}) { const result = await AuthService.signIn(email, password); dispatch({ type: 'SIGNED_IN', - user: result.user!, + user: result.user, session: result.session!, }); } catch (e: unknown) { From 63fcdfc82947c9563127a23003dfd873ff926f43 Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 13:54:51 -0700 Subject: [PATCH 10/32] Wire AuthProvider, ContactsProvider, and auth-gated navigation --- apps/mobile/App.tsx | 18 ++++++++++++------ apps/mobile/src/navigation/RootNavigator.tsx | 17 +++++++++++++---- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/apps/mobile/App.tsx b/apps/mobile/App.tsx index 5da1298..35f94e0 100644 --- a/apps/mobile/App.tsx +++ b/apps/mobile/App.tsx @@ -3,6 +3,8 @@ 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'; @@ -27,12 +29,16 @@ const navTheme = { export default function App() { return ( - - - - - - + + + + + + + + + + ); } 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 ? ( <> Date: Sun, 1 Mar 2026 13:54:55 -0700 Subject: [PATCH 11/32] Wire login and signup screens to auth service --- apps/mobile/src/screens/auth/LoginScreen.tsx | 28 +++++++++++++++---- apps/mobile/src/screens/auth/SignupScreen.tsx | 28 +++++++++++++++---- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/apps/mobile/src/screens/auth/LoginScreen.tsx b/apps/mobile/src/screens/auth/LoginScreen.tsx index 7920fd8..02959fa 100644 --- a/apps/mobile/src/screens/auth/LoginScreen.tsx +++ b/apps/mobile/src/screens/auth/LoginScreen.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'; @@ -18,9 +20,10 @@ export function LoginScreen({navigation}: AuthScreenProps<'Login'>) { 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, From b54ece3a1264de79dea9f790e24a33c40b80ba65 Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 13:54:59 -0700 Subject: [PATCH 12/32] Wire contacts, favorites, and search screens to real Supabase data --- .../src/screens/contacts/AddContactScreen.tsx | 60 +++++++++---------- .../screens/contacts/ContactDetailScreen.tsx | 19 +++--- .../src/screens/main/ContactsScreen.tsx | 59 +++++++----------- .../src/screens/main/FavoritesScreen.tsx | 41 +++++-------- 4 files changed, 72 insertions(+), 107 deletions(-) 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 {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}); @@ -44,8 +48,8 @@ export function ContactDetailScreen({ { text: 'Remove', style: 'destructive', - onPress: () => { - // TODO: wire up contact service + onPress: async () => { + await removeContact(contactId); navigation.goBack(); }, }, @@ -63,14 +67,13 @@ export function ContactDetailScreen({ {name} - @{name.toLowerCase().replace(/\s/g, '')} setIsFavorite(f => !f)} + onPress={() => toggleFavorite(contactId)} activeOpacity={0.7}> @@ -107,10 +110,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 ( @@ -61,10 +45,11 @@ type Section = { data: Contact[]; }; -function buildSections(contacts: Contact[]): Section[] { +function buildSections(items: Contact[]): Section[] { const map = new Map(); - for (const c of contacts) { - const letter = c.name.charAt(0).toUpperCase(); + 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); @@ -77,16 +62,18 @@ function buildSections(contacts: Contact[]): Section[] { export function ContactsScreen({navigation}: MainTabScreenProps<'Contacts'>) { const insets = useSafeAreaInsets(); const [search, setSearch] = useState(''); + const {contacts, fetchContacts} = useContacts(); + + useEffect(() => { fetchContacts(); }, [fetchContacts]); const filtered = useMemo(() => { - if (!search.trim()) { - return MOCK_CONTACTS; - } + if (!search.trim()) return contacts; const q = search.toLowerCase(); - return MOCK_CONTACTS.filter( - c => c.name.toLowerCase().includes(q) || c.username.toLowerCase().includes(q), - ); - }, [search]); + return contacts.filter(c => { + const name = c.profile?.display_name ?? ''; + return name.toLowerCase().includes(q); + }); + }, [search, contacts]); const sections = useMemo(() => buildSections(filtered), [filtered]); @@ -117,7 +104,7 @@ export function ContactsScreen({navigation}: MainTabScreenProps<'Contacts'>) { ) : ( item.id} + keyExtractor={item => item.contact_user_id} contentContainerStyle={{paddingBottom: insets.bottom + spacing.base}} stickySectionHeadersEnabled renderSectionHeader={({section}) => ( @@ -127,19 +114,17 @@ export function ContactsScreen({navigation}: MainTabScreenProps<'Contacts'>) { )} renderItem={({item}) => ( navigation.navigate('ContactDetail', { - contactId: item.id, - name: item.name, + contactId: item.contact_user_id, + name: item.profile?.display_name ?? '?', }) } onCall={() => navigation.navigate('OutgoingCall', { - contactId: item.id, - contactName: item.name, + contactId: item.contact_user_id, + contactName: item.profile?.display_name ?? '?', }) } /> diff --git a/apps/mobile/src/screens/main/FavoritesScreen.tsx b/apps/mobile/src/screens/main/FavoritesScreen.tsx index dfe69c0..223daa8 100644 --- a/apps/mobile/src/screens/main/FavoritesScreen.tsx +++ b/apps/mobile/src/screens/main/FavoritesScreen.tsx @@ -1,27 +1,14 @@ -import React from 'react'; +import React, {useEffect} from 'react'; import {View, FlatList, StyleSheet} from 'react-native'; import Svg, {Path} from 'react-native-svg'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; import {ContactCard} from '../../components/ContactCard'; import {EmptyState} from '../../components/EmptyState'; +import {useContacts} from '../../stores/contactsStore'; import {colors} from '../../theme/colors'; import {spacing} from '../../theme/spacing'; import type {MainTabScreenProps} from '../../navigation/types'; -type FavoriteContact = { - id: string; - name: string; - online: boolean; -}; - -// placeholder data — will be replaced by real contacts -const MOCK_FAVORITES: FavoriteContact[] = [ - {id: '1', name: 'Alice Chen', online: true}, - {id: '2', name: 'Marcus Wright', online: false}, - {id: '3', name: 'Priya Sharma', online: true}, - {id: '4', name: 'James Ko', online: false}, -]; - function StarIcon() { return ( @@ -38,15 +25,11 @@ function StarIcon() { export function FavoritesScreen({navigation}: MainTabScreenProps<'Favorites'>) { const insets = useSafeAreaInsets(); + const {favorites, fetchContacts} = useContacts(); - function handleCallContact(contact: FavoriteContact) { - navigation.navigate('OutgoingCall', { - contactId: contact.id, - contactName: contact.name, - }); - } + useEffect(() => { fetchContacts(); }, [fetchContacts]); - if (MOCK_FAVORITES.length === 0) { + if (favorites.length === 0) { return ( } @@ -59,19 +42,23 @@ export function FavoritesScreen({navigation}: MainTabScreenProps<'Favorites'>) { return ( item.id} + keyExtractor={item => item.contact_user_id} renderItem={({item}) => ( handleCallContact(item)} + name={item.profile?.display_name ?? '?'} + onPress={() => + navigation.navigate('OutgoingCall', { + contactId: item.contact_user_id, + contactName: item.profile?.display_name ?? '?', + }) + } /> )} /> From 8d0cf77707b0eecf0403fbb6ad6428c2b21e7eb6 Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 13:55:03 -0700 Subject: [PATCH 13/32] Add real JWT verification to signaling server using jose --- package-lock.json | 10 +++++ packages/signaling/package.json | 15 +++---- packages/signaling/src/auth.ts | 67 ++++++++++++-------------------- packages/signaling/src/server.ts | 6 +-- 4 files changed, 45 insertions(+), 53 deletions(-) diff --git a/package-lock.json b/package-lock.json index 479f2b2..c86f3ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9737,6 +9737,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", @@ -14179,6 +14188,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/package.json b/packages/signaling/package.json index 6d31508..18314ad 100644 --- a/packages/signaling/package.json +++ b/packages/signaling/package.json @@ -14,19 +14,20 @@ "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/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'); From c5c48adc5f6f54e2bc08b6fd4c338ed1e68b37c7 Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 13:55:52 -0700 Subject: [PATCH 14/32] Fix null checks in authStore for signUp/signIn dispatch --- apps/mobile/src/stores/authStore.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/mobile/src/stores/authStore.ts b/apps/mobile/src/stores/authStore.ts index 1f41500..f37ce1a 100644 --- a/apps/mobile/src/stores/authStore.ts +++ b/apps/mobile/src/stores/authStore.ts @@ -89,7 +89,7 @@ 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, @@ -110,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!, + session: result.session, }); } catch (e: unknown) { dispatch({type: 'ERROR', error: e instanceof Error ? e.message : 'Sign in failed'}); From 9f43d5a7e700e02ec95d0316a11980636a4e351f Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 13:56:26 -0700 Subject: [PATCH 15/32] Fix Supabase security advisories: search_path and pg_trgm schema --- supabase/migrations/001_initial_schema.sql | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/supabase/migrations/001_initial_schema.sql b/supabase/migrations/001_initial_schema.sql index 02f22bc..00856d8 100644 --- a/supabase/migrations/001_initial_schema.sql +++ b/supabase/migrations/001_initial_schema.sql @@ -1,8 +1,9 @@ -- Farscry initial schema -- Run this in the Supabase SQL Editor (Dashboard > SQL Editor > New query) --- Enable the trigram extension for fuzzy display_name search -create extension if not exists pg_trgm; +-- 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 @@ -43,7 +44,7 @@ create table if not exists public.push_tokens ( 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); +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 @@ -80,6 +81,7 @@ create trigger on_auth_user_created create or replace function public.update_updated_at() returns trigger language plpgsql +security invoker set search_path = '' as $$ begin new.updated_at = now(); From a327739389483c0a899e4e0b0c04d5902a2667df Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 14:03:48 -0700 Subject: [PATCH 16/32] Add react-native-url-polyfill for Supabase Realtime compatibility Hermes URL implementation has read-only protocol property which the Supabase Realtime client tries to mutate. --- apps/mobile/package.json | 3 +- apps/mobile/src/services/supabase/client.ts | 1 + package-lock.json | 41 +++++++++++++++++++-- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 424e1f5..5a6620c 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -22,7 +22,8 @@ "react-native-gesture-handler": "^2.30.0", "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" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/apps/mobile/src/services/supabase/client.ts b/apps/mobile/src/services/supabase/client.ts index d0f86a8..7b49f94 100644 --- a/apps/mobile/src/services/supabase/client.ts +++ b/apps/mobile/src/services/supabase/client.ts @@ -1,3 +1,4 @@ +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'; diff --git a/package-lock.json b/package-lock.json index c86f3ed..34cd171 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,8 @@ "react-native-gesture-handler": "^2.30.0", "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" }, "devDependencies": { "@babel/core": "^7.25.2", @@ -5711,7 +5712,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", @@ -8406,7 +8406,6 @@ "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", @@ -11495,7 +11494,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" @@ -11793,6 +11791,18 @@ "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/node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", @@ -13882,12 +13892,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", From 558b0dcc36f478413c1a332e5cdc31493f45de0f Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 14:06:26 -0700 Subject: [PATCH 17/32] Add call infrastructure design doc for family testing phase --- .../2026-03-01-call-infrastructure-design.md | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 docs/plans/2026-03-01-call-infrastructure-design.md 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 From a68986a8992bc5f5b9e299f2de56a6b74de79fa1 Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 14:13:06 -0700 Subject: [PATCH 18/32] Add call infrastructure implementation plan --- docs/plans/2026-03-01-call-infrastructure.md | 572 +++++++++++++++++++ 1 file changed, 572 insertions(+) create mode 100644 docs/plans/2026-03-01-call-infrastructure.md 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" +``` From c809de7c6bf62825d0333b3870488f99f69dc18b Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 14:21:02 -0700 Subject: [PATCH 19/32] Add native call dependencies (WebRTC, CallKeep, InCallManager, permissions, VoIP push) --- apps/mobile/ios/Podfile.lock | 2402 ++++++++++++++++++++++++++++++++++ apps/mobile/package.json | 7 +- package-lock.json | 99 +- 3 files changed, 2506 insertions(+), 2 deletions(-) create mode 100644 apps/mobile/ios/Podfile.lock 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/package.json b/apps/mobile/package.json index 5a6620c..2d09824 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -18,12 +18,17 @@ "@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-url-polyfill": "^3.0.0" + "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/package-lock.json b/package-lock.json index 34cd171..9b064e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,12 +27,17 @@ "@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-url-polyfill": "^3.0.0" + "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", @@ -11721,6 +11726,15 @@ } } }, + "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", @@ -11752,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", @@ -11803,6 +11842,64 @@ "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", From 4831f96889e3bf64ebddaa33e010ae7f571edd2a Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 14:21:22 -0700 Subject: [PATCH 20/32] Add camera/mic permissions and VoIP background modes to Info.plist --- apps/mobile/ios/Farscry/Info.plist | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) 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 From 1b5c5f1f9a410d43ceecb5fd78d6fd94cb3c9a2e Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 14:21:43 -0700 Subject: [PATCH 21/32] Add SIGNALING_URL to environment config --- apps/mobile/.env.example | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/mobile/.env.example b/apps/mobile/.env.example index 191115e..67d78cf 100644 --- a/apps/mobile/.env.example +++ b/apps/mobile/.env.example @@ -1,2 +1,3 @@ SUPABASE_URL=https://your-project.supabase.co SUPABASE_ANON_KEY=your-anon-key +SIGNALING_URL=ws://localhost:8080 From 6c452eb95b59702d0efb258d53bd7033328f969a Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 14:23:50 -0700 Subject: [PATCH 22/32] Add CallProvider context for signaling and call management --- apps/mobile/src/stores/callStore.ts | 125 ++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 apps/mobile/src/stores/callStore.ts 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; +} From 661a4cb5b5ac02e053d2968df66b88941efc94eb Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 14:23:52 -0700 Subject: [PATCH 23/32] Wire CallProvider into app component tree --- apps/mobile/App.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/mobile/App.tsx b/apps/mobile/App.tsx index 35f94e0..ac79334 100644 --- a/apps/mobile/App.tsx +++ b/apps/mobile/App.tsx @@ -5,6 +5,7 @@ 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'; @@ -34,7 +35,9 @@ export default function App() { - + + + From e185090a4009904e6cfe6d0d9fe0a5bddfb6ac18 Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 14:27:07 -0700 Subject: [PATCH 24/32] Wire call screens to real CallManager actions --- .../src/screens/call/ActiveCallScreen.tsx | 38 +++++++++++++++++-- .../src/screens/call/IncomingCallScreen.tsx | 4 ++ .../src/screens/call/OutgoingCallScreen.tsx | 13 ++++++- .../screens/contacts/ContactDetailScreen.tsx | 4 +- 4 files changed, 53 insertions(+), 6 deletions(-) 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/ContactDetailScreen.tsx b/apps/mobile/src/screens/contacts/ContactDetailScreen.tsx index 1ab22b1..476abee 100644 --- a/apps/mobile/src/screens/contacts/ContactDetailScreen.tsx +++ b/apps/mobile/src/screens/contacts/ContactDetailScreen.tsx @@ -4,6 +4,7 @@ import Svg, {Path} from 'react-native-svg'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; import {Avatar} from '../../components/Avatar'; import {CallButton} from '../../components/CallButton'; +import {useCallContext} from '../../stores/callStore'; import {useContacts} from '../../stores/contactsStore'; import {colors} from '../../theme/colors'; import {typography} from '../../theme/typography'; @@ -30,13 +31,14 @@ export function ContactDetailScreen({ }: RootStackScreenProps<'ContactDetail'>) { const insets = useSafeAreaInsets(); const {contactId, name} = route.params; + 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() { From 862a1973bd77f5ab5eae1b1768987aaf1764d757 Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 14:31:07 -0700 Subject: [PATCH 25/32] Fix native service type errors to match installed package APIs --- .../src/services/native/AudioRouteService.ts | 2 +- .../src/services/native/CallKeepService.ts | 35 ++--- .../src/services/native/PermissionsService.ts | 28 ++-- .../mobile/src/services/native/PushService.ts | 3 +- .../src/types/react-native-callkeep.d.ts | 143 ------------------ .../react-native-voip-push-notification.d.ts | 39 ----- 6 files changed, 37 insertions(+), 213 deletions(-) delete mode 100644 apps/mobile/src/types/react-native-callkeep.d.ts delete mode 100644 apps/mobile/src/types/react-native-voip-push-notification.d.ts 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/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; -} From dc221711fbde5e39334be50520924cb74c3b40fd Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 14:35:09 -0700 Subject: [PATCH 26/32] Wire real data into RecentsScreen and SettingsScreen, fix iOS build config - Replace mock call history with Zustand callHistoryStore in RecentsScreen - Add auth-aware profile editing and sign-out to SettingsScreen - Update metro.config.js for monorepo watch folders and module resolution - Update Xcode project with PrivacyInfo, Hermes, and new architecture flags - Add Gemfile.lock and Xcode workspace files --- apps/mobile/Gemfile.lock | 111 ++++++++++++++++++ .../ios/Farscry.xcodeproj/project.pbxproj | 23 +++- .../contents.xcworkspacedata | 10 ++ apps/mobile/metro.config.js | 13 +- .../mobile/src/screens/main/RecentsScreen.tsx | 109 ++++++++--------- .../src/screens/main/SettingsScreen.tsx | 86 ++++++++++++-- 6 files changed, 281 insertions(+), 71 deletions(-) create mode 100644 apps/mobile/Gemfile.lock create mode 100644 apps/mobile/ios/Farscry.xcworkspace/contents.xcworkspacedata diff --git a/apps/mobile/Gemfile.lock b/apps/mobile/Gemfile.lock new file mode 100644 index 0000000..4850ea4 --- /dev/null +++ b/apps/mobile/Gemfile.lock @@ -0,0 +1,111 @@ +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) + +RUBY VERSION + ruby 2.6.10p210 + +BUNDLED WITH + 1.17.2 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/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/src/screens/main/RecentsScreen.tsx b/apps/mobile/src/screens/main/RecentsScreen.tsx index 0b9ee0f..f0bcfeb 100644 --- a/apps/mobile/src/screens/main/RecentsScreen.tsx +++ b/apps/mobile/src/screens/main/RecentsScreen.tsx @@ -4,6 +4,7 @@ import Svg, {Path} from 'react-native-svg'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; import {Avatar} from '../../components/Avatar'; import {EmptyState} from '../../components/EmptyState'; +import {useRecents, type CallRecord} from '../../stores/callHistoryStore'; import {colors} from '../../theme/colors'; import {typography} from '../../theme/typography'; import {spacing} from '../../theme/spacing'; @@ -11,27 +12,6 @@ import type {MainTabScreenProps} from '../../navigation/types'; type CallDirection = 'outgoing' | 'incoming' | 'missed'; -type RecentCall = { - id: string; - contactName: string; - contactId: string; - direction: CallDirection; - duration: number; // seconds - timestamp: Date; -}; - -const now = new Date(); -const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); -const yesterday = new Date(today.getTime() - 86400000); - -const MOCK_RECENTS: RecentCall[] = [ - {id: '1', contactName: 'Alice Chen', contactId: '1', direction: 'outgoing', duration: 342, timestamp: new Date(today.getTime() + 3600000 * 14)}, - {id: '2', contactName: 'Marcus Wright', contactId: '7', direction: 'missed', duration: 0, timestamp: new Date(today.getTime() + 3600000 * 11)}, - {id: '3', contactName: 'Priya Sharma', contactId: '8', direction: 'incoming', duration: 1205, timestamp: new Date(yesterday.getTime() + 3600000 * 20)}, - {id: '4', contactName: 'James Ko', contactId: '6', direction: 'outgoing', duration: 67, timestamp: new Date(yesterday.getTime() + 3600000 * 15)}, - {id: '5', contactName: 'Alice Chen', contactId: '1', direction: 'incoming', duration: 890, timestamp: new Date(yesterday.getTime() + 3600000 * 10)}, -]; - function formatDuration(seconds: number): string { if (seconds === 0) { return 'No answer'; @@ -41,12 +21,16 @@ function formatDuration(seconds: number): string { return m > 0 ? `${m}m ${s}s` : `${s}s`; } -function formatTime(date: Date): string { - return date.toLocaleTimeString([], {hour: 'numeric', minute: '2-digit'}); +function formatTime(isoString: string): string { + return new Date(isoString).toLocaleTimeString([], {hour: 'numeric', minute: '2-digit'}); } -function getDateLabel(date: Date): string { +function getDateLabel(isoString: string): string { + const date = new Date(isoString); const d = new Date(date.getFullYear(), date.getMonth(), date.getDate()); + const now = new Date(); + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const yesterday = new Date(today.getTime() - 86400000); if (d.getTime() === today.getTime()) { return 'Today'; } @@ -98,15 +82,20 @@ function ClockIcon() { ); } +function getDirection(record: CallRecord): CallDirection { + if (record.status === 'missed') return 'missed'; + return record.direction; +} + type Section = { title: string; - data: RecentCall[]; + data: CallRecord[]; }; -function groupByDay(calls: RecentCall[]): Section[] { - const map = new Map(); +function groupByDay(calls: CallRecord[]): Section[] { + const map = new Map(); for (const call of calls) { - const label = getDateLabel(call.timestamp); + const label = getDateLabel(call.startedAt); const group = map.get(label) ?? []; group.push(call); map.set(label, group); @@ -116,9 +105,10 @@ function groupByDay(calls: RecentCall[]): Section[] { export function RecentsScreen({navigation}: MainTabScreenProps<'Recents'>) { const insets = useSafeAreaInsets(); - const sections = useMemo(() => groupByDay(MOCK_RECENTS), []); + const {recents, loading} = useRecents(); + const sections = useMemo(() => groupByDay(recents), [recents]); - if (MOCK_RECENTS.length === 0) { + if (!loading && recents.length === 0) { return ( } @@ -140,36 +130,39 @@ export function RecentsScreen({navigation}: MainTabScreenProps<'Recents'>) { {section.title} )} - renderItem={({item}) => ( - - navigation.navigate('ContactDetail', { - contactId: item.contactId, - name: item.contactName, - }) - } - activeOpacity={0.7}> - - - - {item.contactName} - - - - - {formatDuration(item.duration)} + renderItem={({item}) => { + const direction = getDirection(item); + return ( + + navigation.navigate('ContactDetail', { + contactId: item.contactId, + name: item.contactName, + }) + } + activeOpacity={0.7}> + + + + {item.contactName} + + + + {formatDuration(item.duration)} + + - - {formatTime(item.timestamp)} - - )} + {formatTime(item.startedAt)} + + ); + }} /> ); 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, + }, }); From 30ef2f65b088b4a4631d319285281e36e0b68617 Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 15:30:14 -0700 Subject: [PATCH 27/32] Fix CI: build shared package before typechecking signaling The shared package exports types from dist/, which doesn't exist in a fresh CI checkout. Build it first so signaling can resolve the module. --- .github/workflows/ci.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0639e1..7c9dcc4 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 From 78ba98e4cd703627c715d4fdfdffb40270b02329 Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 15:32:16 -0700 Subject: [PATCH 28/32] Fix signaling lint: add ESLint config and fix src pattern ESLint 8 requires a config file and the trailing slash on `src/` caused "no files found" in CI. Add .eslintrc.json and use `src` without slash. --- packages/signaling/.eslintrc.json | 13 +++++++++++++ packages/signaling/package.json | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 packages/signaling/.eslintrc.json 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 18314ad..034816b 100644 --- a/packages/signaling/package.json +++ b/packages/signaling/package.json @@ -11,7 +11,7 @@ "test": "vitest run", "test:watch": "vitest", "typecheck": "tsc --noEmit", - "lint": "eslint src/" + "lint": "eslint src" }, "dependencies": { "@farscry/shared": "*", From f4f570cefc4432f49d7b3d9982dd8bd849c56f1b Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 15:36:01 -0700 Subject: [PATCH 29/32] Fix CI: Android monorepo paths, iOS bundler compat, and test auth mock - Android: Fix gradle plugin path to resolve from monorepo root node_modules instead of local (hoisted by npm workspaces) - iOS: Update Gemfile.lock bundler version from 1.17.2 to 2.5.6 (1.17.2 incompatible with Ruby 3.2, untaint method removed) - Tests: Mock validateToken in server tests so they don't require a real SUPABASE_JWT_SECRET (tests predate real JWT verification) --- apps/mobile/Gemfile.lock | 5 +--- apps/mobile/android/settings.gradle | 4 ++-- .../signaling/src/__tests__/server.test.ts | 23 +++++++++++++++++-- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/apps/mobile/Gemfile.lock b/apps/mobile/Gemfile.lock index 4850ea4..4ab371b 100644 --- a/apps/mobile/Gemfile.lock +++ b/apps/mobile/Gemfile.lock @@ -104,8 +104,5 @@ DEPENDENCIES mutex_m xcodeproj (< 1.26.0) -RUBY VERSION - ruby 2.6.10p210 - BUNDLED WITH - 1.17.2 + 2.5.6 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/packages/signaling/src/__tests__/server.test.ts b/packages/signaling/src/__tests__/server.test.ts index 82cd69b..1b1d559 100644 --- a/packages/signaling/src/__tests__/server.test.ts +++ b/packages/signaling/src/__tests__/server.test.ts @@ -1,7 +1,26 @@ -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'; -import { SignalingServer } from '../server.js'; + +// Mock auth module so tests don't need a real JWT secret +vi.mock('../auth.js', () => ({ + validateToken: vi.fn(async (token: string) => { + if (token === 'not-a-jwt') { + return { valid: false, error: 'invalid token' }; + } + // Decode the payload from our fake tokens + 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 after mock setup +const { SignalingServer } = await import('../server.js'); function makeToken(sub: string, exp?: number): string { const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url'); From 677028ccd13a031dbf0ded44b38a2ede5551fa7d Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 15:37:53 -0700 Subject: [PATCH 30/32] Fix test mock to use static import instead of top-level await Top-level await is not allowed in CommonJS modules under NodeNext. vi.mock is hoisted before imports by vitest, so a static import works. --- packages/signaling/src/__tests__/server.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/signaling/src/__tests__/server.test.ts b/packages/signaling/src/__tests__/server.test.ts index 1b1d559..600d32d 100644 --- a/packages/signaling/src/__tests__/server.test.ts +++ b/packages/signaling/src/__tests__/server.test.ts @@ -2,13 +2,14 @@ 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 +// 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' }; } - // Decode the payload from our fake tokens try { const parts = token.split('.'); const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString()); @@ -19,8 +20,7 @@ vi.mock('../auth.js', () => ({ }), })); -// Import after mock setup -const { SignalingServer } = await import('../server.js'); +import { SignalingServer } from '../server.js'; function makeToken(sub: string, exp?: number): string { const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url'); From 53b0e50d2fd4c4ab09f8e784ad382d3df12d5692 Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 15:42:23 -0700 Subject: [PATCH 31/32] Fix iOS and Android CI builds for monorepo - iOS: Use Ruby 3.1 (CFPropertyList 3.0.9 in Gemfile.lock requires < 3.2) - Android: Set react-native paths in app/build.gradle to resolve from monorepo root node_modules (npm workspaces hoists dependencies) --- .github/workflows/ci.yml | 2 +- apps/mobile/android/app/build.gradle | 14 +++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c9dcc4..cc7bd7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,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/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 From 503ab1ca107c9b3e7d6a74a61dd6d2b650357f95 Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Sun, 1 Mar 2026 15:52:26 -0700 Subject: [PATCH 32/32] Fix Android build: add AsyncStorage local Maven repo AsyncStorage v3 ships shared_storage as a local Maven artifact in android/local_repo/ but doesn't declare the repo in its own gradle config. Add it to allprojects.repositories with monorepo-aware path. --- apps/mobile/android/build.gradle | 11 +++++++++++ 1 file changed, 11 insertions(+) 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"