dc7e497388
# Conflicts: # app/(tabs)/codex.tsx # app/(tabs)/index.tsx # app/(tabs)/settings.tsx # hooks/useCodexUsage.ts # lib/api/codexApi.ts # package-lock.json # package.json
161 lines
4.3 KiB
TypeScript
161 lines
4.3 KiB
TypeScript
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<boolean> {
|
|
if (!Notifications) return false;
|
|
const { status } = await Notifications.requestPermissionsAsync();
|
|
return status === "granted";
|
|
}
|
|
|
|
export async function getPermissionStatus(): Promise<string> {
|
|
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<void> {
|
|
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.secondary?.usedPercent ?? 0)}% 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<void> {
|
|
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.secondary?.usedPercent ?? 0);
|
|
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<void> {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|