From ba7d957155abc3dfd582a6399354939aa3b7fbeb Mon Sep 17 00:00:00 2001 From: space Date: Wed, 24 Jun 2026 20:16:34 +0200 Subject: [PATCH] Maximus upgradus --- .gitea/workflows/build-android.yml | 7 +- app.json | 5 +- app/(tabs)/_layout.tsx | 26 +- app/(tabs)/claude.tsx | 29 +- app/(tabs)/index.tsx | 331 +++++++++++++++------ app/(tabs)/settings.tsx | 454 +++++++++++++++++++++++------ app/_layout.tsx | 3 +- app/onboarding/claude.tsx | 94 ++++-- babel.config.js | 1 + hooks/useClaudeUsage.ts | 75 ++--- hooks/useNotificationSettings.ts | 64 ++++ lib/api/claudeApi.ts | 39 ++- lib/api/codexApi.ts | 1 + lib/fileReader.ts | 14 +- lib/notifications.ts | 148 ++++++++++ lib/storage.ts | 64 ++++ lib/timeUtils.ts | 22 +- package-lock.json | 448 ++++++---------------------- package.json | 4 +- types/claude.ts | 5 + 20 files changed, 1192 insertions(+), 642 deletions(-) create mode 100644 hooks/useNotificationSettings.ts create mode 100644 lib/notifications.ts diff --git a/.gitea/workflows/build-android.yml b/.gitea/workflows/build-android.yml index eb0a91c..4c8f699 100644 --- a/.gitea/workflows/build-android.yml +++ b/.gitea/workflows/build-android.yml @@ -53,11 +53,14 @@ jobs: - name: ๐Ÿ“ Rename build to APK run: mv app-build codexbar-release.apk + - name: ๐Ÿ—œ Zip APK + run: zip codexbar-release.zip codexbar-release.apk + - name: ๐Ÿ“ค Upload build artifact uses: actions/upload-artifact@v3 with: name: codexbar-android-preview-build - path: codexbar-release.apk + path: codexbar-release.zip if-no-files-found: error release: @@ -84,7 +87,7 @@ jobs: with: tag_name: ${{ env.RELEASE_TAG }} name: ${{ env.RELEASE_TAG }} - files: codexbar-release.apk + files: codexbar-release.zip generate_release_notes: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/app.json b/app.json index 7b380eb..a9b30fc 100644 --- a/app.json +++ b/app.json @@ -6,7 +6,7 @@ "version": "1.0.0", "orientation": "portrait", "icon": "./assets/icon.png", - "userInterfaceStyle": "light", + "userInterfaceStyle": "dark", "ios": { "supportsTablet": true }, @@ -27,7 +27,8 @@ "expo-router", "expo-status-bar", "expo-secure-store", - "expo-document-picker" + "expo-document-picker", + "expo-notifications" ], "extra": { "router": {}, diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 2fbe37a..6394ccd 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -1,10 +1,12 @@ import { Tabs } from "expo-router"; import { useColorScheme } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; import { MaterialIcons } from "@expo/vector-icons"; export default function TabLayout() { const colorScheme = useColorScheme(); const isDark = colorScheme === "dark"; + const insets = useSafeAreaInsets(); return ( - ( - - ), - }} - /> - ( - - ), - }} - /> + + - {sessionKey ? ( + {auth ? ( @@ -85,7 +87,7 @@ export default function ClaudeTab() { Connected - {sessionKey.slice(0, 10)}โ€ฆ + {auth.sessionKey.slice(0, 10)}โ€ฆ + - Chrome DevTools โ†’ Application โ†’ Cookies โ†’ claude.ai โ†’{" "} - - sessionKey - + Chrome DevTools โ†’ Application โ†’ Cookies โ†’ claude.ai โ†’ copy{" "} + sessionKey + {" "}and optionally{" "} + lastActiveOrg @@ -143,7 +154,7 @@ export default function ClaudeTab() { Usage - {status === "idle" && !sessionKey && ( + {status === "idle" && !auth && ( diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index d293015..2172b71 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -4,20 +4,81 @@ import { Text, TouchableOpacity, RefreshControl, + ActivityIndicator, } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import { router, useFocusEffect } from "expo-router"; -import { useCallback } from "react"; +import { useCallback, useEffect } from "react"; import { MaterialIcons } from "@expo/vector-icons"; import { useCodexUsage } from "@/hooks/useCodexUsage"; import { useClaudeUsage } from "@/hooks/useClaudeUsage"; -import { ServiceStatusCard } from "@/components/ServiceStatusCard"; +import { onUsageDataLoaded } from "@/lib/notifications"; +import { ProgressBar } from "@/components/ProgressBar"; +import { ResetCountdown } from "@/components/ResetCountdown"; import { windowLabel } from "@/lib/timeUtils"; +function UsageRow({ + label, + percent, + resetAtSeconds, + resetAtISO, +}: { + label: string; + percent: number; + resetAtSeconds?: number; + resetAtISO?: string; +}) { + const color = + percent >= 90 ? "#ef4444" : percent >= 70 ? "#f59e0b" : "#10a37f"; + return ( + + + + {label} + + + {Math.round(percent)}% used + + + + + + ); +} + +function SetupPrompt({ label }: { label: string }) { + return ( + + + {label} + + router.navigate("/(tabs)/settings")} + className="flex-row items-center self-start gap-x-1.5 px-3 py-2 rounded-xl border border-neutral-200 dark:border-neutral-700" + > + + Set up in Settings + + + + + ); +} + export default function DashboardTab() { const codex = useCodexUsage(); const claude = useClaudeUsage(); + // Fire daily digest reschedule + threshold check whenever fresh data arrives + useEffect(() => { + if (codex.status === "success" || claude.status === "success") { + void onUsageDataLoaded( + claude.status === "success" ? claude.usage : null, + codex.status === "success" ? codex.usage : null + ); + } + }, [codex.status, claude.status]); + useFocusEffect( useCallback(() => { codex.reload(); @@ -33,46 +94,16 @@ export default function DashboardTab() { claude.refresh(); }; - const codexRows = - codex.usage - ? [ - { - label: `Primary (${windowLabel(codex.usage.rate_limit.primary_window.limit_window_seconds)})`, - percent: codex.usage.rate_limit.primary_window.used_percent, - resetAtSeconds: codex.usage.rate_limit.primary_window.reset_at, - }, - { - label: `Secondary (${windowLabel(codex.usage.rate_limit.secondary_window.limit_window_seconds)})`, - percent: codex.usage.rate_limit.secondary_window.used_percent, - resetAtSeconds: codex.usage.rate_limit.secondary_window.reset_at, - }, - ] - : undefined; - - const claudeRows = - claude.usage - ? [ - { - label: "5-hour window", - percent: claude.usage.five_hour.utilization, - resetAtISO: claude.usage.five_hour.resets_at, - }, - { - label: "7-day window", - percent: claude.usage.seven_day.utilization, - resetAtISO: claude.usage.seven_day.resets_at, - }, - ] - : undefined; + const connectedCount = [codex.auth, claude.auth].filter(Boolean).length; return ( {/* Header */} - - - - Codexbar - - - AI usage monitor - + + + + + + + + Codexbar + + + {connectedCount === 0 + ? "No services connected" + : connectedCount === 2 + ? "2 services connected" + : "1 of 2 services connected"} + + {/* Codex card */} - router.navigate("/(tabs)/codex")} - footer={ - codex.usage && !codex.usage.credits.unlimited && codex.usage.credits.has_credits - ? ( - - - - ${Number(codex.usage.credits.balance).toFixed(2)} credits remaining - + + + + + + + - ) - : codex.usage?.credits.unlimited - ? ( - - - - Unlimited credits - - - ) - : null - } - /> + + Codex CLI + + + + {codex.usage?.plan_type && ( + + + {codex.usage.plan_type} + + + )} + {codex.auth && codex.status === "error" && ( + + )} + {codex.status === "success" && !codex.usage?.plan_type && ( + + )} + + + + {!codex.auth && ( + + )} + {codex.auth && codex.status === "loading" && ( + + )} + {codex.auth && codex.status === "error" && ( + + + + Failed to fetch usage + + + )} + {codex.status === "success" && codex.usage && ( + + + + {!codex.usage.credits.unlimited && + codex.usage.credits.has_credits && ( + + + + ${Number(codex.usage.credits.balance).toFixed(2)}{" "} + credits remaining + + + )} + {codex.usage.credits.unlimited && ( + + + + Unlimited credits + + + )} + + )} + + {/* Claude card */} - router.navigate("/(tabs)/claude")} - /> + + + + + + + + + + Claude.ai + + + {claude.auth && claude.status === "error" && ( + + )} + {claude.status === "success" && ( + + )} + + + {!claude.auth && ( + + )} + {claude.auth && claude.status === "loading" && ( + + )} + {claude.auth && claude.status === "error" && ( + + + + Failed to fetch usage + + + )} + {claude.status === "success" && claude.usage && ( + + + + {(claude.usage.seven_day_sonnet || + claude.usage.seven_day_opus) && ( + + + By Model (7-day) + + {claude.usage.seven_day_sonnet && ( + + )} + {claude.usage.seven_day_opus && ( + + )} + + )} + + )} + + ); diff --git a/app/(tabs)/settings.tsx b/app/(tabs)/settings.tsx index 479b8d9..1d029a8 100644 --- a/app/(tabs)/settings.tsx +++ b/app/(tabs)/settings.tsx @@ -1,18 +1,47 @@ import { useEffect, useState, useCallback } from "react"; -import { View, Text, TouchableOpacity, Alert, ScrollView } from "react-native"; +import { + View, + Text, + TouchableOpacity, + Alert, + ScrollView, + TextInput, + Switch, + KeyboardAvoidingView, + Platform, +} from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import { router, useFocusEffect } from "expo-router"; import { MaterialIcons } from "@expo/vector-icons"; import { loadCodexAuth, clearCodexAuth, + saveCodexAuth, loadClaudeSessionKey, clearClaudeSessionKey, + saveClaudeSessionKey, + saveClaudeLastActiveOrg, + clearClaudeLastActiveOrg, } from "@/lib/storage"; +import { pickAndReadCodexAuth } from "@/lib/fileReader"; import { resetOnboarding } from "@/lib/setupState"; +import { useNotificationSettings } from "@/hooks/useNotificationSettings"; import type { CodexAuth } from "@/types/codex"; import Constants from "expo-constants"; +function parseTimeInput(s: string): { hour: number; minute: number } | null { + const m = s.trim().match(/^(\d{1,2}):(\d{2})$/); + if (!m) return null; + const h = parseInt(m[1], 10); + const min = parseInt(m[2], 10); + if (h < 0 || h > 23 || min < 0 || min > 59) return null; + return { hour: h, minute: min }; +} + +function formatTime(hour: number, minute: number): string { + return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`; +} + function SettingRow({ icon, label, @@ -88,14 +117,27 @@ function SectionCard({ children }: { children: React.ReactNode }) { } function Divider() { - return ( - - ); + return ; } export default function SettingsTab() { const [codexAuth, setCodexAuth] = useState(null); const [claudeKey, setClaudeKey] = useState(null); + const [pendingKey, setPendingKey] = useState(""); + const [pendingOrg, setPendingOrg] = useState(""); + const [claudeError, setClaudeError] = useState(null); + + const notif = useNotificationSettings(); + const [timeInput, setTimeInput] = useState(""); + const [thresholdInput, setThresholdInput] = useState(""); + + // Sync local text inputs when settings load + useEffect(() => { + if (notif.loaded) { + setTimeInput(formatTime(notif.settings.dailyHour, notif.settings.dailyMinute)); + setThresholdInput(String(notif.settings.thresholdPct)); + } + }, [notif.loaded]); const reload = useCallback(() => { Promise.all([loadCodexAuth(), loadClaudeSessionKey()]).then( @@ -110,9 +152,40 @@ export default function SettingsTab() { reload(); }, [reload]); - useFocusEffect(useCallback(() => { - reload(); - }, [reload])); + useFocusEffect( + useCallback(() => { + reload(); + }, [reload]) + ); + + const handleImportCodex = async () => { + try { + const auth = await pickAndReadCodexAuth(); + await saveCodexAuth(auth); + setCodexAuth(auth); + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : "UNKNOWN_ERROR"; + if (msg !== "PICKER_CANCELLED") { + Alert.alert("Import failed", msg); + } + } + }; + + const handleSaveClaude = async () => { + const trimmed = pendingKey.trim(); + if (!trimmed.startsWith("sk-ant-")) { + setClaudeError("Key must start with sk-ant-"); + return; + } + const orgTrimmed = pendingOrg.trim() || undefined; + await saveClaudeSessionKey(trimmed); + if (orgTrimmed) await saveClaudeLastActiveOrg(orgTrimmed); + else await clearClaudeLastActiveOrg(); + setClaudeKey(trimmed); + setPendingKey(""); + setPendingOrg(""); + setClaudeError(null); + }; const handleClearCodex = () => { Alert.alert( @@ -172,6 +245,26 @@ export default function SettingsTab() { ); }; + const handleTimeBlur = useCallback(() => { + const parsed = parseTimeInput(timeInput); + if (parsed) { + void notif.update({ dailyHour: parsed.hour, dailyMinute: parsed.minute }); + } else { + // Reset to last valid value + setTimeInput(formatTime(notif.settings.dailyHour, notif.settings.dailyMinute)); + } + }, [timeInput, notif]); + + const handleThresholdBlur = useCallback(() => { + const n = parseInt(thresholdInput, 10); + if (!isNaN(n) && n >= 1 && n <= 99) { + void notif.update({ thresholdPct: n }); + } else { + setThresholdInput(String(notif.settings.thresholdPct)); + } + }, [thresholdInput, notif]); + + const canSaveClaude = pendingKey.trim().startsWith("sk-ant-"); const appVersion = Constants.expoConfig?.version ?? "1.0.0"; return ( @@ -179,88 +272,285 @@ export default function SettingsTab() { className="flex-1 bg-neutral-50 dark:bg-neutral-950" edges={["top", "bottom"]} > - - {/* Header */} - - - Settings - - + + + {/* Header */} + + + Settings + + - {/* Codex CLI section */} - - - {codexAuth ? ( - <> + {/* Codex CLI section */} + + + {codexAuth ? ( + <> + + + + + ) : ( - - - - ) : ( + )} + + + {/* Claude.ai section */} + + + {claudeKey ? ( + <> + + + + + ) : ( + + {claudeError && ( + + {claudeError} + + )} + { + setPendingKey(t); + setClaudeError(null); + }} + placeholder="sk-ant-โ€ฆ (required)" + placeholderTextColor="#a3a3a3" + autoCapitalize="none" + autoCorrect={false} + secureTextEntry + className="border border-neutral-200 dark:border-neutral-700 rounded-xl px-3 py-3 text-sm text-neutral-900 dark:text-white bg-neutral-50 dark:bg-neutral-800 mb-2" + /> + + + + Save Session Key + + + + Chrome DevTools โ†’ Application โ†’ Cookies โ†’ claude.ai โ†’{" "} + sessionKey + + + )} + + + {/* Notifications section */} + + + {notif.permissionStatus !== "granted" ? ( + { + const granted = await notif.askPermissions(); + if (!granted && notif.permissionStatus === "denied") { + Alert.alert( + "Notifications blocked", + "Go to Settings โ†’ CodexBar โ†’ Notifications to enable them." + ); + } + }} + className="flex-row items-center gap-x-3 py-3.5 px-4" + > + + + + + + Enable notifications + + + {notif.permissionStatus === "denied" + ? "Blocked โ€” open system Settings to allow" + : "Required to receive alerts"} + + + {notif.permissionStatus !== "denied" && ( + + + Allow + + + )} + + ) : ( + <> + {/* Daily digest */} + + + + + + + Daily digest + + + Daily reminder with last-known usage + + + void notif.update({ dailyEnabled: v })} + trackColor={{ false: "#e5e5e5", true: "#10a37f" }} + thumbColor="white" + /> + + + {notif.settings.dailyEnabled && ( + <> + + + + + + + Time + + + + + )} + + + + {/* Threshold alert */} + + + + + + + Low quota alert + + + Alert when weekly quota runs low + + + void notif.update({ thresholdEnabled: v })} + trackColor={{ false: "#e5e5e5", true: "#d97706" }} + thumbColor="white" + /> + + + {notif.settings.thresholdEnabled && ( + <> + + + + + + + Alert below + + + + + % remaining + + + + + )} + + )} + + + {/* App section */} + + router.navigate("/(tabs)/codex")} + icon="info-outline" + label="Version" + value={appVersion} + disabled /> - )} - - - {/* Claude.ai section */} - - - {claudeKey ? ( - <> - - - - - ) : ( + router.navigate("/(tabs)/claude")} + icon="restart-alt" + label="Re-run Setup Wizard" + onPress={handleResetSetup} + destructive /> - )} - - - {/* App section */} - - - - - - - + + + ); } diff --git a/app/_layout.tsx b/app/_layout.tsx index 9f1b810..109c5e4 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -2,11 +2,12 @@ import "../global.css"; import { Stack } from "expo-router"; import { SafeAreaProvider } from "react-native-safe-area-context"; import { StatusBar } from "expo-status-bar"; +import "@/lib/notifications"; // registers setNotificationHandler on app start export default function RootLayout() { return ( - + ); diff --git a/app/onboarding/claude.tsx b/app/onboarding/claude.tsx index e50cc90..04101a8 100644 --- a/app/onboarding/claude.tsx +++ b/app/onboarding/claude.tsx @@ -11,17 +11,28 @@ import { import { SafeAreaView } from "react-native-safe-area-context"; import { router } from "expo-router"; import { MaterialIcons } from "@expo/vector-icons"; -import { saveClaudeSessionKey, loadClaudeSessionKey } from "@/lib/storage"; +import { + saveClaudeSessionKey, + saveClaudeLastActiveOrg, + clearClaudeLastActiveOrg, + loadClaudeSessionKey, + loadClaudeLastActiveOrg, +} from "@/lib/storage"; export default function OnboardingClaudeScreen() { const [savedKey, setSavedKey] = useState(null); + const [savedOrg, setSavedOrg] = useState(null); const [pendingKey, setPendingKey] = useState(""); + const [pendingOrg, setPendingOrg] = useState(""); const [error, setError] = useState(null); useEffect(() => { - loadClaudeSessionKey().then((k) => { - if (k) setSavedKey(k); - }); + Promise.all([loadClaudeSessionKey(), loadClaudeLastActiveOrg()]).then( + ([k, o]) => { + if (k) setSavedKey(k); + if (o) setSavedOrg(o); + } + ); }, []); const canSave = pendingKey.trim().startsWith("sk-ant-"); @@ -32,11 +43,16 @@ export default function OnboardingClaudeScreen() { setError("Session key must start with sk-ant-"); return; } + const orgTrimmed = pendingOrg.trim() || undefined; await saveClaudeSessionKey(trimmed); + if (orgTrimmed) await saveClaudeLastActiveOrg(orgTrimmed); + else await clearClaudeLastActiveOrg(); setSavedKey(trimmed); + setSavedOrg(orgTrimmed ?? null); setPendingKey(""); + setPendingOrg(""); setError(null); - }, [pendingKey]); + }, [pendingKey, pendingOrg]); return ( @@ -92,27 +108,38 @@ export default function OnboardingClaudeScreen() { {savedKey ? ( - - - + + + + + + + Connected + + {savedKey.slice(0, 16)}โ€ฆ + + + { + setSavedKey(null); + setSavedOrg(null); + setPendingKey(""); + setPendingOrg(""); + }} + > + Change + - - Connected - - {savedKey.slice(0, 16)}โ€ฆ - - - { - setSavedKey(null); - setPendingKey(""); - }} - > - Change - + {savedOrg && ( + + + org: {savedOrg.slice(0, 8)}โ€ฆ + + + )} ) : ( @@ -127,13 +154,22 @@ export default function OnboardingClaudeScreen() { setPendingKey(t); setError(null); }} - placeholder="sk-ant-..." + placeholder="sk-ant-โ€ฆ (required)" placeholderTextColor="#525252" autoCapitalize="none" autoCorrect={false} secureTextEntry className="border border-neutral-800 rounded-xl px-4 py-3.5 text-sm text-white bg-neutral-900 mb-3" /> + - How to find your session key + How to find your credentials - Open Claude.ai in Chrome โ†’ DevTools (F12) โ†’ Application โ†’ Cookies โ†’ claude.ai โ†’ find{" "} - sessionKey + Open Claude.ai in Chrome โ†’ DevTools (F12) โ†’ Application โ†’ Cookies โ†’ claude.ai{"\n"} + Copy sessionKey and optionally lastActiveOrg diff --git a/babel.config.js b/babel.config.js index f3c649b..1d96698 100644 --- a/babel.config.js +++ b/babel.config.js @@ -5,5 +5,6 @@ module.exports = function (api) { ["babel-preset-expo", { jsxImportSource: "nativewind" }], "nativewind/babel", ], + plugins: ["react-native-reanimated/plugin"], }; }; diff --git a/hooks/useClaudeUsage.ts b/hooks/useClaudeUsage.ts index 9e93eb2..23c8e49 100644 --- a/hooks/useClaudeUsage.ts +++ b/hooks/useClaudeUsage.ts @@ -4,15 +4,18 @@ import { loadClaudeSessionKey, saveClaudeSessionKey, clearClaudeSessionKey, + loadClaudeLastActiveOrg, + saveClaudeLastActiveOrg, + clearClaudeLastActiveOrg, } from "@/lib/storage"; -import type { ClaudeUsageResponse } from "@/types/claude"; +import type { ClaudeAuth, ClaudeUsageResponse } from "@/types/claude"; type Status = "idle" | "loading" | "success" | "error"; async function doFetch( - sessionKey: string, + auth: ClaudeAuth, set: { - sessionKey: (v: string | null) => void; + auth: (v: ClaudeAuth | null) => void; usage: (v: ClaudeUsageResponse | null) => void; status: (v: Status) => void; error: (v: string | null) => void; @@ -21,10 +24,13 @@ async function doFetch( set.status("loading"); set.error(null); try { - const orgs = await fetchClaudeOrgs(sessionKey); - if (!orgs.length) throw new Error("NO_ORGS_FOUND"); - const orgUuid = orgs[0].uuid; - const data = await fetchClaudeUsage(sessionKey, orgUuid); + let orgUuid = auth.lastActiveOrg; + if (!orgUuid) { + const orgs = await fetchClaudeOrgs(auth); + if (!orgs.length) throw new Error("NO_ORGS_FOUND"); + orgUuid = orgs[0].uuid; + } + const data = await fetchClaudeUsage(auth, orgUuid); set.usage(data); set.status("success"); } catch (e: unknown) { @@ -33,29 +39,32 @@ async function doFetch( set.status("error"); if (msg === "TOKEN_EXPIRED") { await clearClaudeSessionKey(); - set.sessionKey(null); + set.auth(null); } } } export function useClaudeUsage() { - const [sessionKey, setSessionKey] = useState(null); + const [auth, setAuth] = useState(null); const [pendingKey, setPendingKey] = useState(""); + const [pendingOrg, setPendingOrg] = useState(""); const [usage, setUsage] = useState(null); const [status, setStatus] = useState("idle"); const [error, setError] = useState(null); - const setters = { sessionKey: setSessionKey, usage: setUsage, status: setStatus, error: setError }; + const setters = { auth: setAuth, usage: setUsage, status: setStatus, error: setError }; useEffect(() => { - loadClaudeSessionKey().then((stored) => { - if (stored) setSessionKey(stored); - }); + Promise.all([loadClaudeSessionKey(), loadClaudeLastActiveOrg()]).then( + ([key, org]) => { + if (key) setAuth({ sessionKey: key, lastActiveOrg: org ?? undefined }); + } + ); }, []); useEffect(() => { - if (sessionKey) void doFetch(sessionKey, setters); - }, [sessionKey]); + if (auth) void doFetch(auth, setters); + }, [auth]); const saveKey = useCallback(async () => { const trimmed = pendingKey.trim(); @@ -63,47 +72,47 @@ export function useClaudeUsage() { setError("Session key must start with sk-ant-"); return; } + const orgTrimmed = pendingOrg.trim() || undefined; await saveClaudeSessionKey(trimmed); - setSessionKey(trimmed); + if (orgTrimmed) await saveClaudeLastActiveOrg(orgTrimmed); + else await clearClaudeLastActiveOrg(); + setAuth({ sessionKey: trimmed, lastActiveOrg: orgTrimmed }); setPendingKey(""); + setPendingOrg(""); setError(null); - }, [pendingKey]); + }, [pendingKey, pendingOrg]); const refresh = useCallback(async () => { - if (!sessionKey) return; - await doFetch(sessionKey, setters); - }, [sessionKey]); + if (!auth) return; + await doFetch(auth, setters); + }, [auth]); - // Re-reads the session key from storage and fetches fresh data. Call from - // useFocusEffect so the dashboard stays current when the key is saved from - // another tab. If the string value is unchanged, React bails out on setState - // so we trigger the fetch directly. const reload = useCallback(async () => { - const stored = await loadClaudeSessionKey(); - if (!stored) { - setSessionKey(null); + const [key, org] = await Promise.all([loadClaudeSessionKey(), loadClaudeLastActiveOrg()]); + if (!key) { + setAuth(null); setUsage(null); setStatus("idle"); return; } - // If already set to the same string, setState bails out and [sessionKey] - // effect won't fire, so call the fetch directly. - setSessionKey(stored); - void doFetch(stored, setters); + // Creating a new object always triggers the [auth] useEffect even if values are identical + setAuth({ sessionKey: key, lastActiveOrg: org ?? undefined }); }, []); const clearKey = useCallback(async () => { await clearClaudeSessionKey(); - setSessionKey(null); + setAuth(null); setUsage(null); setStatus("idle"); setError(null); }, []); return { - sessionKey, + auth, pendingKey, setPendingKey, + pendingOrg, + setPendingOrg, usage, status, error, diff --git a/hooks/useNotificationSettings.ts b/hooks/useNotificationSettings.ts new file mode 100644 index 0000000..f044648 --- /dev/null +++ b/hooks/useNotificationSettings.ts @@ -0,0 +1,64 @@ +import { useState, useEffect, useCallback } from "react"; +import { + loadNotifSettings, + saveNotifSettings, + type NotifSettings, +} from "@/lib/storage"; +import { + requestPermissions, + getPermissionStatus, + scheduleDailyDigest, + cancelDailyDigest, +} from "@/lib/notifications"; + +export function useNotificationSettings() { + const [settings, setSettings] = useState({ + dailyEnabled: false, + dailyHour: 9, + dailyMinute: 0, + thresholdEnabled: false, + thresholdPct: 20, + }); + const [permissionStatus, setPermissionStatus] = useState("undetermined"); + const [loaded, setLoaded] = useState(false); + + useEffect(() => { + Promise.all([loadNotifSettings(), getPermissionStatus()]).then( + ([stored, status]) => { + setSettings(stored); + setPermissionStatus(status); + setLoaded(true); + } + ); + }, []); + + const askPermissions = useCallback(async (): Promise => { + const granted = await requestPermissions(); + setPermissionStatus(granted ? "granted" : "denied"); + return granted; + }, []); + + const update = useCallback( + async (patch: Partial) => { + const next = { ...settings, ...patch }; + setSettings(next); + await saveNotifSettings(next); + + // Keep scheduled notification in sync when toggling or changing time + const dailyChanged = + "dailyEnabled" in patch || + "dailyHour" in patch || + "dailyMinute" in patch; + if (dailyChanged) { + if (next.dailyEnabled) { + await scheduleDailyDigest(next.dailyHour, next.dailyMinute, null, null); + } else { + await cancelDailyDigest(); + } + } + }, + [settings] + ); + + return { settings, permissionStatus, loaded, askPermissions, update }; +} diff --git a/lib/api/claudeApi.ts b/lib/api/claudeApi.ts index 1f211bd..37201b8 100644 --- a/lib/api/claudeApi.ts +++ b/lib/api/claudeApi.ts @@ -1,24 +1,33 @@ -import type { ClaudeOrg, ClaudeUsageResponse } from "@/types/claude"; +import type { ClaudeAuth, ClaudeOrg, ClaudeUsageResponse } from "@/types/claude"; const BASE_URL = "https://claude.ai/api"; -function cookieHeader(sessionKey: string): Record { +const BROWSER_UA = + "Mozilla/5.0 (Linux; Android 10; Mobile) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36"; + +function buildHeaders(auth: ClaudeAuth): Record { + const cookies = [`sessionKey=${auth.sessionKey}`]; + if (auth.lastActiveOrg) cookies.push(`lastActiveOrg=${auth.lastActiveOrg}`); return { - Cookie: `sessionKey=${sessionKey}`, - "Accept": "application/json", - "Origin": "https://claude.ai", + Cookie: cookies.join("; "), + Accept: "application/json, text/plain, */*", + "Accept-Language": "en-US,en;q=0.9", + "User-Agent": BROWSER_UA, + Referer: "https://claude.ai/", + Origin: "https://claude.ai", + "anthropic-client-platform": "web_claude_to_cc_migration", }; } -export async function fetchClaudeOrgs(sessionKey: string): Promise { +export async function fetchClaudeOrgs(auth: ClaudeAuth): Promise { const response = await fetch(`${BASE_URL}/organizations`, { - headers: cookieHeader(sessionKey), + headers: buildHeaders(auth), + credentials: "omit", }); - if (response.status === 401 || response.status === 403) { - throw new Error("TOKEN_EXPIRED"); - } if (!response.ok) { + await response.text().catch(() => null); + if (response.status === 401 || response.status === 403) throw new Error("TOKEN_EXPIRED"); throw new Error(`HTTP_ERROR_${response.status}`); } @@ -26,17 +35,17 @@ export async function fetchClaudeOrgs(sessionKey: string): Promise } export async function fetchClaudeUsage( - sessionKey: string, + auth: ClaudeAuth, orgUuid: string ): Promise { const response = await fetch(`${BASE_URL}/organizations/${orgUuid}/usage`, { - headers: cookieHeader(sessionKey), + headers: buildHeaders(auth), + credentials: "omit", }); - if (response.status === 401 || response.status === 403) { - throw new Error("TOKEN_EXPIRED"); - } if (!response.ok) { + await response.text().catch(() => null); + if (response.status === 401 || response.status === 403) throw new Error("TOKEN_EXPIRED"); throw new Error(`HTTP_ERROR_${response.status}`); } diff --git a/lib/api/codexApi.ts b/lib/api/codexApi.ts index 7ac64b3..0fa1731 100644 --- a/lib/api/codexApi.ts +++ b/lib/api/codexApi.ts @@ -18,6 +18,7 @@ export async function fetchCodexUsage(auth: CodexAuth): Promise null); throw new Error(`HTTP_ERROR_${response.status}`); } diff --git a/lib/fileReader.ts b/lib/fileReader.ts index 1b846af..be97b43 100644 --- a/lib/fileReader.ts +++ b/lib/fileReader.ts @@ -1,5 +1,4 @@ import * as DocumentPicker from "expo-document-picker"; -import { File } from "expo-file-system/next"; import type { CodexAuth } from "@/types/codex"; export async function pickAndReadCodexAuth(): Promise { @@ -8,13 +7,11 @@ export async function pickAndReadCodexAuth(): Promise { copyToCacheDirectory: true, }); - if (result.canceled) { - throw new Error("PICKER_CANCELLED"); - } + if (result.canceled) throw new Error("PICKER_CANCELLED"); const { uri } = result.assets[0]; - const file = new File(uri); - const text = file.text(); + const response = await fetch(uri); + const text = await response.text(); let parsed: Record; try { @@ -23,8 +20,9 @@ export async function pickAndReadCodexAuth(): Promise { throw new Error("INVALID_JSON"); } - const accessToken = parsed["accessToken"] as string | undefined; - const accountId = parsed["accountId"] as string | undefined; + const tokens = parsed["tokens"] as Record | undefined; + const accessToken = (tokens?.["access_token"] ?? parsed["accessToken"]) as string | undefined; + const accountId = (tokens?.["account_id"] ?? parsed["accountId"]) as string | undefined; if (!accessToken || typeof accessToken !== "string") { throw new Error("MISSING_ACCESS_TOKEN"); diff --git a/lib/notifications.ts b/lib/notifications.ts new file mode 100644 index 0000000..ae68127 --- /dev/null +++ b/lib/notifications.ts @@ -0,0 +1,148 @@ +import * as Notifications from "expo-notifications"; +import { + loadNotifSettings, + loadNotifThresholdLastFired, + saveNotifThresholdLastFired, +} from "@/lib/storage"; +import type { ClaudeUsageResponse } from "@/types/claude"; +import type { CodexUsageResponse } from "@/types/codex"; + +Notifications.setNotificationHandler({ + handleNotification: async () => ({ + shouldShowAlert: true, + shouldShowBanner: true, + shouldShowList: true, + shouldPlaySound: false, + shouldSetBadge: false, + }), +}); + +export async function requestPermissions(): Promise { + const { status } = await Notifications.requestPermissionsAsync(); + return status === "granted"; +} + +export async function getPermissionStatus(): Promise { + 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 { + 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 { + 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) { + 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); + } + } + } +} diff --git a/lib/storage.ts b/lib/storage.ts index 825c1b4..2a603df 100644 --- a/lib/storage.ts +++ b/lib/storage.ts @@ -3,8 +3,59 @@ import * as SecureStore from "expo-secure-store"; const KEYS = { CODEX_AUTH: "codexbar_codex_auth", CLAUDE_SESSION_KEY: "codexbar_claude_session_key", + CLAUDE_LAST_ACTIVE_ORG: "codexbar_claude_last_active_org", + NOTIF_DAILY_ENABLED: "notifDailyEnabled", + NOTIF_DAILY_HOUR: "notifDailyHour", + NOTIF_DAILY_MINUTE: "notifDailyMinute", + NOTIF_THRESHOLD_ENABLED: "notifThresholdEnabled", + NOTIF_THRESHOLD_PCT: "notifThresholdPct", + NOTIF_THRESHOLD_LAST_FIRED: "notifThresholdLastFired", } as const; +export interface NotifSettings { + dailyEnabled: boolean; + dailyHour: number; + dailyMinute: number; + thresholdEnabled: boolean; + /** Alert when remaining quota falls below this % (e.g. 20 = fire when < 20% left) */ + thresholdPct: number; +} + +export async function loadNotifSettings(): Promise { + const [de, dh, dm, te, tp] = await Promise.all([ + SecureStore.getItemAsync(KEYS.NOTIF_DAILY_ENABLED), + SecureStore.getItemAsync(KEYS.NOTIF_DAILY_HOUR), + SecureStore.getItemAsync(KEYS.NOTIF_DAILY_MINUTE), + SecureStore.getItemAsync(KEYS.NOTIF_THRESHOLD_ENABLED), + SecureStore.getItemAsync(KEYS.NOTIF_THRESHOLD_PCT), + ]); + return { + dailyEnabled: de === "true", + dailyHour: dh !== null ? parseInt(dh, 10) : 9, + dailyMinute: dm !== null ? parseInt(dm, 10) : 0, + thresholdEnabled: te === "true", + thresholdPct: tp !== null ? parseInt(tp, 10) : 20, + }; +} + +export async function saveNotifSettings(s: NotifSettings): Promise { + await Promise.all([ + SecureStore.setItemAsync(KEYS.NOTIF_DAILY_ENABLED, String(s.dailyEnabled)), + SecureStore.setItemAsync(KEYS.NOTIF_DAILY_HOUR, String(s.dailyHour)), + SecureStore.setItemAsync(KEYS.NOTIF_DAILY_MINUTE, String(s.dailyMinute)), + SecureStore.setItemAsync(KEYS.NOTIF_THRESHOLD_ENABLED, String(s.thresholdEnabled)), + SecureStore.setItemAsync(KEYS.NOTIF_THRESHOLD_PCT, String(s.thresholdPct)), + ]); +} + +export async function loadNotifThresholdLastFired(): Promise { + return SecureStore.getItemAsync(KEYS.NOTIF_THRESHOLD_LAST_FIRED); +} + +export async function saveNotifThresholdLastFired(date: string): Promise { + await SecureStore.setItemAsync(KEYS.NOTIF_THRESHOLD_LAST_FIRED, date); +} + export async function saveCodexAuth(auth: { accessToken: string; accountId?: string; @@ -29,10 +80,23 @@ export async function loadClaudeSessionKey(): Promise { return SecureStore.getItemAsync(KEYS.CLAUDE_SESSION_KEY); } +export async function saveClaudeLastActiveOrg(orgId: string): Promise { + await SecureStore.setItemAsync(KEYS.CLAUDE_LAST_ACTIVE_ORG, orgId); +} + +export async function loadClaudeLastActiveOrg(): Promise { + return SecureStore.getItemAsync(KEYS.CLAUDE_LAST_ACTIVE_ORG); +} + +export async function clearClaudeLastActiveOrg(): Promise { + await SecureStore.deleteItemAsync(KEYS.CLAUDE_LAST_ACTIVE_ORG); +} + export async function clearCodexAuth(): Promise { await SecureStore.deleteItemAsync(KEYS.CODEX_AUTH); } export async function clearClaudeSessionKey(): Promise { await SecureStore.deleteItemAsync(KEYS.CLAUDE_SESSION_KEY); + await SecureStore.deleteItemAsync(KEYS.CLAUDE_LAST_ACTIVE_ORG); } diff --git a/lib/timeUtils.ts b/lib/timeUtils.ts index 809316b..75e9416 100644 --- a/lib/timeUtils.ts +++ b/lib/timeUtils.ts @@ -5,11 +5,17 @@ export function formatResetCountdown(resetAtSeconds: number): string { if (diffSeconds <= 0) return "resetting now"; if (diffSeconds < 60) return "resets in <1m"; - const hours = Math.floor(diffSeconds / 3600); + const totalHours = Math.floor(diffSeconds / 3600); const minutes = Math.floor((diffSeconds % 3600) / 60); - if (hours > 0 && minutes > 0) return `resets in ${hours}h ${minutes}m`; - if (hours > 0) return `resets in ${hours}h`; + if (totalHours >= 24) { + const days = Math.floor(totalHours / 24); + const hours = totalHours % 24; + if (hours > 0) return `resets in ${days}d ${hours}h`; + return `resets in ${days}d`; + } + if (totalHours > 0 && minutes > 0) return `resets in ${totalHours}h ${minutes}m`; + if (totalHours > 0) return `resets in ${totalHours}h`; return `resets in ${minutes}m`; } @@ -19,7 +25,11 @@ export function formatResetCountdownISO(isoString: string): string { } export function windowLabel(limitWindowSeconds: number): string { - if (limitWindowSeconds <= 3600) return "1h window"; - if (limitWindowSeconds <= 86400) return "24h window"; - return `${Math.round(limitWindowSeconds / 3600)}h window`; + const hours = limitWindowSeconds / 3600; + if (hours < 24) return `${Math.round(hours)}h`; + const days = hours / 24; + const wholeDays = Math.floor(days); + const remainderHours = Math.round(hours - wholeDays * 24); + if (remainderHours === 0) return `${wholeDays}d`; + return `${wholeDays}d ${remainderHours}h`; } diff --git a/package-lock.json b/package-lock.json index 7978f7a..0ea5b33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,12 +15,14 @@ "expo-document-picker": "~56.0.4", "expo-file-system": "~56.0.8", "expo-linking": "~56.0.14", + "expo-notifications": "~56.0.18", "expo-router": "~56.2.11", "expo-secure-store": "~56.0.4", "expo-status-bar": "~56.0.4", "nativewind": "^4.2.6", - "react": "^19.2.7", + "react": "19.2.3", "react-native": "0.85.3", + "react-native-reanimated": "4.3.1", "react-native-safe-area-context": "~5.7.0", "react-native-screens": "4.25.2" }, @@ -40,6 +42,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -567,22 +570,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", - "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-transform-async-generator-functions": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", @@ -956,38 +943,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", - "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", - "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-transform-react-pure-annotations": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.29.7.tgz", @@ -1004,22 +959,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz", - "integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-transform-runtime": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.7.tgz", @@ -1049,38 +988,6 @@ "semver": "bin/semver.js" } }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", - "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", - "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-transform-typescript": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", @@ -1665,6 +1572,7 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -1678,6 +1586,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -1687,6 +1596,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -2130,54 +2040,6 @@ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, - "node_modules/@react-native/babel-preset": { - "version": "0.85.3", - "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.85.3.tgz", - "integrity": "sha512-fD7fxEhkJB/aF57tWoXjaAWpklfrExYZS3k6aXPP3BQ77DZY7gvf/b7dbirwjID6NVnP1JDRJyTuPBGr0K/vlw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/core": "^7.25.2", - "@babel/plugin-proposal-export-default-from": "^7.24.7", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-default-from": "^7.24.7", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-transform-async-generator-functions": "^7.25.4", - "@babel/plugin-transform-async-to-generator": "^7.24.7", - "@babel/plugin-transform-block-scoping": "^7.25.0", - "@babel/plugin-transform-class-properties": "^7.25.4", - "@babel/plugin-transform-classes": "^7.25.4", - "@babel/plugin-transform-destructuring": "^7.24.8", - "@babel/plugin-transform-flow-strip-types": "^7.25.2", - "@babel/plugin-transform-for-of": "^7.24.7", - "@babel/plugin-transform-modules-commonjs": "^7.24.8", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", - "@babel/plugin-transform-optional-catch-binding": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.8", - "@babel/plugin-transform-private-methods": "^7.24.7", - "@babel/plugin-transform-private-property-in-object": "^7.24.7", - "@babel/plugin-transform-react-display-name": "^7.24.7", - "@babel/plugin-transform-react-jsx": "^7.25.2", - "@babel/plugin-transform-react-jsx-self": "^7.24.7", - "@babel/plugin-transform-react-jsx-source": "^7.24.7", - "@babel/plugin-transform-regenerator": "^7.24.7", - "@babel/plugin-transform-runtime": "^7.24.7", - "@babel/plugin-transform-typescript": "^7.25.2", - "@babel/plugin-transform-unicode-regex": "^7.24.7", - "@react-native/babel-plugin-codegen": "0.85.3", - "babel-plugin-syntax-hermes-parser": "0.33.3", - "babel-plugin-transform-flow-enums": "^0.0.2", - "react-refresh": "^0.14.0" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - }, - "peerDependencies": { - "@babel/core": "*" - } - }, "node_modules/@react-native/codegen": { "version": "0.85.3", "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.85.3.tgz", @@ -2308,58 +2170,6 @@ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, - "node_modules/@react-native/metro-babel-transformer": { - "version": "0.85.3", - "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.85.3.tgz", - "integrity": "sha512-omuKq+r7jM4XvCMIlNMPP7Up3SyB8o5EAdZtF7YXniKyq7UOMBqhYHFqgsdOXr0lT+3ADf7VCJG3sb82jlBrrQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/core": "^7.25.2", - "@react-native/babel-preset": "0.85.3", - "hermes-parser": "0.33.3", - "nullthrows": "^1.1.1" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - }, - "peerDependencies": { - "@babel/core": "*" - } - }, - "node_modules/@react-native/metro-babel-transformer/node_modules/hermes-estree": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.33.3.tgz", - "integrity": "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==", - "license": "MIT", - "peer": true - }, - "node_modules/@react-native/metro-babel-transformer/node_modules/hermes-parser": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.33.3.tgz", - "integrity": "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==", - "license": "MIT", - "peer": true, - "dependencies": { - "hermes-estree": "0.33.3" - } - }, - "node_modules/@react-native/metro-config": { - "version": "0.85.3", - "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.85.3.tgz", - "integrity": "sha512-sVo6HepUmCcpdfozEf91lA0FjpLNNZYu/Zi9FiYiAQTK8pzATXDVTqhvdxpFrQn435p5eUTSbllvbH/KN+bnyA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@react-native/js-polyfills": "0.85.3", - "@react-native/metro-babel-transformer": "0.85.3", - "metro-config": "^0.84.3", - "metro-runtime": "^0.84.3" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, "node_modules/@react-native/normalize-colors": { "version": "0.85.3", "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.85.3.tgz", @@ -2395,61 +2205,6 @@ "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", "license": "MIT" }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/dom/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@testing-library/dom/node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@testing-library/dom/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "license": "MIT", - "peer": true - }, "node_modules/@testing-library/jest-dom": { "version": "6.9.1", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", @@ -2488,13 +2243,6 @@ "@testing-library/dom": ">=7.21.4" } }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "license": "MIT", - "peer": true - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -2532,21 +2280,12 @@ "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" } }, - "node_modules/@types/react-test-renderer": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-19.1.0.tgz", - "integrity": "sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -2684,12 +2423,14 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, "license": "MIT" }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -2907,6 +2648,12 @@ } } }, + "node_modules/badgin": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/badgin/-/badgin-1.2.3.tgz", + "integrity": "sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==", + "license": "MIT" + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -2961,6 +2708,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3087,6 +2835,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -3132,6 +2881,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -3156,6 +2906,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -3444,6 +3195,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -3456,6 +3208,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -3552,12 +3305,14 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, "license": "Apache-2.0" }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, "license": "MIT" }, "node_modules/dnssd-advertise": { @@ -3566,13 +3321,6 @@ "integrity": "sha512-Ndrrf6BMPalkQPd/zubL+4YghH2J9NspapQ09uDXwYbvOPkP0oaqf5CkcwJ0b50kS2O3ul6yVu+jz+RY62Cejg==", "license": "MIT" }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "license": "MIT", - "peer": true - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -3738,6 +3486,15 @@ } } }, + "node_modules/expo-application": { + "version": "56.0.3", + "resolved": "https://registry.npmjs.org/expo-application/-/expo-application-56.0.3.tgz", + "integrity": "sha512-DdGGPlMuM6cSTeKhbvh6OeLr2O/+EI5BHKYrD+Do8sJPYgLwzGrgESELfyjJCpEhFzT+TgKIdmLmWXhNUQnHiw==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-constants": { "version": "56.0.18", "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-56.0.18.tgz", @@ -3854,6 +3611,24 @@ "react-native": "*" } }, + "node_modules/expo-notifications": { + "version": "56.0.18", + "resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-56.0.18.tgz", + "integrity": "sha512-HHnrwyCLC5srFojcHYS2KskbNroy9o2fwPKdyhjrdjjrBu4sNRKm4LepcuZjDy98cZKEm89WIPW8O45vut8Rgw==", + "license": "MIT", + "dependencies": { + "@expo/image-utils": "^0.10.1", + "abort-controller": "^3.0.0", + "badgin": "^1.1.5", + "expo-application": "~56.0.3", + "expo-constants": "~56.0.18" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/expo-router": { "version": "56.2.11", "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-56.2.11.tgz", @@ -4377,6 +4152,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -4393,6 +4169,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -4405,6 +4182,7 @@ "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -4516,6 +4294,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -4592,6 +4371,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -4766,6 +4546,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -4808,6 +4589,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4826,6 +4608,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -4959,6 +4742,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -5327,6 +5111,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -5339,6 +5124,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, "license": "MIT" }, "node_modules/lodash.debounce": { @@ -5457,16 +5243,6 @@ "yallist": "^3.0.2" } }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "license": "MIT", - "peer": true, - "bin": { - "lz-string": "bin/bin.js" - } - }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -5498,6 +5274,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -5891,6 +5668,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0", @@ -5970,6 +5748,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6012,6 +5791,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6021,6 +5801,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -6267,6 +6048,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6276,6 +6058,7 @@ "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -6345,6 +6128,7 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -6362,6 +6146,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, "funding": [ { "type": "opencollective", @@ -6387,6 +6172,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, "funding": [ { "type": "opencollective", @@ -6429,6 +6215,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -6454,6 +6241,7 @@ "version": "6.1.4", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -6467,6 +6255,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, "license": "MIT" }, "node_modules/pretty-format": { @@ -6566,6 +6355,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, "funding": [ { "type": "github", @@ -6592,9 +6382,9 @@ } }, "node_modules/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", - "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6610,19 +6400,6 @@ "ws": "^7" } }, - "node_modules/react-dom": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", - "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.7" - } - }, "node_modules/react-fast-compare": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", @@ -6993,46 +6770,29 @@ "react-native-reanimated": ">= 2.0.0" } }, - "node_modules/react-native-gesture-handler": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-3.0.2.tgz", - "integrity": "sha512-XBTgmvr2jh/xrMwlHTV2Z1x6IFUl3zfLxyaCAnvj3GSXK5YlY3ZmiIs57hniPmh3I7RtjPFxtGdRr0i+PzyBrg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/react-test-renderer": "^19.1.0", - "invariant": "^2.2.4" - }, - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, "node_modules/react-native-is-edge-to-edge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.3.1.tgz", "integrity": "sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA==", "license": "MIT", - "peer": true, "peerDependencies": { "react": "*", "react-native": "*" } }, "node_modules/react-native-reanimated": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.5.0.tgz", - "integrity": "sha512-+iPfvK34PKKYP/p/4TaBliFkbfvjGDIvXuiiaxvISP5ip7sWegvlacwU/uAV6zNDSSmX0tDyER7PurPMKGDipA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.3.1.tgz", + "integrity": "sha512-KhGsS0YkCA+gusgyzlf9hnqzVPIR398KTpqXyqq/+yYJJPAvyEEPKcxlB0xtOOXSMrR2A9uRKVARVQhZwrOh+Q==", "license": "MIT", - "peer": true, "dependencies": { "react-native-is-edge-to-edge": "^1.3.1", "semver": "^7.7.3" }, "peerDependencies": { "react": "*", - "react-native": "0.83 - 0.86", - "react-native-worklets": "0.10.x" + "react-native": "0.81 - 0.85", + "react-native-worklets": "0.8.x" } }, "node_modules/react-native-safe-area-context": { @@ -7059,33 +6819,6 @@ "react-native": ">=0.82.0" } }, - "node_modules/react-native-worklets": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.10.0.tgz", - "integrity": "sha512-JhE6IxDf6iabC0qu3+TAKA4v9RlluXmoIngPQX7/QUByf75lfrsHZ6/dQhyjEWnp1EEQiwzz8Cpew140ZcewDw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-class-properties": "^7.28.6", - "@babel/plugin-transform-classes": "^7.28.6", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", - "@babel/plugin-transform-optional-chaining": "^7.28.6", - "@babel/plugin-transform-shorthand-properties": "^7.27.1", - "@babel/plugin-transform-template-literals": "^7.27.1", - "@babel/plugin-transform-unicode-regex": "^7.27.1", - "@babel/preset-typescript": "^7.28.5", - "@babel/types": "^7.27.1", - "convert-source-map": "^2.0.0", - "semver": "^7.7.4" - }, - "peerDependencies": { - "@babel/core": "*", - "@react-native/metro-config": "*", - "react": "*", - "react-native": "0.83 - 0.86" - } - }, "node_modules/react-native/node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", @@ -7177,6 +6910,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, "license": "MIT", "dependencies": { "pify": "^2.3.0" @@ -7186,6 +6920,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -7328,6 +7063,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -7338,6 +7074,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "funding": [ { "type": "github", @@ -7752,6 +7489,7 @@ "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -7774,6 +7512,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -7820,6 +7559,7 @@ "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -7897,6 +7637,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0" @@ -7906,6 +7647,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" @@ -8002,6 +7744,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, "license": "Apache-2.0" }, "node_modules/tslib": { @@ -8023,7 +7766,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -8174,6 +7917,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/utils-merge": { diff --git a/package.json b/package.json index c5d1ed6..6f2b89a 100644 --- a/package.json +++ b/package.json @@ -10,12 +10,14 @@ "expo-document-picker": "~56.0.4", "expo-file-system": "~56.0.8", "expo-linking": "~56.0.14", + "expo-notifications": "~56.0.18", "expo-router": "~56.2.11", "expo-secure-store": "~56.0.4", "expo-status-bar": "~56.0.4", "nativewind": "^4.2.6", - "react": "^19.2.7", + "react": "19.2.3", "react-native": "0.85.3", + "react-native-reanimated": "4.3.1", "react-native-safe-area-context": "~5.7.0", "react-native-screens": "4.25.2" }, diff --git a/types/claude.ts b/types/claude.ts index 37afd77..2019d15 100644 --- a/types/claude.ts +++ b/types/claude.ts @@ -1,3 +1,8 @@ +export interface ClaudeAuth { + sessionKey: string; + lastActiveOrg?: string; +} + export interface ClaudeOrg { uuid: string; name: string;