import type * as NotificationsType from "expo-notifications"; import { loadNotifSettings, loadNotifThresholdLastFired, saveNotifThresholdLastFired, } from "@/lib/storage"; import type { ClaudeUsageResponse } from "@/types/claude"; import type { CodexUsageResponse } from "@/types/codex"; // expo-notifications push notifications were removed from Expo Go in SDK 53. // Use require() so the initialization error is caught gracefully in Expo Go. let Notifications: typeof NotificationsType | null = null; try { Notifications = require("expo-notifications") as typeof NotificationsType; Notifications.setNotificationHandler({ handleNotification: async () => ({ shouldShowAlert: true, shouldShowBanner: true, shouldShowList: true, shouldPlaySound: false, shouldSetBadge: false, }), }); } catch { // Running in Expo Go or native module unavailable — notifications disabled. } export async function requestPermissions(): Promise { if (!Notifications) return false; const { status } = await Notifications.requestPermissionsAsync(); return status === "granted"; } export async function getPermissionStatus(): Promise { if (!Notifications) return "undetermined"; const { status } = await Notifications.getPermissionsAsync(); return status; } const DAILY_ID = "codexbar-daily-digest"; export async function scheduleDailyDigest( hour: number, minute: number, claudeUsage: ClaudeUsageResponse | null, codexUsage: CodexUsageResponse | null ): Promise { if (!Notifications) return; try { await Notifications.cancelScheduledNotificationAsync(DAILY_ID); } catch {} const parts: string[] = []; if (claudeUsage) { parts.push(`Claude 7d: ${Math.round(claudeUsage.seven_day.utilization)}% used`); } if (codexUsage) { parts.push( `Codex: ${Math.round(codexUsage.rate_limit.secondary_window.used_percent)}% used` ); } await Notifications.scheduleNotificationAsync({ identifier: DAILY_ID, content: { title: "Usage Digest", body: parts.length > 0 ? parts.join(" · ") : "Open to check your usage limits", }, trigger: { type: Notifications.SchedulableTriggerInputTypes.CALENDAR, hour, minute, repeats: true, }, }); } export async function cancelDailyDigest(): Promise { if (!Notifications) return; try { await Notifications.cancelScheduledNotificationAsync(DAILY_ID); } catch {} } interface ThresholdAlert { service: string; window: string; remaining: number; } function buildThresholdAlerts( thresholdPct: number, claudeUsage: ClaudeUsageResponse | null, codexUsage: CodexUsageResponse | null ): ThresholdAlert[] { const alerts: ThresholdAlert[] = []; if (claudeUsage) { const remaining = 100 - claudeUsage.seven_day.utilization; if (remaining <= thresholdPct) { alerts.push({ service: "Claude", window: "7-day", remaining }); } } if (codexUsage) { const remaining = 100 - codexUsage.rate_limit.secondary_window.used_percent; if (remaining <= thresholdPct) { alerts.push({ service: "Codex", window: "weekly", remaining }); } } return alerts; } /** Call this after every successful usage fetch. Reschedules daily digest and fires threshold alerts. */ export async function onUsageDataLoaded( claudeUsage: ClaudeUsageResponse | null, codexUsage: CodexUsageResponse | null ): Promise { const settings = await loadNotifSettings(); if (settings.dailyEnabled) { await scheduleDailyDigest( settings.dailyHour, settings.dailyMinute, claudeUsage, codexUsage ); } if (settings.thresholdEnabled) { const today = new Date().toISOString().slice(0, 10); const lastFired = await loadNotifThresholdLastFired(); if (lastFired !== today) { const alerts = buildThresholdAlerts( settings.thresholdPct, claudeUsage, codexUsage ); if (alerts.length > 0 && Notifications) { await Notifications.scheduleNotificationAsync({ content: { title: "Low quota warning", body: alerts .map( (a) => `${a.service} ${a.window}: ${Math.round(a.remaining)}% remaining` ) .join("\n"), }, trigger: null, }); await saveNotifThresholdLastFired(today); } } } }