diff options
Diffstat (limited to 'api')
| -rw-r--r-- | api/activity.ts | 23 | ||||
| -rw-r--r-- | api/alerts.ts | 18 | ||||
| -rw-r--r-- | api/client.ts | 147 | ||||
| -rw-r--r-- | api/controls.ts | 101 | ||||
| -rw-r--r-- | api/devices.ts | 38 | ||||
| -rw-r--r-- | api/home.ts | 31 | ||||
| -rw-r--r-- | api/index.ts | 8 | ||||
| -rw-r--r-- | api/settings.ts | 42 | ||||
| -rw-r--r-- | api/types.ts | 53 |
9 files changed, 461 insertions, 0 deletions
diff --git a/api/activity.ts b/api/activity.ts new file mode 100644 index 0000000..a36c666 --- /dev/null +++ b/api/activity.ts @@ -0,0 +1,23 @@ +import { apiClient } from "./client"; +import { ActivityData } from "./types"; + +export async function getActivityData(): Promise<ActivityData> { + try { + const response = await apiClient.get<{ + success: boolean; + period: string; + metrics: ActivityData["metrics"]; + }>("/parent/activity"); + + return { + period: response.period, + metrics: response.metrics, + }; + } catch (e) { + console.error("Failed to fetch activity data", e); + return { + period: "Last 7 days", + metrics: [], + }; + } +} diff --git a/api/alerts.ts b/api/alerts.ts new file mode 100644 index 0000000..8fbcb07 --- /dev/null +++ b/api/alerts.ts @@ -0,0 +1,18 @@ +import { apiClient } from "./client"; +import { Alert } from "./types"; + +export async function getAlerts(): Promise<Alert[]> { + try { + console.log("getAlerts: Making API call to /parent/alerts"); + const response = await apiClient.get<{ + success: boolean; + alerts: Alert[]; + }>("/parent/alerts"); + + console.log("getAlerts: API response:", response); + return response.alerts; + } catch (e) { + console.error("Failed to fetch alerts", e); + return []; + } +} diff --git a/api/client.ts b/api/client.ts new file mode 100644 index 0000000..703b1f0 --- /dev/null +++ b/api/client.ts @@ -0,0 +1,147 @@ +import Constants from "expo-constants"; +import * as SecureStore from "expo-secure-store"; + +function getApiBaseUrl() { + const envUrl = (process as any)?.env?.API_BASE_URL; + const extraUrl = + (Constants.manifest as any)?.extra?.API_BASE_URL || + (Constants.expoConfig as any)?.extra?.API_BASE_URL; + if (envUrl) return envUrl; + if (extraUrl) return extraUrl; + + throw new Error("API_BASE_URL is not defined in environment or Expo config"); +} + +const API_BASE_URL = getApiBaseUrl(); + +const AUTH_TOKEN_KEY = "buddy_auth_token"; +const SELECTED_DEVICE_KEY = "buddy_selected_device"; + +class ApiClient { + private token: string | null = null; + private selectedDeviceId: string | null = null; + + async initialize() { + try { + this.token = await SecureStore.getItemAsync(AUTH_TOKEN_KEY); + this.selectedDeviceId = + await SecureStore.getItemAsync(SELECTED_DEVICE_KEY); + } catch (e) { + console.error("Failed to load auth token", e); + } + } + + async setToken(token: string) { + this.token = token; + await SecureStore.setItemAsync(AUTH_TOKEN_KEY, token); + } + + async clearToken() { + this.token = null; + await SecureStore.deleteItemAsync(AUTH_TOKEN_KEY); + } + + async setSelectedDevice(deviceId: string) { + this.selectedDeviceId = deviceId; + await SecureStore.setItemAsync(SELECTED_DEVICE_KEY, deviceId); + } + + getSelectedDeviceId(): string | null { + return this.selectedDeviceId; + } + + isAuthenticated(): boolean { + return this.token !== null; + } + + private async request<T>( + method: "GET" | "POST" | "PUT" | "DELETE", + endpoint: string, + body?: any, + ): Promise<T> { + const headers: HeadersInit = { + "Content-Type": "application/json", + }; + + if (this.token) { + headers["Authorization"] = `Bearer ${this.token}`; + } + + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }); + + if (!response.ok) { + const error = await response + .json() + .catch(() => ({ reason: "Unknown error" })); + throw new Error(error.reason || `HTTP ${response.status}`); + } + + return response.json(); + } + + async get<T>(endpoint: string): Promise<T> { + return this.request<T>("GET", endpoint); + } + + async post<T>(endpoint: string, body?: any): Promise<T> { + return this.request<T>("POST", endpoint, body); + } + + async delete<T>(endpoint: string, body?: any): Promise<T> { + return this.request<T>("DELETE", endpoint, body); + } + + // Auth endpoints + async signIn( + email: string, + password: string, + ): Promise<{ success: boolean; token?: string; reason?: string }> { + const result = await this.post<{ + success: boolean; + token: string; + reason: string; + }>("/signin", { + email, + password, + }); + + if (result.success && result.token) { + await this.setToken(result.token); + } + + return result; + } + + async signUp( + email: string, + password: string, + ): Promise<{ success: boolean; token?: string; reason?: string }> { + const result = await this.post<{ + success: boolean; + token: string; + reason: string; + }>("/signup", { + email, + password, + }); + + if (result.success && result.token) { + await this.setToken(result.token); + } + + return result; + } + + async signOut() { + await this.clearToken(); + } +} + +export const apiClient = new ApiClient(); + +// Initialize on import +apiClient.initialize(); diff --git a/api/controls.ts b/api/controls.ts new file mode 100644 index 0000000..53db7a1 --- /dev/null +++ b/api/controls.ts @@ -0,0 +1,101 @@ +import { apiClient } from "./client"; +import { ControlsData, SafetyControl } from "./types"; + +export async function getControlsData(): Promise<ControlsData> { + const deviceId = apiClient.getSelectedDeviceId(); + + if (!deviceId) { + // Return default controls if no device selected + return { + safetyControls: getDefaultControls(), + }; + } + + try { + const response = await apiClient.get<{ + success: boolean; + safetyControls: SafetyControl[]; + }>(`/parent/controls/${deviceId}`); + + return { + safetyControls: response.safetyControls, + }; + } catch (e) { + console.error("Failed to fetch controls data", e); + return { + safetyControls: getDefaultControls(), + }; + } +} + +export async function updateSafetyControl( + key: string, + value: boolean +): Promise<void> { + const deviceId = apiClient.getSelectedDeviceId(); + + if (!deviceId) { + console.warn("No device selected, cannot update control"); + return; + } + + try { + await apiClient.post(`/parent/controls/${deviceId}`, { key, value }); + } catch (e) { + console.error(`Failed to update ${key}`, e); + throw e; + } +} + +function getDefaultControls(): SafetyControl[] { + return [ + { + key: "disable_buddy", + title: "Disable Buddy", + description: "Temporarily disable Buddy", + defaultValue: false, + }, + { + key: "adult_sites", + title: "Adult sites", + description: "Block adult websites.", + defaultValue: true, + }, + { + key: "family_link_anti_circumvention", + title: "Anti-Circumvention", + description: "Prevent disabling of Family Link protections.", + defaultValue: false, + }, + { + key: "filtering", + title: "Content filtering", + description: "Block unsafe or adult content.", + defaultValue: true, + }, + { + key: "new_people", + title: "New contact alerts", + description: "Get notified when your child chats with someone new.", + defaultValue: true, + }, + { + key: "block_strangers", + title: "Block communications with strangers", + description: "Block or scan communications with strangers.", + defaultValue: false, + }, + { + key: "notify_dangerous_messages", + title: "Dangerous messages notifications", + description: "Notify when messages are potentially dangerous.", + defaultValue: true, + }, + { + key: "notify_new_contact_added", + title: "New contact added notifications", + description: "Notify when a new contact is added.", + defaultValue: true, + }, + ]; +} diff --git a/api/devices.ts b/api/devices.ts new file mode 100644 index 0000000..e8bbb92 --- /dev/null +++ b/api/devices.ts @@ -0,0 +1,38 @@ +import { apiClient } from "./client"; +import { Device } from "./types"; + +export async function getDevices(): Promise<Device[]> { + try { + const response = await apiClient.get<{ + success: boolean; + devices: Device[]; + }>("/parent/devices"); + + return response.devices; + } catch (e) { + console.error("Failed to fetch devices", e); + return []; + } +} + +interface RenameDeviceResponse { + success: boolean; +} + +export async function renameDevice( + deviceId: string, + name: string +): Promise<boolean> { + try { + const response: RenameDeviceResponse = await apiClient.post( + `/parent/device/${deviceId}/rename`, + { + name, + } + ); + return response.success; + } catch (e) { + console.error("Failed to rename device", e); + return false; + } +} diff --git a/api/home.ts b/api/home.ts new file mode 100644 index 0000000..89e1cca --- /dev/null +++ b/api/home.ts @@ -0,0 +1,31 @@ +import { apiClient } from "./client"; +import { HomeData } from "./types"; + +export async function getHomeData(deviceId?: string): Promise<HomeData> { + try { + const endpoint = deviceId ? `/parent/home/${deviceId}` : "/parent/home"; + const response = await apiClient.get<{ + success: boolean; + overallStatus: HomeData["overallStatus"]; + deviceOnline: boolean; + alertStats: HomeData["alertStats"]; + }>(endpoint); + + return { + overallStatus: response.overallStatus, + deviceOnline: response.deviceOnline, + alertStats: response.alertStats, + }; + } catch (e) { + console.error("Failed to fetch home data", e); + // Return default data on error + return { + overallStatus: "all_clear", + deviceOnline: false, + alertStats: { + last24Hours: 0, + thisWeekReviewed: 0, + }, + }; + } +} diff --git a/api/index.ts b/api/index.ts new file mode 100644 index 0000000..05a61aa --- /dev/null +++ b/api/index.ts @@ -0,0 +1,8 @@ +export * from "./activity"; +export * from "./alerts"; +export * from "./client"; +export * from "./controls"; +export * from "./devices"; +export * from "./home"; +export * from "./settings"; +export * from "./types"; diff --git a/api/settings.ts b/api/settings.ts new file mode 100644 index 0000000..2823afa --- /dev/null +++ b/api/settings.ts @@ -0,0 +1,42 @@ +import { apiClient } from "./client"; + +interface VerifyEmailResponse { + success: boolean; + reason?: string; +} + +export async function verifyEmail( + code: string, +): Promise<{ success: boolean; error?: string }> { + try { + const response: VerifyEmailResponse = await apiClient.post( + "/parent/verifyemail", + { code }, + ); + return { success: response.success }; + } catch (e) { + console.error("Failed to verify email", e); + return { + success: false, + error: e instanceof Error ? e.message : "Failed to verify email", + }; + } +} + +export interface UserProfile { + email: string; + emailVerified: boolean; +} + +export async function getUserProfile(): Promise<UserProfile | null> { + try { + const response = await apiClient.get<{ + success: boolean; + profile: UserProfile; + }>("/parent/profile"); + return response.profile; + } catch (e) { + console.error("Failed to fetch user profile", e); + return null; + } +} diff --git a/api/types.ts b/api/types.ts new file mode 100644 index 0000000..7338d4b --- /dev/null +++ b/api/types.ts @@ -0,0 +1,53 @@ +export type OverallStatus = "all_clear" | "attention"; + +export type AlertSeverity = "gentle" | "needs_attention"; + +export type Alert = { + id: string; + title: string; + timeLabel: string; + whatHappened: string; + whyItMatters: string; + suggestedAction: string; + severity: AlertSeverity; +}; + +export type HomeData = { + overallStatus: OverallStatus; + deviceOnline: boolean; + alertStats: { + last24Hours: number; + thisWeekReviewed: number; + }; +}; + +export type ActivityMetric = { + id: string; + icon: "chatbubbles" | "people" | "time"; + title: string; + description: string; + level: "Normal" | "Low" | "High" | "Better" | "Worse"; +}; + +export type ActivityData = { + period: string; + metrics: ActivityMetric[]; +}; + +export type Device = { + id: string; + name: string; + status: "online" | "offline"; + lastCheck: string; +}; + +export type SafetyControl = { + key: string; + title: string; + description: string; + defaultValue: boolean; +}; + +export type ControlsData = { + safetyControls: SafetyControl[]; +}; |