summaryrefslogtreecommitdiff
path: root/api
diff options
context:
space:
mode:
Diffstat (limited to 'api')
-rw-r--r--api/activity.ts23
-rw-r--r--api/alerts.ts18
-rw-r--r--api/client.ts147
-rw-r--r--api/controls.ts101
-rw-r--r--api/devices.ts38
-rw-r--r--api/home.ts31
-rw-r--r--api/index.ts8
-rw-r--r--api/settings.ts42
-rw-r--r--api/types.ts53
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[];
+};