feat(app): add Android product flavors and env-based daemon config

This commit is contained in:
Mohamed Boudra
2025-11-28 08:19:41 +01:00
parent 08dc2ed937
commit b97ae99b2b
7 changed files with 160 additions and 18 deletions
@@ -146,6 +146,57 @@ function createProfile(label: string, wsUrl: string): DaemonProfile {
};
}
type EnvDaemonConfig = {
label?: string;
wsUrl: string;
restUrl?: string | null;
};
function parseEnvDaemonDefaults(): DaemonProfile[] {
const envDaemons = (() => {
// Primary: allow a JSON array string like
// EXPO_PUBLIC_DAEMONS='[{"label":"Host","wsUrl":"ws://10.0.0.1:6767/ws"}]'
const jsonList = process.env.EXPO_PUBLIC_DAEMONS;
if (jsonList) {
try {
const parsed = JSON.parse(jsonList) as EnvDaemonConfig[];
if (Array.isArray(parsed) && parsed.length > 0) {
return parsed;
}
} catch (error) {
console.warn("[DaemonRegistry] Failed to parse EXPO_PUBLIC_DAEMONS:", error);
}
}
// Fallback: single host via EXPO_PUBLIC_DAEMON_WS_URL (+ optional label/rest URL)
const singleWsUrl = process.env.EXPO_PUBLIC_DAEMON_WS_URL;
if (singleWsUrl) {
return [
{
label: process.env.EXPO_PUBLIC_DAEMON_LABEL,
wsUrl: singleWsUrl,
restUrl: process.env.EXPO_PUBLIC_DAEMON_REST_URL,
},
];
}
return [];
})();
const timestamp = new Date().toISOString();
return envDaemons
.filter((entry) => typeof entry?.wsUrl === "string" && entry.wsUrl.trim().length > 0)
.map((entry) => ({
id: generateDaemonId(),
label: entry.label?.trim() || deriveLabelFromUrl(entry.wsUrl),
wsUrl: entry.wsUrl.trim(),
restUrl: entry.restUrl ?? null,
createdAt: timestamp,
updatedAt: timestamp,
metadata: null,
}));
}
async function loadDaemonRegistryFromStorage(): Promise<DaemonProfile[]> {
try {
const stored = await AsyncStorage.getItem(REGISTRY_STORAGE_KEY);
@@ -164,6 +215,12 @@ async function loadDaemonRegistryFromStorage(): Promise<DaemonProfile[]> {
}
}
const envDefaults = parseEnvDaemonDefaults();
if (envDefaults.length > 0) {
await AsyncStorage.setItem(REGISTRY_STORAGE_KEY, JSON.stringify(envDefaults));
return envDefaults;
}
const fallback = [createProfile("Local Host", FALLBACK_DAEMON_URL)];
await AsyncStorage.setItem(REGISTRY_STORAGE_KEY, JSON.stringify(fallback));
return fallback;
@@ -1,4 +1,4 @@
import { createContext, useContext, useState, useRef, ReactNode, useCallback, useEffect, useMemo } from "react";
import { createContext, useState, useRef, ReactNode, useCallback, useEffect, useMemo } from "react";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useWebSocket, type UseWebSocketReturn } from "@/hooks/use-websocket";
import { useDaemonRequest } from "@/hooks/use-daemon-request";
@@ -378,20 +378,13 @@ export interface SessionContextValue {
const SessionContext = createContext<SessionContextValue | null>(null);
export function useSession() {
const context = useContext(SessionContext);
if (!context) {
throw new Error("useSession must be used within SessionProvider");
}
return context;
}
interface SessionProviderProps {
children: ReactNode;
serverUrl: string;
serverId: string;
}
// SessionProvider feeds session state into Zustand; external consumers should use useDaemonSession or useSessionStore instead of accessing SessionContext directly.
export function SessionProvider({ children, serverUrl, serverId }: SessionProviderProps) {
const ws = useWebSocket(serverUrl);
const wsIsConnected = ws.isConnected;