Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a00822690 | |||
| 2c31be11c4 | |||
| 9dd17cf565 | |||
| 6992e58b9c |
@@ -1,35 +1,75 @@
|
|||||||
{
|
{
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "codexbar-mobile",
|
"name": "Codexbar",
|
||||||
"slug": "codexbar-mobile",
|
"slug": "codexbar-mobile",
|
||||||
|
"description": "Monitor Codex CLI and Claude.ai usage limits, resets, and credits at a glance.",
|
||||||
"scheme": "codexbar",
|
"scheme": "codexbar",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
"userInterfaceStyle": "dark",
|
"backgroundColor": "#090D11",
|
||||||
|
"primaryColor": "#10A37F",
|
||||||
|
"userInterfaceStyle": "automatic",
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true
|
"bundleIdentifier": "dev.reversed.codexbar",
|
||||||
|
"buildNumber": "1",
|
||||||
|
"supportsTablet": true,
|
||||||
|
"config": {
|
||||||
|
"usesNonExemptEncryption": false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"package": "dev.reversed.codexbar",
|
"package": "dev.reversed.codexbar",
|
||||||
|
"versionCode": 1,
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"backgroundColor": "#E6F4FE",
|
"backgroundColor": "#090D11",
|
||||||
"foregroundImage": "./assets/android-icon-foreground.png",
|
"foregroundImage": "./assets/android-icon-foreground.png",
|
||||||
"backgroundImage": "./assets/android-icon-background.png",
|
"backgroundImage": "./assets/android-icon-background.png",
|
||||||
"monochromeImage": "./assets/android-icon-monochrome.png"
|
"monochromeImage": "./assets/android-icon-monochrome.png"
|
||||||
},
|
},
|
||||||
"predictiveBackGestureEnabled": false
|
"predictiveBackGestureEnabled": true
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
|
"bundler": "metro",
|
||||||
|
"output": "static",
|
||||||
"favicon": "./assets/favicon.png"
|
"favicon": "./assets/favicon.png"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"expo-router",
|
"expo-router",
|
||||||
"expo-status-bar",
|
[
|
||||||
"expo-secure-store",
|
"expo-splash-screen",
|
||||||
|
{
|
||||||
|
"image": "./assets/splash-icon.png",
|
||||||
|
"imageWidth": 220,
|
||||||
|
"resizeMode": "contain",
|
||||||
|
"backgroundColor": "#090D11",
|
||||||
|
"dark": {
|
||||||
|
"image": "./assets/splash-icon.png",
|
||||||
|
"backgroundColor": "#090D11"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"expo-secure-store",
|
||||||
|
{
|
||||||
|
"configureAndroidBackup": true,
|
||||||
|
"faceIDPermission": "Allow Codexbar to access your securely stored credentials."
|
||||||
|
}
|
||||||
|
],
|
||||||
"expo-document-picker",
|
"expo-document-picker",
|
||||||
"expo-notifications"
|
[
|
||||||
|
"expo-notifications",
|
||||||
|
{
|
||||||
|
"icon": "./assets/notification-icon.png",
|
||||||
|
"color": "#10A37F",
|
||||||
|
"defaultChannel": "usage-alerts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expo-font"
|
||||||
],
|
],
|
||||||
|
"experiments": {
|
||||||
|
"typedRoutes": true
|
||||||
|
},
|
||||||
"extra": {
|
"extra": {
|
||||||
"router": {},
|
"router": {},
|
||||||
"eas": {
|
"eas": {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Tabs } from "expo-router";
|
|||||||
import { useColorScheme } from "react-native";
|
import { useColorScheme } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { MaterialIcons } from "@expo/vector-icons";
|
import { MaterialIcons } from "@expo/vector-icons";
|
||||||
|
import { COLORS } from "@/lib/constants";
|
||||||
|
|
||||||
export default function TabLayout() {
|
export default function TabLayout() {
|
||||||
const colorScheme = useColorScheme();
|
const colorScheme = useColorScheme();
|
||||||
@@ -12,7 +13,7 @@ export default function TabLayout() {
|
|||||||
<Tabs
|
<Tabs
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
tabBarActiveTintColor: "#10a37f",
|
tabBarActiveTintColor: COLORS.codex,
|
||||||
tabBarInactiveTintColor: isDark ? "#52525b" : "#a1a1aa",
|
tabBarInactiveTintColor: isDark ? "#52525b" : "#a1a1aa",
|
||||||
tabBarStyle: {
|
tabBarStyle: {
|
||||||
backgroundColor: isDark ? "#09090b" : "#ffffff",
|
backgroundColor: isDark ? "#09090b" : "#ffffff",
|
||||||
|
|||||||
@@ -14,8 +14,11 @@ import { MaterialIcons } from "@expo/vector-icons";
|
|||||||
import { useClaudeUsage } from "@/hooks/useClaudeUsage";
|
import { useClaudeUsage } from "@/hooks/useClaudeUsage";
|
||||||
import { UsageStat } from "@/components/UsageStat";
|
import { UsageStat } from "@/components/UsageStat";
|
||||||
import { ErrorMessage } from "@/components/ErrorMessage";
|
import { ErrorMessage } from "@/components/ErrorMessage";
|
||||||
|
import { LastUpdated } from "@/components/LastUpdated";
|
||||||
|
import { ScreenErrorBoundary } from "@/components/ScreenErrorBoundary";
|
||||||
|
import { COLORS } from "@/lib/constants";
|
||||||
|
|
||||||
export default function ClaudeTab() {
|
function ClaudeTabContent() {
|
||||||
const {
|
const {
|
||||||
auth,
|
auth,
|
||||||
pendingKey,
|
pendingKey,
|
||||||
@@ -23,15 +26,16 @@ export default function ClaudeTab() {
|
|||||||
pendingOrg,
|
pendingOrg,
|
||||||
setPendingOrg,
|
setPendingOrg,
|
||||||
usage,
|
usage,
|
||||||
|
lastFetchedAt,
|
||||||
status,
|
status,
|
||||||
error,
|
error,
|
||||||
|
keyValidationError,
|
||||||
|
canSaveKey,
|
||||||
saveKey,
|
saveKey,
|
||||||
refresh,
|
refresh,
|
||||||
clearKey,
|
clearKey,
|
||||||
} = useClaudeUsage();
|
} = useClaudeUsage();
|
||||||
|
|
||||||
const canSave = pendingKey.trim().startsWith("sk-ant-");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView
|
<SafeAreaView
|
||||||
className="flex-1 bg-neutral-50 dark:bg-neutral-950"
|
className="flex-1 bg-neutral-50 dark:bg-neutral-950"
|
||||||
@@ -49,7 +53,7 @@ export default function ClaudeTab() {
|
|||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={status === "loading"}
|
refreshing={status === "loading"}
|
||||||
onRefresh={refresh}
|
onRefresh={refresh}
|
||||||
tintColor="#d97706"
|
tintColor={COLORS.claude}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -57,9 +61,9 @@ export default function ClaudeTab() {
|
|||||||
<View className="flex-row items-center gap-x-3 py-5">
|
<View className="flex-row items-center gap-x-3 py-5">
|
||||||
<View
|
<View
|
||||||
className="w-9 h-9 rounded-xl items-center justify-center"
|
className="w-9 h-9 rounded-xl items-center justify-center"
|
||||||
style={{ backgroundColor: "#d9770622" }}
|
style={{ backgroundColor: `${COLORS.claude}22` }}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="psychology" size={18} color="#d97706" />
|
<MaterialIcons name="psychology" size={18} color={COLORS.claude} />
|
||||||
</View>
|
</View>
|
||||||
<View>
|
<View>
|
||||||
<Text className="text-xl font-bold text-neutral-900 dark:text-white tracking-tight">
|
<Text className="text-xl font-bold text-neutral-900 dark:text-white tracking-tight">
|
||||||
@@ -111,6 +115,11 @@ export default function ClaudeTab() {
|
|||||||
secureTextEntry
|
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-white dark:bg-neutral-800 mb-2"
|
className="border border-neutral-200 dark:border-neutral-700 rounded-xl px-3 py-3 text-sm text-neutral-900 dark:text-white bg-white dark:bg-neutral-800 mb-2"
|
||||||
/>
|
/>
|
||||||
|
{keyValidationError && (
|
||||||
|
<Text selectable className="mb-2 text-xs text-red-500">
|
||||||
|
{keyValidationError}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
<TextInput
|
<TextInput
|
||||||
value={pendingOrg}
|
value={pendingOrg}
|
||||||
onChangeText={setPendingOrg}
|
onChangeText={setPendingOrg}
|
||||||
@@ -122,13 +131,15 @@ export default function ClaudeTab() {
|
|||||||
/>
|
/>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={saveKey}
|
onPress={saveKey}
|
||||||
disabled={!canSave}
|
disabled={!canSaveKey}
|
||||||
className="py-3.5 px-4 rounded-xl items-center"
|
className="py-3.5 px-4 rounded-xl items-center"
|
||||||
style={{ backgroundColor: canSave ? "#d97706" : "#e5e5e5" }}
|
style={{
|
||||||
|
backgroundColor: canSaveKey ? COLORS.claude : COLORS.disabled,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
className="font-semibold text-sm"
|
className="font-semibold text-sm"
|
||||||
style={{ color: canSave ? "white" : "#a3a3a3" }}
|
style={{ color: canSaveKey ? "white" : COLORS.muted }}
|
||||||
>
|
>
|
||||||
Save Session Key
|
Save Session Key
|
||||||
</Text>
|
</Text>
|
||||||
@@ -162,11 +173,11 @@ export default function ClaudeTab() {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{status === "loading" && (
|
{status === "loading" && !usage && (
|
||||||
<ActivityIndicator color="#d97706" style={{ marginVertical: 24 }} />
|
<ActivityIndicator color={COLORS.claude} style={{ marginVertical: 24 }} />
|
||||||
)}
|
)}
|
||||||
{status === "error" && error && <ErrorMessage message={error} />}
|
{status === "error" && error && <ErrorMessage message={error} />}
|
||||||
{status === "success" && usage && (
|
{usage && (
|
||||||
<View>
|
<View>
|
||||||
<UsageStat
|
<UsageStat
|
||||||
label="5-hour window"
|
label="5-hour window"
|
||||||
@@ -198,6 +209,9 @@ export default function ClaudeTab() {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
<View className="mt-4 border-t border-neutral-100 pt-3 dark:border-neutral-800">
|
||||||
|
<LastUpdated timestamp={lastFetchedAt} />
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -206,3 +220,11 @@ export default function ClaudeTab() {
|
|||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function ClaudeTab() {
|
||||||
|
return (
|
||||||
|
<ScreenErrorBoundary screenName="Claude">
|
||||||
|
<ClaudeTabContent />
|
||||||
|
</ScreenErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,10 +11,22 @@ import { MaterialIcons } from "@expo/vector-icons";
|
|||||||
import { useCodexUsage } from "@/hooks/useCodexUsage";
|
import { useCodexUsage } from "@/hooks/useCodexUsage";
|
||||||
import { UsageStat } from "@/components/UsageStat";
|
import { UsageStat } from "@/components/UsageStat";
|
||||||
import { ErrorMessage } from "@/components/ErrorMessage";
|
import { ErrorMessage } from "@/components/ErrorMessage";
|
||||||
|
import { LastUpdated } from "@/components/LastUpdated";
|
||||||
|
import { ScreenErrorBoundary } from "@/components/ScreenErrorBoundary";
|
||||||
|
import { COLORS } from "@/lib/constants";
|
||||||
import { windowLabel } from "@/lib/timeUtils";
|
import { windowLabel } from "@/lib/timeUtils";
|
||||||
|
|
||||||
export default function CodexTab() {
|
function CodexTabContent() {
|
||||||
const { auth, usage, status, error, importAuthFile, refresh, clearAuth } =
|
const {
|
||||||
|
auth,
|
||||||
|
usage,
|
||||||
|
lastFetchedAt,
|
||||||
|
status,
|
||||||
|
error,
|
||||||
|
importAuthFile,
|
||||||
|
refresh,
|
||||||
|
clearAuth,
|
||||||
|
} =
|
||||||
useCodexUsage();
|
useCodexUsage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -29,7 +41,7 @@ export default function CodexTab() {
|
|||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={status === "loading"}
|
refreshing={status === "loading"}
|
||||||
onRefresh={refresh}
|
onRefresh={refresh}
|
||||||
tintColor="#10a37f"
|
tintColor={COLORS.codex}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -37,9 +49,9 @@ export default function CodexTab() {
|
|||||||
<View className="flex-row items-center gap-x-3 py-5">
|
<View className="flex-row items-center gap-x-3 py-5">
|
||||||
<View
|
<View
|
||||||
className="w-9 h-9 rounded-xl items-center justify-center"
|
className="w-9 h-9 rounded-xl items-center justify-center"
|
||||||
style={{ backgroundColor: "#10a37f22" }}
|
style={{ backgroundColor: `${COLORS.codex}22` }}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="auto-awesome" size={18} color="#10a37f" />
|
<MaterialIcons name="auto-awesome" size={18} color={COLORS.codex} />
|
||||||
</View>
|
</View>
|
||||||
<View>
|
<View>
|
||||||
<Text className="text-xl font-bold text-neutral-900 dark:text-white tracking-tight">
|
<Text className="text-xl font-bold text-neutral-900 dark:text-white tracking-tight">
|
||||||
@@ -84,7 +96,7 @@ export default function CodexTab() {
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={importAuthFile}
|
onPress={importAuthFile}
|
||||||
className="flex-row items-center justify-center gap-x-2 py-3.5 px-4 rounded-xl"
|
className="flex-row items-center justify-center gap-x-2 py-3.5 px-4 rounded-xl"
|
||||||
style={{ backgroundColor: "#10a37f" }}
|
style={{ backgroundColor: COLORS.codex }}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="upload-file" size={16} color="white" />
|
<MaterialIcons name="upload-file" size={16} color="white" />
|
||||||
<Text className="text-white font-semibold text-sm">
|
<Text className="text-white font-semibold text-sm">
|
||||||
@@ -112,11 +124,11 @@ export default function CodexTab() {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{status === "loading" && (
|
{status === "loading" && !usage && (
|
||||||
<ActivityIndicator color="#10a37f" style={{ marginVertical: 24 }} />
|
<ActivityIndicator color={COLORS.codex} style={{ marginVertical: 24 }} />
|
||||||
)}
|
)}
|
||||||
{status === "error" && error && <ErrorMessage message={error} />}
|
{status === "error" && error && <ErrorMessage message={error} />}
|
||||||
{status === "success" && usage && (
|
{usage && (
|
||||||
<View>
|
<View>
|
||||||
{/* Plan badge */}
|
{/* Plan badge */}
|
||||||
<View className="flex-row items-center mb-5">
|
<View className="flex-row items-center mb-5">
|
||||||
@@ -145,7 +157,7 @@ export default function CodexTab() {
|
|||||||
</Text>
|
</Text>
|
||||||
{usage.credits.unlimited ? (
|
{usage.credits.unlimited ? (
|
||||||
<View className="flex-row items-center gap-x-2">
|
<View className="flex-row items-center gap-x-2">
|
||||||
<MaterialIcons name="all-inclusive" size={16} color="#10a37f" />
|
<MaterialIcons name="all-inclusive" size={16} color={COLORS.codex} />
|
||||||
<Text className="text-sm font-semibold text-green-600 dark:text-green-400">
|
<Text className="text-sm font-semibold text-green-600 dark:text-green-400">
|
||||||
Unlimited
|
Unlimited
|
||||||
</Text>
|
</Text>
|
||||||
@@ -158,6 +170,9 @@ export default function CodexTab() {
|
|||||||
<Text className="text-sm text-neutral-400">No credits</Text>
|
<Text className="text-sm text-neutral-400">No credits</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
<View className="mt-4 border-t border-neutral-100 pt-3 dark:border-neutral-800">
|
||||||
|
<LastUpdated timestamp={lastFetchedAt} />
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -165,3 +180,11 @@ export default function CodexTab() {
|
|||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function CodexTab() {
|
||||||
|
return (
|
||||||
|
<ScreenErrorBoundary screenName="Codex">
|
||||||
|
<CodexTabContent />
|
||||||
|
</ScreenErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ import { useClaudeUsage } from "@/hooks/useClaudeUsage";
|
|||||||
import { onUsageDataLoaded } from "@/lib/notifications";
|
import { onUsageDataLoaded } from "@/lib/notifications";
|
||||||
import { ProgressBar } from "@/components/ProgressBar";
|
import { ProgressBar } from "@/components/ProgressBar";
|
||||||
import { ResetCountdown } from "@/components/ResetCountdown";
|
import { ResetCountdown } from "@/components/ResetCountdown";
|
||||||
|
import { LastUpdated } from "@/components/LastUpdated";
|
||||||
|
import { ErrorMessage } from "@/components/ErrorMessage";
|
||||||
|
import { ScreenErrorBoundary } from "@/components/ScreenErrorBoundary";
|
||||||
|
import { COLORS, getUsageColor } from "@/lib/constants";
|
||||||
import { windowLabel } from "@/lib/timeUtils";
|
import { windowLabel } from "@/lib/timeUtils";
|
||||||
|
|
||||||
function UsageRow({
|
function UsageRow({
|
||||||
@@ -28,8 +32,7 @@ function UsageRow({
|
|||||||
resetAtSeconds?: number;
|
resetAtSeconds?: number;
|
||||||
resetAtISO?: string;
|
resetAtISO?: string;
|
||||||
}) {
|
}) {
|
||||||
const color =
|
const color = getUsageColor(percent);
|
||||||
percent >= 90 ? "#ef4444" : percent >= 70 ? "#f59e0b" : "#10a37f";
|
|
||||||
return (
|
return (
|
||||||
<View className="mb-4">
|
<View className="mb-4">
|
||||||
<View className="flex-row justify-between items-baseline mb-1.5">
|
<View className="flex-row justify-between items-baseline mb-1.5">
|
||||||
@@ -65,33 +68,33 @@ function SetupPrompt({ label }: { label: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DashboardTab() {
|
function DashboardTabContent() {
|
||||||
const codex = useCodexUsage();
|
const codex = useCodexUsage();
|
||||||
const claude = useClaudeUsage();
|
const claude = useClaudeUsage();
|
||||||
|
|
||||||
// Fire daily digest reschedule + threshold check whenever fresh data arrives
|
// Fire daily digest reschedule + threshold check whenever fresh data arrives
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (codex.status === "success" || claude.status === "success") {
|
if (codex.usage || claude.usage) {
|
||||||
void onUsageDataLoaded(
|
void onUsageDataLoaded(
|
||||||
claude.status === "success" ? claude.usage : null,
|
claude.usage,
|
||||||
codex.status === "success" ? codex.usage : null
|
codex.usage
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [codex.status, claude.status]);
|
}, [codex.lastFetchedAt, claude.lastFetchedAt]);
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
codex.reload();
|
void codex.reloadCredentials();
|
||||||
claude.reload();
|
void claude.reloadCredentials();
|
||||||
}, [])
|
}, [codex.reloadCredentials, claude.reloadCredentials])
|
||||||
);
|
);
|
||||||
|
|
||||||
const isRefreshing =
|
const isRefreshing =
|
||||||
codex.status === "loading" || claude.status === "loading";
|
codex.status === "loading" || claude.status === "loading";
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
codex.refresh();
|
void codex.refresh();
|
||||||
claude.refresh();
|
void claude.refresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
const connectedCount = [codex.auth, claude.auth].filter(Boolean).length;
|
const connectedCount = [codex.auth, claude.auth].filter(Boolean).length;
|
||||||
@@ -108,7 +111,7 @@ export default function DashboardTab() {
|
|||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={isRefreshing}
|
refreshing={isRefreshing}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
tintColor="#10a37f"
|
tintColor={COLORS.codex}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -135,21 +138,21 @@ export default function DashboardTab() {
|
|||||||
onPress={handleRefresh}
|
onPress={handleRefresh}
|
||||||
className="w-9 h-9 rounded-xl bg-neutral-100 dark:bg-neutral-800 items-center justify-center"
|
className="w-9 h-9 rounded-xl bg-neutral-100 dark:bg-neutral-800 items-center justify-center"
|
||||||
>
|
>
|
||||||
<MaterialIcons name="refresh" size={18} color="#10a37f" />
|
<MaterialIcons name="refresh" size={18} color={COLORS.codex} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Codex card */}
|
{/* Codex card */}
|
||||||
<View className="rounded-2xl bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 mb-4 overflow-hidden">
|
<View className="rounded-2xl bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 mb-4 overflow-hidden">
|
||||||
<View style={{ height: 3, backgroundColor: "#10a37f" }} />
|
<View style={{ height: 3, backgroundColor: COLORS.codex }} />
|
||||||
<View className="p-4">
|
<View className="p-4">
|
||||||
<View className="flex-row items-center justify-between mb-4">
|
<View className="flex-row items-center justify-between mb-4">
|
||||||
<View className="flex-row items-center gap-x-2.5">
|
<View className="flex-row items-center gap-x-2.5">
|
||||||
<View
|
<View
|
||||||
className="w-8 h-8 rounded-xl items-center justify-center"
|
className="w-8 h-8 rounded-xl items-center justify-center"
|
||||||
style={{ backgroundColor: "#10a37f18" }}
|
style={{ backgroundColor: `${COLORS.codex}18` }}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="auto-awesome" size={16} color="#10a37f" />
|
<MaterialIcons name="auto-awesome" size={16} color={COLORS.codex} />
|
||||||
</View>
|
</View>
|
||||||
<Text className="text-sm font-semibold text-neutral-900 dark:text-white">
|
<Text className="text-sm font-semibold text-neutral-900 dark:text-white">
|
||||||
Codex CLI
|
Codex CLI
|
||||||
@@ -175,21 +178,16 @@ export default function DashboardTab() {
|
|||||||
{!codex.auth && (
|
{!codex.auth && (
|
||||||
<SetupPrompt label="Import auth.json to see your Codex rate limits." />
|
<SetupPrompt label="Import auth.json to see your Codex rate limits." />
|
||||||
)}
|
)}
|
||||||
{codex.auth && codex.status === "loading" && (
|
{codex.auth && codex.status === "loading" && !codex.usage && (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
color="#10a37f"
|
color={COLORS.codex}
|
||||||
style={{ marginVertical: 16 }}
|
style={{ marginVertical: 16 }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{codex.auth && codex.status === "error" && (
|
{codex.auth && codex.status === "error" && (
|
||||||
<View className="flex-row items-center gap-x-2 py-1">
|
<ErrorMessage message={codex.error} />
|
||||||
<MaterialIcons name="error-outline" size={16} color="#ef4444" />
|
|
||||||
<Text className="text-sm text-red-500 dark:text-red-400">
|
|
||||||
Failed to fetch usage
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
)}
|
||||||
{codex.status === "success" && codex.usage && (
|
{codex.usage && (
|
||||||
<View>
|
<View>
|
||||||
<UsageRow
|
<UsageRow
|
||||||
label={`${windowLabel(codex.usage.rate_limit.primary_window.limit_window_seconds)} window`}
|
label={`${windowLabel(codex.usage.rate_limit.primary_window.limit_window_seconds)} window`}
|
||||||
@@ -220,13 +218,16 @@ export default function DashboardTab() {
|
|||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name="all-inclusive"
|
name="all-inclusive"
|
||||||
size={14}
|
size={14}
|
||||||
color="#10a37f"
|
color={COLORS.codex}
|
||||||
/>
|
/>
|
||||||
<Text className="text-xs text-green-600 dark:text-green-400">
|
<Text className="text-xs text-green-600 dark:text-green-400">
|
||||||
Unlimited credits
|
Unlimited credits
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
<View className="mt-3 border-t border-neutral-100 pt-3 dark:border-neutral-800">
|
||||||
|
<LastUpdated timestamp={codex.lastFetchedAt} />
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -234,15 +235,15 @@ export default function DashboardTab() {
|
|||||||
|
|
||||||
{/* Claude card */}
|
{/* Claude card */}
|
||||||
<View className="rounded-2xl bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 mb-4 overflow-hidden">
|
<View className="rounded-2xl bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 mb-4 overflow-hidden">
|
||||||
<View style={{ height: 3, backgroundColor: "#d97706" }} />
|
<View style={{ height: 3, backgroundColor: COLORS.claude }} />
|
||||||
<View className="p-4">
|
<View className="p-4">
|
||||||
<View className="flex-row items-center justify-between mb-4">
|
<View className="flex-row items-center justify-between mb-4">
|
||||||
<View className="flex-row items-center gap-x-2.5">
|
<View className="flex-row items-center gap-x-2.5">
|
||||||
<View
|
<View
|
||||||
className="w-8 h-8 rounded-xl items-center justify-center"
|
className="w-8 h-8 rounded-xl items-center justify-center"
|
||||||
style={{ backgroundColor: "#d9770618" }}
|
style={{ backgroundColor: `${COLORS.claude}18` }}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="psychology" size={16} color="#d97706" />
|
<MaterialIcons name="psychology" size={16} color={COLORS.claude} />
|
||||||
</View>
|
</View>
|
||||||
<Text className="text-sm font-semibold text-neutral-900 dark:text-white">
|
<Text className="text-sm font-semibold text-neutral-900 dark:text-white">
|
||||||
Claude.ai
|
Claude.ai
|
||||||
@@ -259,21 +260,16 @@ export default function DashboardTab() {
|
|||||||
{!claude.auth && (
|
{!claude.auth && (
|
||||||
<SetupPrompt label="Add your session key to see Claude usage windows." />
|
<SetupPrompt label="Add your session key to see Claude usage windows." />
|
||||||
)}
|
)}
|
||||||
{claude.auth && claude.status === "loading" && (
|
{claude.auth && claude.status === "loading" && !claude.usage && (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
color="#d97706"
|
color={COLORS.claude}
|
||||||
style={{ marginVertical: 16 }}
|
style={{ marginVertical: 16 }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{claude.auth && claude.status === "error" && (
|
{claude.auth && claude.status === "error" && (
|
||||||
<View className="flex-row items-center gap-x-2 py-1">
|
<ErrorMessage message={claude.error} />
|
||||||
<MaterialIcons name="error-outline" size={16} color="#ef4444" />
|
|
||||||
<Text className="text-sm text-red-500 dark:text-red-400">
|
|
||||||
Failed to fetch usage
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
)}
|
||||||
{claude.status === "success" && claude.usage && (
|
{claude.usage && (
|
||||||
<View>
|
<View>
|
||||||
<UsageRow
|
<UsageRow
|
||||||
label="5-hour window"
|
label="5-hour window"
|
||||||
@@ -305,6 +301,9 @@ export default function DashboardTab() {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
<View className="mt-3 border-t border-neutral-100 pt-3 dark:border-neutral-800">
|
||||||
|
<LastUpdated timestamp={claude.lastFetchedAt} />
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -313,3 +312,11 @@ export default function DashboardTab() {
|
|||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function DashboardTab() {
|
||||||
|
return (
|
||||||
|
<ScreenErrorBoundary screenName="Dashboard">
|
||||||
|
<DashboardTabContent />
|
||||||
|
</ScreenErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
TextInput,
|
TextInput,
|
||||||
Switch,
|
Switch,
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
Platform,
|
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { SafeAreaView } from "react-native-safe-area-context";
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
import { router, useFocusEffect } from "expo-router";
|
import { router, useFocusEffect } from "expo-router";
|
||||||
@@ -26,22 +25,16 @@ import {
|
|||||||
import { pickAndReadCodexAuth } from "@/lib/fileReader";
|
import { pickAndReadCodexAuth } from "@/lib/fileReader";
|
||||||
import { resetOnboarding } from "@/lib/setupState";
|
import { resetOnboarding } from "@/lib/setupState";
|
||||||
import { useNotificationSettings } from "@/hooks/useNotificationSettings";
|
import { useNotificationSettings } from "@/hooks/useNotificationSettings";
|
||||||
|
import {
|
||||||
|
isClaudeSessionKeyValid,
|
||||||
|
validateClaudeSessionKey,
|
||||||
|
} from "@/lib/claudeCredentials";
|
||||||
|
import { COLORS } from "@/lib/constants";
|
||||||
|
import { formatTime, parseTimeInput } from "@/lib/timeUtils";
|
||||||
|
import { ScreenErrorBoundary } from "@/components/ScreenErrorBoundary";
|
||||||
import type { CodexAuth } from "@/types/codex";
|
import type { CodexAuth } from "@/types/codex";
|
||||||
import Constants from "expo-constants";
|
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({
|
function SettingRow({
|
||||||
icon,
|
icon,
|
||||||
label,
|
label,
|
||||||
@@ -120,7 +113,7 @@ function Divider() {
|
|||||||
return <View className="ml-16 mr-0 h-px bg-neutral-100 dark:bg-neutral-800" />;
|
return <View className="ml-16 mr-0 h-px bg-neutral-100 dark:bg-neutral-800" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SettingsTab() {
|
function SettingsTabContent() {
|
||||||
const [codexAuth, setCodexAuth] = useState<CodexAuth | null>(null);
|
const [codexAuth, setCodexAuth] = useState<CodexAuth | null>(null);
|
||||||
const [claudeKey, setClaudeKey] = useState<string | null>(null);
|
const [claudeKey, setClaudeKey] = useState<string | null>(null);
|
||||||
const [pendingKey, setPendingKey] = useState("");
|
const [pendingKey, setPendingKey] = useState("");
|
||||||
@@ -129,6 +122,7 @@ export default function SettingsTab() {
|
|||||||
|
|
||||||
const notif = useNotificationSettings();
|
const notif = useNotificationSettings();
|
||||||
const [timeInput, setTimeInput] = useState("");
|
const [timeInput, setTimeInput] = useState("");
|
||||||
|
const [timeError, setTimeError] = useState<string | null>(null);
|
||||||
const [thresholdInput, setThresholdInput] = useState("");
|
const [thresholdInput, setThresholdInput] = useState("");
|
||||||
|
|
||||||
// Sync local text inputs when settings load
|
// Sync local text inputs when settings load
|
||||||
@@ -173,8 +167,9 @@ export default function SettingsTab() {
|
|||||||
|
|
||||||
const handleSaveClaude = async () => {
|
const handleSaveClaude = async () => {
|
||||||
const trimmed = pendingKey.trim();
|
const trimmed = pendingKey.trim();
|
||||||
if (!trimmed.startsWith("sk-ant-")) {
|
const validationError = validateClaudeSessionKey(trimmed);
|
||||||
setClaudeError("Key must start with sk-ant-");
|
if (validationError) {
|
||||||
|
setClaudeError(validationError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const orgTrimmed = pendingOrg.trim() || undefined;
|
const orgTrimmed = pendingOrg.trim() || undefined;
|
||||||
@@ -249,9 +244,10 @@ export default function SettingsTab() {
|
|||||||
const parsed = parseTimeInput(timeInput);
|
const parsed = parseTimeInput(timeInput);
|
||||||
if (parsed) {
|
if (parsed) {
|
||||||
void notif.update({ dailyHour: parsed.hour, dailyMinute: parsed.minute });
|
void notif.update({ dailyHour: parsed.hour, dailyMinute: parsed.minute });
|
||||||
|
setTimeInput(formatTime(parsed.hour, parsed.minute));
|
||||||
|
setTimeError(null);
|
||||||
} else {
|
} else {
|
||||||
// Reset to last valid value
|
setTimeError("Use HH:MM format, from 00:00 to 23:59.");
|
||||||
setTimeInput(formatTime(notif.settings.dailyHour, notif.settings.dailyMinute));
|
|
||||||
}
|
}
|
||||||
}, [timeInput, notif]);
|
}, [timeInput, notif]);
|
||||||
|
|
||||||
@@ -264,7 +260,9 @@ export default function SettingsTab() {
|
|||||||
}
|
}
|
||||||
}, [thresholdInput, notif]);
|
}, [thresholdInput, notif]);
|
||||||
|
|
||||||
const canSaveClaude = pendingKey.trim().startsWith("sk-ant-");
|
const canSaveClaude = isClaudeSessionKeyValid(pendingKey);
|
||||||
|
const inlineClaudeError =
|
||||||
|
claudeError ?? (pendingKey ? validateClaudeSessionKey(pendingKey) : null);
|
||||||
const appVersion = Constants.expoConfig?.version ?? "1.0.0";
|
const appVersion = Constants.expoConfig?.version ?? "1.0.0";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -274,7 +272,7 @@ export default function SettingsTab() {
|
|||||||
>
|
>
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
behavior={process.env.EXPO_OS === "ios" ? "padding" : "height"}
|
||||||
>
|
>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
@@ -341,9 +339,9 @@ export default function SettingsTab() {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<View className="p-4">
|
<View className="p-4">
|
||||||
{claudeError && (
|
{inlineClaudeError && (
|
||||||
<Text className="text-xs text-red-500 mb-2">
|
<Text selectable className="text-xs text-red-500 mb-2">
|
||||||
{claudeError}
|
{inlineClaudeError}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -373,12 +371,14 @@ export default function SettingsTab() {
|
|||||||
disabled={!canSaveClaude}
|
disabled={!canSaveClaude}
|
||||||
className="py-3 px-4 rounded-xl items-center"
|
className="py-3 px-4 rounded-xl items-center"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: canSaveClaude ? "#d97706" : "#e5e5e5",
|
backgroundColor: canSaveClaude
|
||||||
|
? COLORS.claude
|
||||||
|
: COLORS.disabled,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
className="font-semibold text-sm"
|
className="font-semibold text-sm"
|
||||||
style={{ color: canSaveClaude ? "white" : "#a3a3a3" }}
|
style={{ color: canSaveClaude ? "white" : COLORS.muted }}
|
||||||
>
|
>
|
||||||
Save Session Key
|
Save Session Key
|
||||||
</Text>
|
</Text>
|
||||||
@@ -446,7 +446,7 @@ export default function SettingsTab() {
|
|||||||
<Switch
|
<Switch
|
||||||
value={notif.settings.dailyEnabled}
|
value={notif.settings.dailyEnabled}
|
||||||
onValueChange={(v) => void notif.update({ dailyEnabled: v })}
|
onValueChange={(v) => void notif.update({ dailyEnabled: v })}
|
||||||
trackColor={{ false: "#e5e5e5", true: "#10a37f" }}
|
trackColor={{ false: COLORS.disabled, true: COLORS.codex }}
|
||||||
thumbColor="white"
|
thumbColor="white"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -454,25 +454,35 @@ export default function SettingsTab() {
|
|||||||
{notif.settings.dailyEnabled && (
|
{notif.settings.dailyEnabled && (
|
||||||
<>
|
<>
|
||||||
<Divider />
|
<Divider />
|
||||||
<View className="flex-row items-center gap-x-3 py-3 px-4">
|
<View className="px-4 py-3">
|
||||||
<View className="w-8 items-center">
|
<View className="flex-row items-center gap-x-3">
|
||||||
<MaterialIcons name="schedule" size={20} color="#737373" />
|
<View className="w-8 items-center">
|
||||||
|
<MaterialIcons name="schedule" size={20} color="#737373" />
|
||||||
|
</View>
|
||||||
|
<Text className="flex-1 text-sm font-medium text-neutral-800 dark:text-neutral-100">
|
||||||
|
Time
|
||||||
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
value={timeInput}
|
||||||
|
onChangeText={(value) => {
|
||||||
|
setTimeInput(value);
|
||||||
|
setTimeError(null);
|
||||||
|
}}
|
||||||
|
onBlur={handleTimeBlur}
|
||||||
|
placeholder="09:00"
|
||||||
|
placeholderTextColor={COLORS.muted}
|
||||||
|
keyboardType="numbers-and-punctuation"
|
||||||
|
returnKeyType="done"
|
||||||
|
maxLength={5}
|
||||||
|
className="text-sm font-mono text-neutral-600 dark:text-neutral-300 text-right"
|
||||||
|
style={{ minWidth: 52 }}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
<Text className="flex-1 text-sm font-medium text-neutral-800 dark:text-neutral-100">
|
{timeError && (
|
||||||
Time
|
<Text selectable className="ml-11 mt-1.5 text-xs text-red-500">
|
||||||
</Text>
|
{timeError}
|
||||||
<TextInput
|
</Text>
|
||||||
value={timeInput}
|
)}
|
||||||
onChangeText={setTimeInput}
|
|
||||||
onBlur={handleTimeBlur}
|
|
||||||
placeholder="09:00"
|
|
||||||
placeholderTextColor="#a3a3a3"
|
|
||||||
keyboardType="numbers-and-punctuation"
|
|
||||||
returnKeyType="done"
|
|
||||||
maxLength={5}
|
|
||||||
className="text-sm font-mono text-neutral-600 dark:text-neutral-300 text-right"
|
|
||||||
style={{ minWidth: 52 }}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -495,7 +505,7 @@ export default function SettingsTab() {
|
|||||||
<Switch
|
<Switch
|
||||||
value={notif.settings.thresholdEnabled}
|
value={notif.settings.thresholdEnabled}
|
||||||
onValueChange={(v) => void notif.update({ thresholdEnabled: v })}
|
onValueChange={(v) => void notif.update({ thresholdEnabled: v })}
|
||||||
trackColor={{ false: "#e5e5e5", true: "#d97706" }}
|
trackColor={{ false: COLORS.disabled, true: COLORS.claude }}
|
||||||
thumbColor="white"
|
thumbColor="white"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -554,3 +564,11 @@ export default function SettingsTab() {
|
|||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function SettingsTab() {
|
||||||
|
return (
|
||||||
|
<ScreenErrorBoundary screenName="Settings">
|
||||||
|
<SettingsTabContent />
|
||||||
|
</ScreenErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect } from "react";
|
|||||||
import { View, ActivityIndicator } from "react-native";
|
import { View, ActivityIndicator } from "react-native";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import { hasCompletedOnboarding } from "@/lib/setupState";
|
import { hasCompletedOnboarding } from "@/lib/setupState";
|
||||||
|
import { COLORS } from "@/lib/constants";
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -12,7 +13,7 @@ export default function Index() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="flex-1 bg-neutral-950 items-center justify-center">
|
<View className="flex-1 bg-neutral-950 items-center justify-center">
|
||||||
<ActivityIndicator color="#10a37f" size="large" />
|
<ActivityIndicator color={COLORS.codex} size="large" />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
TextInput,
|
TextInput,
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
Platform,
|
|
||||||
ScrollView,
|
ScrollView,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { SafeAreaView } from "react-native-safe-area-context";
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
@@ -18,6 +17,11 @@ import {
|
|||||||
loadClaudeSessionKey,
|
loadClaudeSessionKey,
|
||||||
loadClaudeLastActiveOrg,
|
loadClaudeLastActiveOrg,
|
||||||
} from "@/lib/storage";
|
} from "@/lib/storage";
|
||||||
|
import {
|
||||||
|
isClaudeSessionKeyValid,
|
||||||
|
validateClaudeSessionKey,
|
||||||
|
} from "@/lib/claudeCredentials";
|
||||||
|
import { COLORS } from "@/lib/constants";
|
||||||
|
|
||||||
export default function OnboardingClaudeScreen() {
|
export default function OnboardingClaudeScreen() {
|
||||||
const [savedKey, setSavedKey] = useState<string | null>(null);
|
const [savedKey, setSavedKey] = useState<string | null>(null);
|
||||||
@@ -35,12 +39,15 @@ export default function OnboardingClaudeScreen() {
|
|||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const canSave = pendingKey.trim().startsWith("sk-ant-");
|
const canSave = isClaudeSessionKeyValid(pendingKey);
|
||||||
|
const inlineError =
|
||||||
|
error ?? (pendingKey ? validateClaudeSessionKey(pendingKey) : null);
|
||||||
|
|
||||||
const handleSave = useCallback(async () => {
|
const handleSave = useCallback(async () => {
|
||||||
const trimmed = pendingKey.trim();
|
const trimmed = pendingKey.trim();
|
||||||
if (!trimmed.startsWith("sk-ant-")) {
|
const validationError = validateClaudeSessionKey(trimmed);
|
||||||
setError("Session key must start with sk-ant-");
|
if (validationError) {
|
||||||
|
setError(validationError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const orgTrimmed = pendingOrg.trim() || undefined;
|
const orgTrimmed = pendingOrg.trim() || undefined;
|
||||||
@@ -58,7 +65,7 @@ export default function OnboardingClaudeScreen() {
|
|||||||
<SafeAreaView className="flex-1 bg-neutral-950">
|
<SafeAreaView className="flex-1 bg-neutral-950">
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
behavior={process.env.EXPO_OS === "ios" ? "padding" : "height"}
|
||||||
>
|
>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
@@ -77,14 +84,14 @@ export default function OnboardingClaudeScreen() {
|
|||||||
<View className="flex-row items-center gap-x-2">
|
<View className="flex-row items-center gap-x-2">
|
||||||
<View
|
<View
|
||||||
className="w-6 h-6 rounded-full items-center justify-center"
|
className="w-6 h-6 rounded-full items-center justify-center"
|
||||||
style={{ backgroundColor: "#10a37f" }}
|
style={{ backgroundColor: COLORS.codex }}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="check" size={14} color="white" />
|
<MaterialIcons name="check" size={14} color="white" />
|
||||||
</View>
|
</View>
|
||||||
<View className="w-12 h-0.5" style={{ backgroundColor: "#d97706" }} />
|
<View className="w-12 h-0.5" style={{ backgroundColor: COLORS.claude }} />
|
||||||
<View
|
<View
|
||||||
className="w-6 h-6 rounded-full items-center justify-center"
|
className="w-6 h-6 rounded-full items-center justify-center"
|
||||||
style={{ backgroundColor: "#d97706" }}
|
style={{ backgroundColor: COLORS.claude }}
|
||||||
>
|
>
|
||||||
<Text className="text-white text-xs font-bold">2</Text>
|
<Text className="text-white text-xs font-bold">2</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -96,9 +103,9 @@ export default function OnboardingClaudeScreen() {
|
|||||||
<View>
|
<View>
|
||||||
<View
|
<View
|
||||||
className="w-16 h-16 rounded-2xl items-center justify-center mb-6"
|
className="w-16 h-16 rounded-2xl items-center justify-center mb-6"
|
||||||
style={{ backgroundColor: "#d9770622" }}
|
style={{ backgroundColor: `${COLORS.claude}22` }}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="psychology" size={32} color="#d97706" />
|
<MaterialIcons name="psychology" size={32} color={COLORS.claude} />
|
||||||
</View>
|
</View>
|
||||||
<Text className="text-3xl font-bold text-white mb-3">
|
<Text className="text-3xl font-bold text-white mb-3">
|
||||||
Connect Claude.ai
|
Connect Claude.ai
|
||||||
@@ -112,9 +119,9 @@ export default function OnboardingClaudeScreen() {
|
|||||||
<View className="flex-row items-center gap-x-3 p-4">
|
<View className="flex-row items-center gap-x-3 p-4">
|
||||||
<View
|
<View
|
||||||
className="w-10 h-10 rounded-xl items-center justify-center"
|
className="w-10 h-10 rounded-xl items-center justify-center"
|
||||||
style={{ backgroundColor: "#d9770622" }}
|
style={{ backgroundColor: `${COLORS.claude}22` }}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="check-circle" size={22} color="#d97706" />
|
<MaterialIcons name="check-circle" size={22} color={COLORS.claude} />
|
||||||
</View>
|
</View>
|
||||||
<View className="flex-1">
|
<View className="flex-1">
|
||||||
<Text className="text-white font-semibold">Connected</Text>
|
<Text className="text-white font-semibold">Connected</Text>
|
||||||
@@ -143,9 +150,9 @@ export default function OnboardingClaudeScreen() {
|
|||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<View>
|
<View>
|
||||||
{error && (
|
{inlineError && (
|
||||||
<View className="bg-red-950 border border-red-900 rounded-xl px-4 py-3 mb-4">
|
<View className="bg-red-950 border border-red-900 rounded-xl px-4 py-3 mb-4">
|
||||||
<Text className="text-red-400 text-sm">{error}</Text>
|
<Text selectable className="text-red-400 text-sm">{inlineError}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -174,7 +181,9 @@ export default function OnboardingClaudeScreen() {
|
|||||||
onPress={handleSave}
|
onPress={handleSave}
|
||||||
disabled={!canSave}
|
disabled={!canSave}
|
||||||
className="rounded-2xl py-4 items-center mb-4"
|
className="rounded-2xl py-4 items-center mb-4"
|
||||||
style={{ backgroundColor: canSave ? "#d97706" : "#1a1a1a" }}
|
style={{
|
||||||
|
backgroundColor: canSave ? COLORS.claude : "#1a1a1a",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
className="font-bold text-base"
|
className="font-bold text-base"
|
||||||
@@ -201,7 +210,9 @@ export default function OnboardingClaudeScreen() {
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => router.push("/onboarding/done")}
|
onPress={() => router.push("/onboarding/done")}
|
||||||
className="rounded-2xl py-4 items-center"
|
className="rounded-2xl py-4 items-center"
|
||||||
style={{ backgroundColor: savedKey ? "#d97706" : "#1a1a1a" }}
|
style={{
|
||||||
|
backgroundColor: savedKey ? COLORS.claude : "#1a1a1a",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
className="font-bold text-base"
|
className="font-bold text-base"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { MaterialIcons } from "@expo/vector-icons";
|
|||||||
import { pickAndReadCodexAuth } from "@/lib/fileReader";
|
import { pickAndReadCodexAuth } from "@/lib/fileReader";
|
||||||
import { saveCodexAuth, loadCodexAuth } from "@/lib/storage";
|
import { saveCodexAuth, loadCodexAuth } from "@/lib/storage";
|
||||||
import type { CodexAuth } from "@/types/codex";
|
import type { CodexAuth } from "@/types/codex";
|
||||||
|
import { COLORS } from "@/lib/constants";
|
||||||
|
|
||||||
export default function OnboardingCodexScreen() {
|
export default function OnboardingCodexScreen() {
|
||||||
const [auth, setAuth] = useState<CodexAuth | null>(null);
|
const [auth, setAuth] = useState<CodexAuth | null>(null);
|
||||||
@@ -45,7 +46,7 @@ export default function OnboardingCodexScreen() {
|
|||||||
<MaterialIcons name="arrow-back" size={18} color="#a3a3a3" />
|
<MaterialIcons name="arrow-back" size={18} color="#a3a3a3" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View className="flex-row items-center gap-x-2">
|
<View className="flex-row items-center gap-x-2">
|
||||||
<View className="w-6 h-6 rounded-full items-center justify-center" style={{ backgroundColor: "#10a37f" }}>
|
<View className="w-6 h-6 rounded-full items-center justify-center" style={{ backgroundColor: COLORS.codex }}>
|
||||||
<Text className="text-white text-xs font-bold">1</Text>
|
<Text className="text-white text-xs font-bold">1</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="w-12 h-0.5 bg-neutral-800" />
|
<View className="w-12 h-0.5 bg-neutral-800" />
|
||||||
@@ -60,9 +61,9 @@ export default function OnboardingCodexScreen() {
|
|||||||
<View>
|
<View>
|
||||||
<View
|
<View
|
||||||
className="w-16 h-16 rounded-2xl items-center justify-center mb-6"
|
className="w-16 h-16 rounded-2xl items-center justify-center mb-6"
|
||||||
style={{ backgroundColor: "#10a37f22" }}
|
style={{ backgroundColor: `${COLORS.codex}22` }}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="auto-awesome" size={32} color="#10a37f" />
|
<MaterialIcons name="auto-awesome" size={32} color={COLORS.codex} />
|
||||||
</View>
|
</View>
|
||||||
<Text className="text-3xl font-bold text-white mb-3">
|
<Text className="text-3xl font-bold text-white mb-3">
|
||||||
Connect Codex CLI
|
Connect Codex CLI
|
||||||
@@ -80,9 +81,9 @@ export default function OnboardingCodexScreen() {
|
|||||||
<View className="flex-row items-center gap-x-3 bg-neutral-900 rounded-2xl p-4 border border-neutral-800 mb-4">
|
<View className="flex-row items-center gap-x-3 bg-neutral-900 rounded-2xl p-4 border border-neutral-800 mb-4">
|
||||||
<View
|
<View
|
||||||
className="w-10 h-10 rounded-xl items-center justify-center"
|
className="w-10 h-10 rounded-xl items-center justify-center"
|
||||||
style={{ backgroundColor: "#10a37f22" }}
|
style={{ backgroundColor: `${COLORS.codex}22` }}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="check-circle" size={22} color="#10a37f" />
|
<MaterialIcons name="check-circle" size={22} color={COLORS.codex} />
|
||||||
</View>
|
</View>
|
||||||
<View className="flex-1">
|
<View className="flex-1">
|
||||||
<Text className="text-white font-semibold">Connected</Text>
|
<Text className="text-white font-semibold">Connected</Text>
|
||||||
@@ -107,10 +108,10 @@ export default function OnboardingCodexScreen() {
|
|||||||
onPress={importFile}
|
onPress={importFile}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="rounded-2xl py-4 items-center mb-3"
|
className="rounded-2xl py-4 items-center mb-3"
|
||||||
style={{ backgroundColor: loading ? "#1a1a1a" : "#10a37f" }}
|
style={{ backgroundColor: loading ? "#1a1a1a" : COLORS.codex }}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<ActivityIndicator color="#10a37f" />
|
<ActivityIndicator color={COLORS.codex} />
|
||||||
) : (
|
) : (
|
||||||
<View className="flex-row items-center gap-x-2">
|
<View className="flex-row items-center gap-x-2">
|
||||||
<MaterialIcons name="upload-file" size={18} color="white" />
|
<MaterialIcons name="upload-file" size={18} color="white" />
|
||||||
@@ -132,7 +133,7 @@ export default function OnboardingCodexScreen() {
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => router.push("/onboarding/claude")}
|
onPress={() => router.push("/onboarding/claude")}
|
||||||
className="rounded-2xl py-4 items-center"
|
className="rounded-2xl py-4 items-center"
|
||||||
style={{ backgroundColor: auth ? "#10a37f" : "#1a1a1a" }}
|
style={{ backgroundColor: auth ? COLORS.codex : "#1a1a1a" }}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
className="font-bold text-base"
|
className="font-bold text-base"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { router } from "expo-router";
|
|||||||
import { MaterialIcons } from "@expo/vector-icons";
|
import { MaterialIcons } from "@expo/vector-icons";
|
||||||
import { markOnboardingComplete } from "@/lib/setupState";
|
import { markOnboardingComplete } from "@/lib/setupState";
|
||||||
import { loadCodexAuth, loadClaudeSessionKey } from "@/lib/storage";
|
import { loadCodexAuth, loadClaudeSessionKey } from "@/lib/storage";
|
||||||
|
import { COLORS } from "@/lib/constants";
|
||||||
|
|
||||||
export default function OnboardingDoneScreen() {
|
export default function OnboardingDoneScreen() {
|
||||||
const [codexConfigured, setCodexConfigured] = useState(false);
|
const [codexConfigured, setCodexConfigured] = useState(false);
|
||||||
@@ -32,12 +33,12 @@ export default function OnboardingDoneScreen() {
|
|||||||
{/* Icon */}
|
{/* Icon */}
|
||||||
<View
|
<View
|
||||||
className="w-20 h-20 rounded-3xl items-center justify-center mb-8"
|
className="w-20 h-20 rounded-3xl items-center justify-center mb-8"
|
||||||
style={{ backgroundColor: "#10a37f22" }}
|
style={{ backgroundColor: `${COLORS.codex}22` }}
|
||||||
>
|
>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name={noneConfigured ? "info-outline" : "check-circle"}
|
name={noneConfigured ? "info-outline" : "check-circle"}
|
||||||
size={40}
|
size={40}
|
||||||
color={noneConfigured ? "#a3a3a3" : "#10a37f"}
|
color={noneConfigured ? COLORS.muted : COLORS.codex}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -57,13 +58,15 @@ export default function OnboardingDoneScreen() {
|
|||||||
<View
|
<View
|
||||||
className="w-10 h-10 rounded-xl items-center justify-center"
|
className="w-10 h-10 rounded-xl items-center justify-center"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: codexConfigured ? "#10a37f22" : "#27272a",
|
backgroundColor: codexConfigured
|
||||||
|
? `${COLORS.codex}22`
|
||||||
|
: "#27272a",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name={codexConfigured ? "check-circle" : "radio-button-unchecked"}
|
name={codexConfigured ? "check-circle" : "radio-button-unchecked"}
|
||||||
size={22}
|
size={22}
|
||||||
color={codexConfigured ? "#10a37f" : "#525252"}
|
color={codexConfigured ? COLORS.codex : "#525252"}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View className="flex-1">
|
<View className="flex-1">
|
||||||
@@ -83,13 +86,15 @@ export default function OnboardingDoneScreen() {
|
|||||||
<View
|
<View
|
||||||
className="w-10 h-10 rounded-xl items-center justify-center"
|
className="w-10 h-10 rounded-xl items-center justify-center"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: claudeConfigured ? "#d9770622" : "#27272a",
|
backgroundColor: claudeConfigured
|
||||||
|
? `${COLORS.claude}22`
|
||||||
|
: "#27272a",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name={claudeConfigured ? "check-circle" : "radio-button-unchecked"}
|
name={claudeConfigured ? "check-circle" : "radio-button-unchecked"}
|
||||||
size={22}
|
size={22}
|
||||||
color={claudeConfigured ? "#d97706" : "#525252"}
|
color={claudeConfigured ? COLORS.claude : "#525252"}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View className="flex-1">
|
<View className="flex-1">
|
||||||
@@ -110,7 +115,7 @@ export default function OnboardingDoneScreen() {
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={handleEnterApp}
|
onPress={handleEnterApp}
|
||||||
className="w-full rounded-2xl py-4 items-center"
|
className="w-full rounded-2xl py-4 items-center"
|
||||||
style={{ backgroundColor: "#10a37f" }}
|
style={{ backgroundColor: COLORS.codex }}
|
||||||
>
|
>
|
||||||
<Text className="text-white font-bold text-base">Open App</Text>
|
<Text className="text-white font-bold text-base">Open App</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { View, Text, TouchableOpacity } from "react-native";
|
|||||||
import { SafeAreaView } from "react-native-safe-area-context";
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import { MaterialIcons } from "@expo/vector-icons";
|
import { MaterialIcons } from "@expo/vector-icons";
|
||||||
|
import { COLORS } from "@/lib/constants";
|
||||||
|
|
||||||
export default function WelcomeScreen() {
|
export default function WelcomeScreen() {
|
||||||
return (
|
return (
|
||||||
@@ -12,13 +13,13 @@ export default function WelcomeScreen() {
|
|||||||
<View className="flex-row items-center justify-center mb-8">
|
<View className="flex-row items-center justify-center mb-8">
|
||||||
<View
|
<View
|
||||||
className="w-16 h-16 rounded-2xl items-center justify-center"
|
className="w-16 h-16 rounded-2xl items-center justify-center"
|
||||||
style={{ backgroundColor: "#10a37f" }}
|
style={{ backgroundColor: COLORS.codex }}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="auto-awesome" size={28} color="white" />
|
<MaterialIcons name="auto-awesome" size={28} color="white" />
|
||||||
</View>
|
</View>
|
||||||
<View
|
<View
|
||||||
className="w-16 h-16 rounded-2xl items-center justify-center -ml-5"
|
className="w-16 h-16 rounded-2xl items-center justify-center -ml-5"
|
||||||
style={{ backgroundColor: "#d97706" }}
|
style={{ backgroundColor: COLORS.claude }}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="psychology" size={28} color="white" />
|
<MaterialIcons name="psychology" size={28} color="white" />
|
||||||
</View>
|
</View>
|
||||||
@@ -36,9 +37,9 @@ export default function WelcomeScreen() {
|
|||||||
<View className="flex-row items-center gap-x-3 bg-neutral-900 rounded-2xl p-4 border border-neutral-800">
|
<View className="flex-row items-center gap-x-3 bg-neutral-900 rounded-2xl p-4 border border-neutral-800">
|
||||||
<View
|
<View
|
||||||
className="w-10 h-10 rounded-xl items-center justify-center"
|
className="w-10 h-10 rounded-xl items-center justify-center"
|
||||||
style={{ backgroundColor: "#10a37f22" }}
|
style={{ backgroundColor: `${COLORS.codex}22` }}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="auto-awesome" size={20} color="#10a37f" />
|
<MaterialIcons name="auto-awesome" size={20} color={COLORS.codex} />
|
||||||
</View>
|
</View>
|
||||||
<View className="flex-1">
|
<View className="flex-1">
|
||||||
<Text className="text-white font-semibold mb-0.5">Codex CLI</Text>
|
<Text className="text-white font-semibold mb-0.5">Codex CLI</Text>
|
||||||
@@ -50,9 +51,9 @@ export default function WelcomeScreen() {
|
|||||||
<View className="flex-row items-center gap-x-3 bg-neutral-900 rounded-2xl p-4 border border-neutral-800">
|
<View className="flex-row items-center gap-x-3 bg-neutral-900 rounded-2xl p-4 border border-neutral-800">
|
||||||
<View
|
<View
|
||||||
className="w-10 h-10 rounded-xl items-center justify-center"
|
className="w-10 h-10 rounded-xl items-center justify-center"
|
||||||
style={{ backgroundColor: "#d9770622" }}
|
style={{ backgroundColor: `${COLORS.claude}22` }}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="psychology" size={20} color="#d97706" />
|
<MaterialIcons name="psychology" size={20} color={COLORS.claude} />
|
||||||
</View>
|
</View>
|
||||||
<View className="flex-1">
|
<View className="flex-1">
|
||||||
<Text className="text-white font-semibold mb-0.5">Claude.ai</Text>
|
<Text className="text-white font-semibold mb-0.5">Claude.ai</Text>
|
||||||
@@ -68,7 +69,7 @@ export default function WelcomeScreen() {
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => router.push("/onboarding/codex")}
|
onPress={() => router.push("/onboarding/codex")}
|
||||||
className="rounded-2xl py-4 items-center"
|
className="rounded-2xl py-4 items-center"
|
||||||
style={{ backgroundColor: "#10a37f" }}
|
style={{ backgroundColor: COLORS.codex }}
|
||||||
>
|
>
|
||||||
<Text className="text-white font-bold text-base">Get Started</Text>
|
<Text className="text-white font-bold text-base">Get Started</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 1006 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 222 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 384 KiB After Width: | Height: | Size: 985 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 985 KiB |
@@ -5,11 +5,29 @@ const ERROR_MESSAGES: Record<string, string> = {
|
|||||||
MISSING_ACCESS_TOKEN: "auth.json is missing the accessToken field.",
|
MISSING_ACCESS_TOKEN: "auth.json is missing the accessToken field.",
|
||||||
INVALID_JSON: "The selected file is not valid JSON.",
|
INVALID_JSON: "The selected file is not valid JSON.",
|
||||||
NO_ORGS_FOUND: "No Claude organizations found for this session key.",
|
NO_ORGS_FOUND: "No Claude organizations found for this session key.",
|
||||||
|
INVALID_CODEX_RESPONSE:
|
||||||
|
"Codex returned an unexpected response. The app was kept safe from invalid data.",
|
||||||
|
INVALID_CLAUDE_RESPONSE:
|
||||||
|
"Claude returned an unexpected usage response. The app was kept safe from invalid data.",
|
||||||
|
INVALID_CLAUDE_ORGS_RESPONSE:
|
||||||
|
"Claude returned an unexpected organizations response.",
|
||||||
|
UNKNOWN_ERROR: "An unknown error occurred. Try refreshing.",
|
||||||
};
|
};
|
||||||
|
|
||||||
function humanize(code: string): string {
|
function humanize(code: string): string {
|
||||||
if (code.startsWith("HTTP_ERROR_")) {
|
if (code.startsWith("HTTP_ERROR_")) {
|
||||||
return `Unexpected server error (HTTP ${code.replace("HTTP_ERROR_", "")}).`;
|
const status = code.replace("HTTP_ERROR_", "");
|
||||||
|
if (status === "429") return "Rate limited by the server. Try again shortly.";
|
||||||
|
if (status === "500" || status === "502" || status === "503")
|
||||||
|
return `Server error (${status}). The service may be down.`;
|
||||||
|
return `Unexpected server response (HTTP ${status}).`;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
code === "Network request failed" ||
|
||||||
|
code.includes("fetch") ||
|
||||||
|
code.includes("network")
|
||||||
|
) {
|
||||||
|
return "Network error. Check your internet connection and try again.";
|
||||||
}
|
}
|
||||||
return ERROR_MESSAGES[code] ?? `Unexpected error: ${code}`;
|
return ERROR_MESSAGES[code] ?? `Unexpected error: ${code}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Text } from "react-native";
|
||||||
|
import { COUNTDOWN_INTERVAL_MS } from "@/lib/constants";
|
||||||
|
import { formatLastUpdated } from "@/lib/timeUtils";
|
||||||
|
|
||||||
|
export function LastUpdated({ timestamp }: { timestamp: number | null }) {
|
||||||
|
const [now, setNow] = useState(Date.now);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => setNow(Date.now()), COUNTDOWN_INTERVAL_MS);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!timestamp) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text selectable className="text-xs text-neutral-400 dark:text-neutral-500">
|
||||||
|
{formatLastUpdated(timestamp, now)}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
|
import { PROGRESS_THRESHOLDS } from "@/lib/constants";
|
||||||
|
|
||||||
interface ProgressBarProps {
|
interface ProgressBarProps {
|
||||||
percent: number;
|
percent: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBarColor(percent: number): string {
|
function getBarColor(percent: number): string {
|
||||||
if (percent >= 85) return "bg-red-500";
|
if (percent >= PROGRESS_THRESHOLDS.danger) return "bg-red-500";
|
||||||
if (percent >= 60) return "bg-yellow-400";
|
if (percent >= PROGRESS_THRESHOLDS.warning) return "bg-yellow-400";
|
||||||
return "bg-green-500";
|
return "bg-green-500";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Text } from "react-native";
|
import { Text } from "react-native";
|
||||||
|
import { COUNTDOWN_INTERVAL_MS } from "@/lib/constants";
|
||||||
import { formatResetCountdown, formatResetCountdownISO } from "@/lib/timeUtils";
|
import { formatResetCountdown, formatResetCountdownISO } from "@/lib/timeUtils";
|
||||||
|
|
||||||
interface ResetCountdownProps {
|
interface ResetCountdownProps {
|
||||||
@@ -8,18 +9,28 @@ interface ResetCountdownProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ResetCountdown({ resetAtSeconds, resetAtISO }: ResetCountdownProps) {
|
export function ResetCountdown({ resetAtSeconds, resetAtISO }: ResetCountdownProps) {
|
||||||
|
const latest = useRef({ resetAtSeconds, resetAtISO });
|
||||||
|
latest.current = { resetAtSeconds, resetAtISO };
|
||||||
|
|
||||||
const getLabel = () => {
|
const getLabel = () => {
|
||||||
if (resetAtSeconds !== undefined) return formatResetCountdown(resetAtSeconds);
|
if (latest.current.resetAtSeconds !== undefined) {
|
||||||
if (resetAtISO) return formatResetCountdownISO(resetAtISO);
|
return formatResetCountdown(latest.current.resetAtSeconds);
|
||||||
|
}
|
||||||
|
if (latest.current.resetAtISO) return formatResetCountdownISO(latest.current.resetAtISO);
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const [label, setLabel] = useState(getLabel);
|
const [label, setLabel] = useState(getLabel);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLabel(getLabel());
|
const update = () => setLabel(getLabel());
|
||||||
const interval = setInterval(() => setLabel(getLabel()), 60_000);
|
update();
|
||||||
|
const interval = setInterval(update, COUNTDOWN_INTERVAL_MS);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLabel(getLabel());
|
||||||
}, [resetAtSeconds, resetAtISO]);
|
}, [resetAtSeconds, resetAtISO]);
|
||||||
|
|
||||||
if (!label) return null;
|
if (!label) return null;
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Text, TouchableOpacity, View } from "react-native";
|
||||||
|
import { COLORS } from "@/lib/constants";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: React.ReactNode;
|
||||||
|
screenName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ScreenErrorBoundary extends React.Component<Props, State> {
|
||||||
|
state: State = { error: null };
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: Error): State {
|
||||||
|
return { error };
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.state.error) return this.props.children;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="flex-1 items-center justify-center bg-neutral-50 dark:bg-neutral-950 px-8">
|
||||||
|
<Text selectable className="text-xl font-bold text-neutral-900 dark:text-white">
|
||||||
|
{this.props.screenName} hit a snag
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
selectable
|
||||||
|
className="mt-2 text-center text-sm text-neutral-500 dark:text-neutral-400"
|
||||||
|
>
|
||||||
|
This screen could not be rendered. The rest of Codexbar is still available.
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => this.setState({ error: null })}
|
||||||
|
className="mt-5 rounded-xl px-4 py-3"
|
||||||
|
style={{ backgroundColor: COLORS.codex }}
|
||||||
|
>
|
||||||
|
<Text className="text-sm font-semibold text-white">Try again</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,47 +1,52 @@
|
|||||||
import { useState, useCallback, useEffect } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { fetchClaudeOrgs, fetchClaudeUsage } from "@/lib/api/claudeApi";
|
import {
|
||||||
|
fetchClaudeOrgs,
|
||||||
|
fetchClaudeUsage,
|
||||||
|
parseClaudeUsageResponse,
|
||||||
|
} from "@/lib/api/claudeApi";
|
||||||
|
import {
|
||||||
|
isClaudeSessionKeyValid,
|
||||||
|
validateClaudeSessionKey,
|
||||||
|
} from "@/lib/claudeCredentials";
|
||||||
|
import { RETRY_DELAY_MS } from "@/lib/constants";
|
||||||
import {
|
import {
|
||||||
loadClaudeSessionKey,
|
|
||||||
saveClaudeSessionKey,
|
|
||||||
clearClaudeSessionKey,
|
|
||||||
loadClaudeLastActiveOrg,
|
|
||||||
saveClaudeLastActiveOrg,
|
|
||||||
clearClaudeLastActiveOrg,
|
clearClaudeLastActiveOrg,
|
||||||
|
clearClaudeSessionKey,
|
||||||
|
clearClaudeUsageCache,
|
||||||
|
loadClaudeLastActiveOrg,
|
||||||
|
loadClaudeSessionKey,
|
||||||
|
loadClaudeUsageCache,
|
||||||
|
saveClaudeLastActiveOrg,
|
||||||
|
saveClaudeSessionKey,
|
||||||
|
saveClaudeUsageCache,
|
||||||
} from "@/lib/storage";
|
} from "@/lib/storage";
|
||||||
import type { ClaudeAuth, ClaudeUsageResponse } from "@/types/claude";
|
import type { ClaudeAuth, ClaudeUsageResponse } from "@/types/claude";
|
||||||
|
|
||||||
type Status = "idle" | "loading" | "success" | "error";
|
type Status = "idle" | "loading" | "success" | "error";
|
||||||
|
|
||||||
async function doFetch(
|
function shouldRetry(error: unknown): boolean {
|
||||||
auth: ClaudeAuth,
|
const message = error instanceof Error ? error.message : "";
|
||||||
set: {
|
return ![
|
||||||
auth: (v: ClaudeAuth | null) => void;
|
"TOKEN_EXPIRED",
|
||||||
usage: (v: ClaudeUsageResponse | null) => void;
|
"NO_ORGS_FOUND",
|
||||||
status: (v: Status) => void;
|
"INVALID_CLAUDE_RESPONSE",
|
||||||
error: (v: string | null) => void;
|
"INVALID_CLAUDE_ORGS_RESPONSE",
|
||||||
}
|
].includes(message);
|
||||||
) {
|
}
|
||||||
set.status("loading");
|
|
||||||
set.error(null);
|
function wait(ms: number): Promise<void> {
|
||||||
try {
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
let orgUuid = auth.lastActiveOrg;
|
}
|
||||||
if (!orgUuid) {
|
|
||||||
const orgs = await fetchClaudeOrgs(auth);
|
async function requestClaudeUsage(auth: ClaudeAuth): Promise<ClaudeUsageResponse> {
|
||||||
if (!orgs.length) throw new Error("NO_ORGS_FOUND");
|
let orgUuid = auth.lastActiveOrg?.trim();
|
||||||
orgUuid = orgs[0].uuid;
|
if (!orgUuid) {
|
||||||
}
|
const orgs = await fetchClaudeOrgs(auth);
|
||||||
const data = await fetchClaudeUsage(auth, orgUuid);
|
if (!orgs.length) throw new Error("NO_ORGS_FOUND");
|
||||||
set.usage(data);
|
orgUuid = orgs[0].uuid;
|
||||||
set.status("success");
|
await saveClaudeLastActiveOrg(orgUuid);
|
||||||
} catch (e: unknown) {
|
|
||||||
const msg = e instanceof Error ? e.message : "UNKNOWN_ERROR";
|
|
||||||
set.error(msg);
|
|
||||||
set.status("error");
|
|
||||||
if (msg === "TOKEN_EXPIRED") {
|
|
||||||
await clearClaudeSessionKey();
|
|
||||||
set.auth(null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return fetchClaudeUsage(auth, orgUuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useClaudeUsage() {
|
export function useClaudeUsage() {
|
||||||
@@ -49,60 +54,171 @@ export function useClaudeUsage() {
|
|||||||
const [pendingKey, setPendingKey] = useState("");
|
const [pendingKey, setPendingKey] = useState("");
|
||||||
const [pendingOrg, setPendingOrg] = useState("");
|
const [pendingOrg, setPendingOrg] = useState("");
|
||||||
const [usage, setUsage] = useState<ClaudeUsageResponse | null>(null);
|
const [usage, setUsage] = useState<ClaudeUsageResponse | null>(null);
|
||||||
|
const [lastFetchedAt, setLastFetchedAt] = useState<number | null>(null);
|
||||||
const [status, setStatus] = useState<Status>("idle");
|
const [status, setStatus] = useState<Status>("idle");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const mounted = useRef(true);
|
||||||
const setters = { auth: setAuth, usage: setUsage, status: setStatus, error: setError };
|
const inFlight = useRef<{
|
||||||
|
key: string;
|
||||||
|
promise: Promise<void>;
|
||||||
|
} | null>(null);
|
||||||
|
const requestVersion = useRef(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([loadClaudeSessionKey(), loadClaudeLastActiveOrg()]).then(
|
mounted.current = true;
|
||||||
([key, org]) => {
|
return () => {
|
||||||
if (key) setAuth({ sessionKey: key, lastActiveOrg: org ?? undefined });
|
mounted.current = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchUsage = useCallback((credentials: ClaudeAuth): Promise<void> => {
|
||||||
|
const sanitized = {
|
||||||
|
sessionKey: credentials.sessionKey.trim(),
|
||||||
|
lastActiveOrg: credentials.lastActiveOrg?.trim() || undefined,
|
||||||
|
};
|
||||||
|
const requestKey = `${sanitized.lastActiveOrg ?? ""}:${sanitized.sessionKey}`;
|
||||||
|
if (inFlight.current?.key === requestKey) return inFlight.current.promise;
|
||||||
|
const version = ++requestVersion.current;
|
||||||
|
|
||||||
|
const promise = (async () => {
|
||||||
|
if (mounted.current) {
|
||||||
|
setStatus("loading");
|
||||||
|
setError(null);
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
try {
|
||||||
|
let data: ClaudeUsageResponse;
|
||||||
|
try {
|
||||||
|
data = await requestClaudeUsage(sanitized);
|
||||||
|
} catch (firstError) {
|
||||||
|
if (!shouldRetry(firstError)) throw firstError;
|
||||||
|
await wait(RETRY_DELAY_MS);
|
||||||
|
if (version !== requestVersion.current) return;
|
||||||
|
data = await requestClaudeUsage(sanitized);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version !== requestVersion.current) return;
|
||||||
|
const fetchedAt = Date.now();
|
||||||
|
await saveClaudeUsageCache({ data, lastFetchedAt: fetchedAt });
|
||||||
|
if (mounted.current && version === requestVersion.current) {
|
||||||
|
setUsage(data);
|
||||||
|
setLastFetchedAt(fetchedAt);
|
||||||
|
setStatus("success");
|
||||||
|
}
|
||||||
|
} catch (caught: unknown) {
|
||||||
|
if (version !== requestVersion.current) return;
|
||||||
|
const message = caught instanceof Error ? caught.message : "UNKNOWN_ERROR";
|
||||||
|
if (message === "TOKEN_EXPIRED") {
|
||||||
|
await clearClaudeSessionKey();
|
||||||
|
if (mounted.current) {
|
||||||
|
setAuth(null);
|
||||||
|
setUsage(null);
|
||||||
|
setLastFetchedAt(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mounted.current) {
|
||||||
|
setError(message);
|
||||||
|
setStatus("error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})().finally(() => {
|
||||||
|
if (inFlight.current?.promise === promise) inFlight.current = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
inFlight.current = { key: requestKey, promise };
|
||||||
|
return promise;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (auth) void doFetch(auth, setters);
|
let active = true;
|
||||||
}, [auth]);
|
|
||||||
|
void Promise.all([
|
||||||
|
loadClaudeSessionKey(),
|
||||||
|
loadClaudeLastActiveOrg(),
|
||||||
|
loadClaudeUsageCache(),
|
||||||
|
]).then(async ([key, org, cache]) => {
|
||||||
|
if (!active) return;
|
||||||
|
|
||||||
|
const storedAuth = key
|
||||||
|
? { sessionKey: key.trim(), lastActiveOrg: org?.trim() || undefined }
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (storedAuth && cache) {
|
||||||
|
try {
|
||||||
|
if (!Number.isFinite(cache.lastFetchedAt)) {
|
||||||
|
throw new Error("INVALID_CACHE_TIMESTAMP");
|
||||||
|
}
|
||||||
|
const cachedUsage = parseClaudeUsageResponse(cache.data);
|
||||||
|
setUsage(cachedUsage);
|
||||||
|
setLastFetchedAt(cache.lastFetchedAt);
|
||||||
|
setStatus("success");
|
||||||
|
} catch {
|
||||||
|
await clearClaudeUsageCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuth(storedAuth);
|
||||||
|
if (storedAuth) await fetchUsage(storedAuth);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, [fetchUsage]);
|
||||||
|
|
||||||
const saveKey = useCallback(async () => {
|
const saveKey = useCallback(async () => {
|
||||||
const trimmed = pendingKey.trim();
|
const trimmedKey = pendingKey.trim();
|
||||||
if (!trimmed.startsWith("sk-ant-")) {
|
const validationError = validateClaudeSessionKey(trimmedKey);
|
||||||
setError("Session key must start with sk-ant-");
|
if (validationError) {
|
||||||
|
setError(validationError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const orgTrimmed = pendingOrg.trim() || undefined;
|
|
||||||
await saveClaudeSessionKey(trimmed);
|
const trimmedOrg = pendingOrg.trim() || undefined;
|
||||||
if (orgTrimmed) await saveClaudeLastActiveOrg(orgTrimmed);
|
await saveClaudeSessionKey(trimmedKey);
|
||||||
|
if (trimmedOrg) await saveClaudeLastActiveOrg(trimmedOrg);
|
||||||
else await clearClaudeLastActiveOrg();
|
else await clearClaudeLastActiveOrg();
|
||||||
setAuth({ sessionKey: trimmed, lastActiveOrg: orgTrimmed });
|
|
||||||
|
const nextAuth = { sessionKey: trimmedKey, lastActiveOrg: trimmedOrg };
|
||||||
|
setAuth(nextAuth);
|
||||||
setPendingKey("");
|
setPendingKey("");
|
||||||
setPendingOrg("");
|
setPendingOrg("");
|
||||||
setError(null);
|
setError(null);
|
||||||
}, [pendingKey, pendingOrg]);
|
await fetchUsage(nextAuth);
|
||||||
|
}, [fetchUsage, pendingKey, pendingOrg]);
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
if (!auth) return;
|
if (auth) await fetchUsage(auth);
|
||||||
await doFetch(auth, setters);
|
}, [auth, fetchUsage]);
|
||||||
}, [auth]);
|
|
||||||
|
|
||||||
const reload = useCallback(async () => {
|
const reloadCredentials = useCallback(async () => {
|
||||||
const [key, org] = await Promise.all([loadClaudeSessionKey(), loadClaudeLastActiveOrg()]);
|
const [key, org] = await Promise.all([
|
||||||
|
loadClaudeSessionKey(),
|
||||||
|
loadClaudeLastActiveOrg(),
|
||||||
|
]);
|
||||||
if (!key) {
|
if (!key) {
|
||||||
|
requestVersion.current += 1;
|
||||||
setAuth(null);
|
setAuth(null);
|
||||||
setUsage(null);
|
setUsage(null);
|
||||||
|
setLastFetchedAt(null);
|
||||||
setStatus("idle");
|
setStatus("idle");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Creating a new object always triggers the [auth] useEffect even if values are identical
|
|
||||||
setAuth({ sessionKey: key, lastActiveOrg: org ?? undefined });
|
const stored = {
|
||||||
}, []);
|
sessionKey: key.trim(),
|
||||||
|
lastActiveOrg: org?.trim() || undefined,
|
||||||
|
};
|
||||||
|
setAuth(stored);
|
||||||
|
await fetchUsage(stored);
|
||||||
|
}, [fetchUsage]);
|
||||||
|
|
||||||
const clearKey = useCallback(async () => {
|
const clearKey = useCallback(async () => {
|
||||||
|
requestVersion.current += 1;
|
||||||
await clearClaudeSessionKey();
|
await clearClaudeSessionKey();
|
||||||
setAuth(null);
|
setAuth(null);
|
||||||
setUsage(null);
|
setUsage(null);
|
||||||
|
setLastFetchedAt(null);
|
||||||
setStatus("idle");
|
setStatus("idle");
|
||||||
setError(null);
|
setError(null);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -114,11 +230,16 @@ export function useClaudeUsage() {
|
|||||||
pendingOrg,
|
pendingOrg,
|
||||||
setPendingOrg,
|
setPendingOrg,
|
||||||
usage,
|
usage,
|
||||||
|
lastFetchedAt,
|
||||||
status,
|
status,
|
||||||
error,
|
error,
|
||||||
|
keyValidationError: pendingKey
|
||||||
|
? validateClaudeSessionKey(pendingKey)
|
||||||
|
: null,
|
||||||
|
canSaveKey: isClaudeSessionKeyValid(pendingKey),
|
||||||
saveKey,
|
saveKey,
|
||||||
refresh,
|
refresh,
|
||||||
reload,
|
reloadCredentials,
|
||||||
clearKey,
|
clearKey,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +1,138 @@
|
|||||||
import { useState, useCallback, useEffect } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { fetchCodexUsage } from "@/lib/api/codexApi";
|
import {
|
||||||
import { loadCodexAuth, saveCodexAuth, clearCodexAuth } from "@/lib/storage";
|
fetchCodexUsage,
|
||||||
|
parseCodexUsageResponse,
|
||||||
|
} from "@/lib/api/codexApi";
|
||||||
|
import { RETRY_DELAY_MS } from "@/lib/constants";
|
||||||
import { pickAndReadCodexAuth } from "@/lib/fileReader";
|
import { pickAndReadCodexAuth } from "@/lib/fileReader";
|
||||||
import type { CodexUsageResponse, CodexAuth } from "@/types/codex";
|
import {
|
||||||
|
clearCodexAuth,
|
||||||
|
clearCodexUsageCache,
|
||||||
|
loadCodexAuth,
|
||||||
|
loadCodexUsageCache,
|
||||||
|
saveCodexAuth,
|
||||||
|
saveCodexUsageCache,
|
||||||
|
} from "@/lib/storage";
|
||||||
|
import type { CodexAuth, CodexUsageResponse } from "@/types/codex";
|
||||||
|
|
||||||
type Status = "idle" | "loading" | "success" | "error";
|
type Status = "idle" | "loading" | "success" | "error";
|
||||||
|
|
||||||
async function doFetch(
|
function shouldRetry(error: unknown): boolean {
|
||||||
auth: CodexAuth,
|
const message = error instanceof Error ? error.message : "";
|
||||||
set: {
|
return (
|
||||||
auth: (v: CodexAuth | null) => void;
|
message !== "TOKEN_EXPIRED" &&
|
||||||
usage: (v: CodexUsageResponse | null) => void;
|
message !== "INVALID_CODEX_RESPONSE"
|
||||||
status: (v: Status) => void;
|
);
|
||||||
error: (v: string | null) => void;
|
}
|
||||||
}
|
|
||||||
) {
|
function wait(ms: number): Promise<void> {
|
||||||
set.status("loading");
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
set.error(null);
|
|
||||||
try {
|
|
||||||
const data = await fetchCodexUsage(auth);
|
|
||||||
set.usage(data);
|
|
||||||
set.status("success");
|
|
||||||
} catch (e: unknown) {
|
|
||||||
const msg = e instanceof Error ? e.message : "UNKNOWN_ERROR";
|
|
||||||
set.error(msg);
|
|
||||||
set.status("error");
|
|
||||||
if (msg === "TOKEN_EXPIRED") {
|
|
||||||
await clearCodexAuth();
|
|
||||||
set.auth(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCodexUsage() {
|
export function useCodexUsage() {
|
||||||
const [auth, setAuth] = useState<CodexAuth | null>(null);
|
const [auth, setAuth] = useState<CodexAuth | null>(null);
|
||||||
const [usage, setUsage] = useState<CodexUsageResponse | null>(null);
|
const [usage, setUsage] = useState<CodexUsageResponse | null>(null);
|
||||||
|
const [lastFetchedAt, setLastFetchedAt] = useState<number | null>(null);
|
||||||
const [status, setStatus] = useState<Status>("idle");
|
const [status, setStatus] = useState<Status>("idle");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const mounted = useRef(true);
|
||||||
const setters = { auth: setAuth, usage: setUsage, status: setStatus, error: setError };
|
const inFlight = useRef<{
|
||||||
|
key: string;
|
||||||
|
promise: Promise<void>;
|
||||||
|
} | null>(null);
|
||||||
|
const requestVersion = useRef(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadCodexAuth().then((stored) => {
|
mounted.current = true;
|
||||||
if (stored) setAuth(stored);
|
return () => {
|
||||||
|
mounted.current = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchUsage = useCallback((credentials: CodexAuth): Promise<void> => {
|
||||||
|
const requestKey = `${credentials.accountId ?? ""}:${credentials.accessToken}`;
|
||||||
|
if (inFlight.current?.key === requestKey) return inFlight.current.promise;
|
||||||
|
const version = ++requestVersion.current;
|
||||||
|
|
||||||
|
const promise = (async () => {
|
||||||
|
if (mounted.current) {
|
||||||
|
setStatus("loading");
|
||||||
|
setError(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let data: CodexUsageResponse;
|
||||||
|
try {
|
||||||
|
data = await fetchCodexUsage(credentials);
|
||||||
|
} catch (firstError) {
|
||||||
|
if (!shouldRetry(firstError)) throw firstError;
|
||||||
|
await wait(RETRY_DELAY_MS);
|
||||||
|
if (version !== requestVersion.current) return;
|
||||||
|
data = await fetchCodexUsage(credentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version !== requestVersion.current) return;
|
||||||
|
const fetchedAt = Date.now();
|
||||||
|
await saveCodexUsageCache({ data, lastFetchedAt: fetchedAt });
|
||||||
|
if (mounted.current && version === requestVersion.current) {
|
||||||
|
setUsage(data);
|
||||||
|
setLastFetchedAt(fetchedAt);
|
||||||
|
setStatus("success");
|
||||||
|
}
|
||||||
|
} catch (caught: unknown) {
|
||||||
|
if (version !== requestVersion.current) return;
|
||||||
|
const message = caught instanceof Error ? caught.message : "UNKNOWN_ERROR";
|
||||||
|
if (message === "TOKEN_EXPIRED") {
|
||||||
|
await clearCodexAuth();
|
||||||
|
if (mounted.current) {
|
||||||
|
setAuth(null);
|
||||||
|
setUsage(null);
|
||||||
|
setLastFetchedAt(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mounted.current) {
|
||||||
|
setError(message);
|
||||||
|
setStatus("error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})().finally(() => {
|
||||||
|
if (inFlight.current?.promise === promise) inFlight.current = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
inFlight.current = { key: requestKey, promise };
|
||||||
|
return promise;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (auth) void doFetch(auth, setters);
|
let active = true;
|
||||||
}, [auth]);
|
|
||||||
|
void Promise.all([loadCodexAuth(), loadCodexUsageCache()]).then(
|
||||||
|
async ([storedAuth, cache]) => {
|
||||||
|
if (!active) return;
|
||||||
|
|
||||||
|
if (storedAuth && cache) {
|
||||||
|
try {
|
||||||
|
if (!Number.isFinite(cache.lastFetchedAt)) {
|
||||||
|
throw new Error("INVALID_CACHE_TIMESTAMP");
|
||||||
|
}
|
||||||
|
const cachedUsage = parseCodexUsageResponse(cache.data);
|
||||||
|
setUsage(cachedUsage);
|
||||||
|
setLastFetchedAt(cache.lastFetchedAt);
|
||||||
|
setStatus("success");
|
||||||
|
} catch {
|
||||||
|
await clearCodexUsageCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuth(storedAuth);
|
||||||
|
if (storedAuth) await fetchUsage(storedAuth);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, [fetchUsage]);
|
||||||
|
|
||||||
const importAuthFile = useCallback(async () => {
|
const importAuthFile = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -56,39 +140,50 @@ export function useCodexUsage() {
|
|||||||
await saveCodexAuth(parsed);
|
await saveCodexAuth(parsed);
|
||||||
setAuth(parsed);
|
setAuth(parsed);
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (e: unknown) {
|
await fetchUsage(parsed);
|
||||||
const msg = e instanceof Error ? e.message : "UNKNOWN_ERROR";
|
} catch (caught: unknown) {
|
||||||
if (msg !== "PICKER_CANCELLED") setError(msg);
|
const message = caught instanceof Error ? caught.message : "UNKNOWN_ERROR";
|
||||||
|
if (message !== "PICKER_CANCELLED") setError(message);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [fetchUsage]);
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
if (!auth) return;
|
if (auth) await fetchUsage(auth);
|
||||||
await doFetch(auth, setters);
|
}, [auth, fetchUsage]);
|
||||||
}, [auth]);
|
|
||||||
|
|
||||||
// Re-reads auth from storage and fetches fresh data. Call from useFocusEffect
|
const reloadCredentials = useCallback(async () => {
|
||||||
// so the dashboard stays current when credentials are saved from another tab.
|
|
||||||
const reload = useCallback(async () => {
|
|
||||||
const stored = await loadCodexAuth();
|
const stored = await loadCodexAuth();
|
||||||
if (!stored) {
|
if (!stored) {
|
||||||
|
requestVersion.current += 1;
|
||||||
setAuth(null);
|
setAuth(null);
|
||||||
setUsage(null);
|
setUsage(null);
|
||||||
|
setLastFetchedAt(null);
|
||||||
setStatus("idle");
|
setStatus("idle");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Always pass a fresh object so the [auth] effect fires even if the content
|
|
||||||
// is identical (JSON.parse returns a new reference each time, which is fine).
|
|
||||||
setAuth(stored);
|
setAuth(stored);
|
||||||
}, []);
|
await fetchUsage(stored);
|
||||||
|
}, [fetchUsage]);
|
||||||
|
|
||||||
const clearAuth = useCallback(async () => {
|
const clearAuth = useCallback(async () => {
|
||||||
|
requestVersion.current += 1;
|
||||||
await clearCodexAuth();
|
await clearCodexAuth();
|
||||||
setAuth(null);
|
setAuth(null);
|
||||||
setUsage(null);
|
setUsage(null);
|
||||||
|
setLastFetchedAt(null);
|
||||||
setStatus("idle");
|
setStatus("idle");
|
||||||
setError(null);
|
setError(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return { auth, usage, status, error, importAuthFile, refresh, reload, clearAuth };
|
return {
|
||||||
|
auth,
|
||||||
|
usage,
|
||||||
|
lastFetchedAt,
|
||||||
|
status,
|
||||||
|
error,
|
||||||
|
importAuthFile,
|
||||||
|
refresh,
|
||||||
|
reloadCredentials,
|
||||||
|
clearAuth,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,78 @@ function buildHeaders(auth: ClaudeAuth): Record<string, string> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isClaudeWindow(value: unknown): boolean {
|
||||||
|
if (!value || typeof value !== "object") return false;
|
||||||
|
const window = value as Record<string, unknown>;
|
||||||
|
return (
|
||||||
|
typeof window.utilization === "number" &&
|
||||||
|
Number.isFinite(window.utilization) &&
|
||||||
|
(window.resets_at === undefined ||
|
||||||
|
window.resets_at === null ||
|
||||||
|
typeof window.resets_at === "string")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseClaudeOrgsResponse(data: unknown): ClaudeOrg[] {
|
||||||
|
if (
|
||||||
|
!Array.isArray(data) ||
|
||||||
|
data.some(
|
||||||
|
(org) =>
|
||||||
|
!org ||
|
||||||
|
typeof org !== "object" ||
|
||||||
|
typeof (org as Record<string, unknown>).uuid !== "string" ||
|
||||||
|
typeof (org as Record<string, unknown>).name !== "string"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error("INVALID_CLAUDE_ORGS_RESPONSE");
|
||||||
|
}
|
||||||
|
return data.map((org) => {
|
||||||
|
const value = org as Record<string, unknown>;
|
||||||
|
return { uuid: value.uuid as string, name: value.name as string };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseClaudeUsageResponse(data: unknown): ClaudeUsageResponse {
|
||||||
|
if (!data || typeof data !== "object") {
|
||||||
|
throw new Error("INVALID_CLAUDE_RESPONSE");
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = data as Record<string, unknown>;
|
||||||
|
if (
|
||||||
|
!isClaudeWindow(value.five_hour) ||
|
||||||
|
!isClaudeWindow(value.seven_day) ||
|
||||||
|
(value.seven_day_sonnet !== undefined &&
|
||||||
|
value.seven_day_sonnet !== null &&
|
||||||
|
!isClaudeWindow(value.seven_day_sonnet)) ||
|
||||||
|
(value.seven_day_opus !== undefined &&
|
||||||
|
value.seven_day_opus !== null &&
|
||||||
|
!isClaudeWindow(value.seven_day_opus))
|
||||||
|
) {
|
||||||
|
throw new Error("INVALID_CLAUDE_RESPONSE");
|
||||||
|
}
|
||||||
|
|
||||||
|
const toWindow = (window: unknown) => {
|
||||||
|
const item = window as Record<string, unknown>;
|
||||||
|
return {
|
||||||
|
utilization: item.utilization as number,
|
||||||
|
...(typeof item.resets_at === "string"
|
||||||
|
? { resets_at: item.resets_at }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
five_hour: toWindow(value.five_hour),
|
||||||
|
seven_day: toWindow(value.seven_day),
|
||||||
|
...(value.seven_day_sonnet
|
||||||
|
? { seven_day_sonnet: toWindow(value.seven_day_sonnet) }
|
||||||
|
: {}),
|
||||||
|
...(value.seven_day_opus
|
||||||
|
? { seven_day_opus: toWindow(value.seven_day_opus) }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchClaudeOrgs(auth: ClaudeAuth): Promise<ClaudeOrg[]> {
|
export async function fetchClaudeOrgs(auth: ClaudeAuth): Promise<ClaudeOrg[]> {
|
||||||
const response = await fetch(`${BASE_URL}/organizations`, {
|
const response = await fetch(`${BASE_URL}/organizations`, {
|
||||||
headers: buildHeaders(auth),
|
headers: buildHeaders(auth),
|
||||||
@@ -31,7 +103,8 @@ export async function fetchClaudeOrgs(auth: ClaudeAuth): Promise<ClaudeOrg[]> {
|
|||||||
throw new Error(`HTTP_ERROR_${response.status}`);
|
throw new Error(`HTTP_ERROR_${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json() as Promise<ClaudeOrg[]>;
|
const data: unknown = await response.json();
|
||||||
|
return parseClaudeOrgsResponse(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchClaudeUsage(
|
export async function fetchClaudeUsage(
|
||||||
@@ -49,5 +122,6 @@ export async function fetchClaudeUsage(
|
|||||||
throw new Error(`HTTP_ERROR_${response.status}`);
|
throw new Error(`HTTP_ERROR_${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json() as Promise<ClaudeUsageResponse>;
|
const data: unknown = await response.json();
|
||||||
|
return parseClaudeUsageResponse(data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,64 @@ import type { CodexAuth, CodexUsageResponse } from "@/types/codex";
|
|||||||
|
|
||||||
const USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
const USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
||||||
|
|
||||||
|
function isNumber(value: unknown): value is number {
|
||||||
|
return typeof value === "number" && Number.isFinite(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseCodexUsageResponse(data: unknown): CodexUsageResponse {
|
||||||
|
if (!data || typeof data !== "object") {
|
||||||
|
throw new Error("INVALID_CODEX_RESPONSE");
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = data as Record<string, unknown>;
|
||||||
|
const rateLimit = value.rate_limit as Record<string, unknown> | undefined;
|
||||||
|
const primary = rateLimit?.primary_window as Record<string, unknown> | undefined;
|
||||||
|
const secondary = rateLimit?.secondary_window as Record<string, unknown> | undefined;
|
||||||
|
const credits = value.credits as Record<string, unknown> | undefined;
|
||||||
|
|
||||||
|
const validWindow = (window: Record<string, unknown> | undefined) =>
|
||||||
|
!!window &&
|
||||||
|
isNumber(window.used_percent) &&
|
||||||
|
isNumber(window.reset_at) &&
|
||||||
|
isNumber(window.limit_window_seconds);
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof value.plan_type !== "string" ||
|
||||||
|
!validWindow(primary) ||
|
||||||
|
!validWindow(secondary) ||
|
||||||
|
!credits ||
|
||||||
|
typeof credits.has_credits !== "boolean" ||
|
||||||
|
typeof credits.unlimited !== "boolean" ||
|
||||||
|
!isNumber(credits.balance)
|
||||||
|
) {
|
||||||
|
throw new Error("INVALID_CODEX_RESPONSE");
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryWindow = primary as Record<string, unknown>;
|
||||||
|
const secondaryWindow = secondary as Record<string, unknown>;
|
||||||
|
|
||||||
|
return {
|
||||||
|
plan_type: value.plan_type,
|
||||||
|
rate_limit: {
|
||||||
|
primary_window: {
|
||||||
|
used_percent: primaryWindow.used_percent as number,
|
||||||
|
reset_at: primaryWindow.reset_at as number,
|
||||||
|
limit_window_seconds: primaryWindow.limit_window_seconds as number,
|
||||||
|
},
|
||||||
|
secondary_window: {
|
||||||
|
used_percent: secondaryWindow.used_percent as number,
|
||||||
|
reset_at: secondaryWindow.reset_at as number,
|
||||||
|
limit_window_seconds: secondaryWindow.limit_window_seconds as number,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
credits: {
|
||||||
|
has_credits: credits.has_credits,
|
||||||
|
unlimited: credits.unlimited,
|
||||||
|
balance: credits.balance,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchCodexUsage(auth: CodexAuth): Promise<CodexUsageResponse> {
|
export async function fetchCodexUsage(auth: CodexAuth): Promise<CodexUsageResponse> {
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
Authorization: `Bearer ${auth.accessToken}`,
|
Authorization: `Bearer ${auth.accessToken}`,
|
||||||
@@ -22,5 +80,6 @@ export async function fetchCodexUsage(auth: CodexAuth): Promise<CodexUsageRespon
|
|||||||
throw new Error(`HTTP_ERROR_${response.status}`);
|
throw new Error(`HTTP_ERROR_${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json() as Promise<CodexUsageResponse>;
|
const data: unknown = await response.json();
|
||||||
|
return parseCodexUsageResponse(data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
const CLAUDE_SESSION_KEY_PATTERN = /^sk-ant-[A-Za-z0-9_-]+$/;
|
||||||
|
|
||||||
|
export function validateClaudeSessionKey(value: string): string | null {
|
||||||
|
const key = value.trim();
|
||||||
|
if (!key.startsWith("sk-ant-")) {
|
||||||
|
return "Session key must start with sk-ant-";
|
||||||
|
}
|
||||||
|
if (key.length <= 40) {
|
||||||
|
return "Session key looks too short. Paste the complete cookie value.";
|
||||||
|
}
|
||||||
|
if (!CLAUDE_SESSION_KEY_PATTERN.test(key)) {
|
||||||
|
return "Session key contains unexpected characters. Check the pasted value.";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isClaudeSessionKeyValid(value: string): boolean {
|
||||||
|
return validateClaudeSessionKey(value) === null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
export const COLORS = {
|
||||||
|
codex: "#10a37f",
|
||||||
|
claude: "#d97706",
|
||||||
|
danger: "#ef4444",
|
||||||
|
warning: "#f59e0b",
|
||||||
|
disabled: "#e5e5e5",
|
||||||
|
muted: "#a3a3a3",
|
||||||
|
appBackground: "#090d11",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const PROGRESS_THRESHOLDS = {
|
||||||
|
warning: 60,
|
||||||
|
danger: 85,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const COUNTDOWN_INTERVAL_MS = 60_000;
|
||||||
|
export const RETRY_DELAY_MS = 5_000;
|
||||||
|
|
||||||
|
export function getUsageColor(percent: number): string {
|
||||||
|
if (percent >= PROGRESS_THRESHOLDS.danger) return COLORS.danger;
|
||||||
|
if (percent >= PROGRESS_THRESHOLDS.warning) return COLORS.warning;
|
||||||
|
return COLORS.codex;
|
||||||
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
import * as SecureStore from "expo-secure-store";
|
import * as SecureStore from "expo-secure-store";
|
||||||
|
import type { ClaudeUsageResponse } from "@/types/claude";
|
||||||
|
import type { CodexUsageResponse } from "@/types/codex";
|
||||||
|
|
||||||
const KEYS = {
|
const KEYS = {
|
||||||
CODEX_AUTH: "codexbar_codex_auth",
|
CODEX_AUTH: "codexbar_codex_auth",
|
||||||
CLAUDE_SESSION_KEY: "codexbar_claude_session_key",
|
CLAUDE_SESSION_KEY: "codexbar_claude_session_key",
|
||||||
CLAUDE_LAST_ACTIVE_ORG: "codexbar_claude_last_active_org",
|
CLAUDE_LAST_ACTIVE_ORG: "codexbar_claude_last_active_org",
|
||||||
|
CODEX_USAGE_CACHE: "codexbar_codex_usage_cache",
|
||||||
|
CLAUDE_USAGE_CACHE: "codexbar_claude_usage_cache",
|
||||||
NOTIF_DAILY_ENABLED: "notifDailyEnabled",
|
NOTIF_DAILY_ENABLED: "notifDailyEnabled",
|
||||||
NOTIF_DAILY_HOUR: "notifDailyHour",
|
NOTIF_DAILY_HOUR: "notifDailyHour",
|
||||||
NOTIF_DAILY_MINUTE: "notifDailyMinute",
|
NOTIF_DAILY_MINUTE: "notifDailyMinute",
|
||||||
@@ -12,6 +16,27 @@ const KEYS = {
|
|||||||
NOTIF_THRESHOLD_LAST_FIRED: "notifThresholdLastFired",
|
NOTIF_THRESHOLD_LAST_FIRED: "notifThresholdLastFired",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export interface UsageCache<T> {
|
||||||
|
data: T;
|
||||||
|
lastFetchedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadJson<T>(key: string): Promise<T | null> {
|
||||||
|
const raw = await SecureStore.getItemAsync(key);
|
||||||
|
if (!raw) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw) as T;
|
||||||
|
} catch {
|
||||||
|
await SecureStore.deleteItemAsync(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveJson(key: string, value: unknown): Promise<void> {
|
||||||
|
await SecureStore.setItemAsync(key, JSON.stringify(value));
|
||||||
|
}
|
||||||
|
|
||||||
export interface NotifSettings {
|
export interface NotifSettings {
|
||||||
dailyEnabled: boolean;
|
dailyEnabled: boolean;
|
||||||
dailyHour: number;
|
dailyHour: number;
|
||||||
@@ -60,20 +85,46 @@ export async function saveCodexAuth(auth: {
|
|||||||
accessToken: string;
|
accessToken: string;
|
||||||
accountId?: string;
|
accountId?: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
await SecureStore.setItemAsync(KEYS.CODEX_AUTH, JSON.stringify(auth));
|
await saveJson(KEYS.CODEX_AUTH, auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadCodexAuth(): Promise<{
|
export async function loadCodexAuth(): Promise<{
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
accountId?: string;
|
accountId?: string;
|
||||||
} | null> {
|
} | null> {
|
||||||
const raw = await SecureStore.getItemAsync(KEYS.CODEX_AUTH);
|
return loadJson(KEYS.CODEX_AUTH);
|
||||||
if (!raw) return null;
|
}
|
||||||
return JSON.parse(raw) as { accessToken: string; accountId?: string };
|
|
||||||
|
export function loadCodexUsageCache(): Promise<UsageCache<CodexUsageResponse> | null> {
|
||||||
|
return loadJson(KEYS.CODEX_USAGE_CACHE);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveCodexUsageCache(
|
||||||
|
cache: UsageCache<CodexUsageResponse>
|
||||||
|
): Promise<void> {
|
||||||
|
return saveJson(KEYS.CODEX_USAGE_CACHE, cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearCodexUsageCache(): Promise<void> {
|
||||||
|
return SecureStore.deleteItemAsync(KEYS.CODEX_USAGE_CACHE);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadClaudeUsageCache(): Promise<UsageCache<ClaudeUsageResponse> | null> {
|
||||||
|
return loadJson(KEYS.CLAUDE_USAGE_CACHE);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveClaudeUsageCache(
|
||||||
|
cache: UsageCache<ClaudeUsageResponse>
|
||||||
|
): Promise<void> {
|
||||||
|
return saveJson(KEYS.CLAUDE_USAGE_CACHE, cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearClaudeUsageCache(): Promise<void> {
|
||||||
|
return SecureStore.deleteItemAsync(KEYS.CLAUDE_USAGE_CACHE);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveClaudeSessionKey(key: string): Promise<void> {
|
export async function saveClaudeSessionKey(key: string): Promise<void> {
|
||||||
await SecureStore.setItemAsync(KEYS.CLAUDE_SESSION_KEY, key);
|
await SecureStore.setItemAsync(KEYS.CLAUDE_SESSION_KEY, key.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadClaudeSessionKey(): Promise<string | null> {
|
export async function loadClaudeSessionKey(): Promise<string | null> {
|
||||||
@@ -81,7 +132,7 @@ export async function loadClaudeSessionKey(): Promise<string | null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function saveClaudeLastActiveOrg(orgId: string): Promise<void> {
|
export async function saveClaudeLastActiveOrg(orgId: string): Promise<void> {
|
||||||
await SecureStore.setItemAsync(KEYS.CLAUDE_LAST_ACTIVE_ORG, orgId);
|
await SecureStore.setItemAsync(KEYS.CLAUDE_LAST_ACTIVE_ORG, orgId.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadClaudeLastActiveOrg(): Promise<string | null> {
|
export async function loadClaudeLastActiveOrg(): Promise<string | null> {
|
||||||
@@ -93,10 +144,16 @@ export async function clearClaudeLastActiveOrg(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function clearCodexAuth(): Promise<void> {
|
export async function clearCodexAuth(): Promise<void> {
|
||||||
await SecureStore.deleteItemAsync(KEYS.CODEX_AUTH);
|
await Promise.all([
|
||||||
|
SecureStore.deleteItemAsync(KEYS.CODEX_AUTH),
|
||||||
|
clearCodexUsageCache(),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clearClaudeSessionKey(): Promise<void> {
|
export async function clearClaudeSessionKey(): Promise<void> {
|
||||||
await SecureStore.deleteItemAsync(KEYS.CLAUDE_SESSION_KEY);
|
await Promise.all([
|
||||||
await SecureStore.deleteItemAsync(KEYS.CLAUDE_LAST_ACTIVE_ORG);
|
SecureStore.deleteItemAsync(KEYS.CLAUDE_SESSION_KEY),
|
||||||
|
SecureStore.deleteItemAsync(KEYS.CLAUDE_LAST_ACTIVE_ORG),
|
||||||
|
clearClaudeUsageCache(),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,37 @@ export function formatResetCountdown(resetAtSeconds: number): string {
|
|||||||
return `resets in ${minutes}m`;
|
return `resets in ${minutes}m`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseTimeInput(
|
||||||
|
value: string
|
||||||
|
): { hour: number; minute: number } | null {
|
||||||
|
const match = value.trim().match(/^(\d{1,2}):(\d{2})$/);
|
||||||
|
if (!match) return null;
|
||||||
|
|
||||||
|
const hour = Number.parseInt(match[1], 10);
|
||||||
|
const minute = Number.parseInt(match[2], 10);
|
||||||
|
if (hour > 23 || minute > 59) return null;
|
||||||
|
|
||||||
|
return { hour, minute };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatTime(hour: number, minute: number): string {
|
||||||
|
return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatLastUpdated(timestamp: number, now = Date.now()): string {
|
||||||
|
const elapsedSeconds = Math.max(0, Math.floor((now - timestamp) / 1000));
|
||||||
|
if (elapsedSeconds < 60) return "Updated just now";
|
||||||
|
|
||||||
|
const minutes = Math.floor(elapsedSeconds / 60);
|
||||||
|
if (minutes < 60) return `Updated ${minutes} min ago`;
|
||||||
|
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
if (hours < 24) return `Updated ${hours} hr${hours === 1 ? "" : "s"} ago`;
|
||||||
|
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
return `Updated ${days} day${days === 1 ? "" : "s"} ago`;
|
||||||
|
}
|
||||||
|
|
||||||
export function formatResetCountdownISO(isoString: string): string {
|
export function formatResetCountdownISO(isoString: string): string {
|
||||||
const resetAtSeconds = Math.floor(new Date(isoString).getTime() / 1000);
|
const resetAtSeconds = Math.floor(new Date(isoString).getTime() / 1000);
|
||||||
return formatResetCountdown(resetAtSeconds);
|
return formatResetCountdown(resetAtSeconds);
|
||||||
|
|||||||
@@ -8,23 +8,30 @@
|
|||||||
"name": "codexbar.mobile",
|
"name": "codexbar.mobile",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@expo/metro-runtime": "~56.0.15",
|
||||||
"@expo/vector-icons": "^15.1.1",
|
"@expo/vector-icons": "^15.1.1",
|
||||||
"babel-preset-expo": "~56.0.0",
|
"babel-preset-expo": "~56.0.0",
|
||||||
"expo": "~56.0.12",
|
"expo": "~56.0.12",
|
||||||
"expo-constants": "~56.0.18",
|
"expo-constants": "~56.0.18",
|
||||||
"expo-document-picker": "~56.0.4",
|
"expo-document-picker": "~56.0.4",
|
||||||
"expo-file-system": "~56.0.8",
|
"expo-file-system": "~56.0.8",
|
||||||
|
"expo-font": "~56.0.7",
|
||||||
"expo-linking": "~56.0.14",
|
"expo-linking": "~56.0.14",
|
||||||
"expo-notifications": "~56.0.18",
|
"expo-notifications": "~56.0.18",
|
||||||
"expo-router": "~56.2.11",
|
"expo-router": "~56.2.11",
|
||||||
"expo-secure-store": "~56.0.4",
|
"expo-secure-store": "~56.0.4",
|
||||||
|
"expo-splash-screen": "~56.0.10",
|
||||||
"expo-status-bar": "~56.0.4",
|
"expo-status-bar": "~56.0.4",
|
||||||
|
"expo-system-ui": "~56.0.5",
|
||||||
"nativewind": "^4.2.6",
|
"nativewind": "^4.2.6",
|
||||||
"react": "19.2.7",
|
"react": "19.2.3",
|
||||||
|
"react-dom": "^19.2.3",
|
||||||
"react-native": "0.85.3",
|
"react-native": "0.85.3",
|
||||||
"react-native-reanimated": "4.3.1",
|
"react-native-reanimated": "4.3.1",
|
||||||
"react-native-safe-area-context": "~5.7.0",
|
"react-native-safe-area-context": "~5.7.0",
|
||||||
"react-native-screens": "4.25.2"
|
"react-native-screens": "4.25.2",
|
||||||
|
"react-native-web": "^0.21.2",
|
||||||
|
"react-native-worklets": "^0.8.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "~19.2.2",
|
"@types/react": "~19.2.2",
|
||||||
@@ -78,7 +85,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
|
||||||
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.7",
|
"@babel/code-frame": "^7.29.7",
|
||||||
"@babel/generator": "^7.29.7",
|
"@babel/generator": "^7.29.7",
|
||||||
@@ -963,6 +969,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
|
"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==",
|
"integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.29.7"
|
"@babel/helper-plugin-utils": "^7.29.7"
|
||||||
},
|
},
|
||||||
@@ -978,6 +985,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
|
"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==",
|
"integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.29.7"
|
"@babel/helper-plugin-utils": "^7.29.7"
|
||||||
},
|
},
|
||||||
@@ -1009,6 +1017,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz",
|
||||||
"integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==",
|
"integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.29.7"
|
"@babel/helper-plugin-utils": "^7.29.7"
|
||||||
},
|
},
|
||||||
@@ -1436,6 +1445,31 @@
|
|||||||
"walker": "^1.0.8"
|
"walker": "^1.0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@expo/metro-runtime": {
|
||||||
|
"version": "56.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-56.0.15.tgz",
|
||||||
|
"integrity": "sha512-WIWeVsL6kCSB57oYZdUA4MTkH7c67UFMIjdNoQzKXwxZYwBFE/xL2cGPDC3z8RWt0femzJTVxAVZUOW/hiqRzA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@expo/log-box": "^56.0.13",
|
||||||
|
"anser": "^1.4.9",
|
||||||
|
"pretty-format": "^29.7.0",
|
||||||
|
"stacktrace-parser": "^0.1.10",
|
||||||
|
"whatwg-fetch": "^3.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@expo/log-box": "^56.0.13",
|
||||||
|
"expo": "*",
|
||||||
|
"react": "*",
|
||||||
|
"react-dom": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@expo/osascript": {
|
"node_modules/@expo/osascript": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.6.0.tgz",
|
||||||
@@ -2132,6 +2166,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.85.3.tgz",
|
"resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.85.3.tgz",
|
||||||
"integrity": "sha512-fD7fxEhkJB/aF57tWoXjaAWpklfrExYZS3k6aXPP3BQ77DZY7gvf/b7dbirwjID6NVnP1JDRJyTuPBGr0K/vlw==",
|
"integrity": "sha512-fD7fxEhkJB/aF57tWoXjaAWpklfrExYZS3k6aXPP3BQ77DZY7gvf/b7dbirwjID6NVnP1JDRJyTuPBGr0K/vlw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
"@babel/plugin-proposal-export-default-from": "^7.24.7",
|
"@babel/plugin-proposal-export-default-from": "^7.24.7",
|
||||||
@@ -2309,6 +2344,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.85.3.tgz",
|
"resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.85.3.tgz",
|
||||||
"integrity": "sha512-omuKq+r7jM4XvCMIlNMPP7Up3SyB8o5EAdZtF7YXniKyq7UOMBqhYHFqgsdOXr0lT+3ADf7VCJG3sb82jlBrrQ==",
|
"integrity": "sha512-omuKq+r7jM4XvCMIlNMPP7Up3SyB8o5EAdZtF7YXniKyq7UOMBqhYHFqgsdOXr0lT+3ADf7VCJG3sb82jlBrrQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
"@react-native/babel-preset": "0.85.3",
|
"@react-native/babel-preset": "0.85.3",
|
||||||
@@ -2326,13 +2362,15 @@
|
|||||||
"version": "0.33.3",
|
"version": "0.33.3",
|
||||||
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.33.3.tgz",
|
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.33.3.tgz",
|
||||||
"integrity": "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==",
|
"integrity": "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@react-native/metro-babel-transformer/node_modules/hermes-parser": {
|
"node_modules/@react-native/metro-babel-transformer/node_modules/hermes-parser": {
|
||||||
"version": "0.33.3",
|
"version": "0.33.3",
|
||||||
"resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.33.3.tgz",
|
"resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.33.3.tgz",
|
||||||
"integrity": "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==",
|
"integrity": "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hermes-estree": "0.33.3"
|
"hermes-estree": "0.33.3"
|
||||||
}
|
}
|
||||||
@@ -2342,6 +2380,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.85.3.tgz",
|
"resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.85.3.tgz",
|
||||||
"integrity": "sha512-sVo6HepUmCcpdfozEf91lA0FjpLNNZYu/Zi9FiYiAQTK8pzATXDVTqhvdxpFrQn435p5eUTSbllvbH/KN+bnyA==",
|
"integrity": "sha512-sVo6HepUmCcpdfozEf91lA0FjpLNNZYu/Zi9FiYiAQTK8pzATXDVTqhvdxpFrQn435p5eUTSbllvbH/KN+bnyA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-native/js-polyfills": "0.85.3",
|
"@react-native/js-polyfills": "0.85.3",
|
||||||
"@react-native/metro-babel-transformer": "0.85.3",
|
"@react-native/metro-babel-transformer": "0.85.3",
|
||||||
@@ -2392,6 +2431,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
|
||||||
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.10.4",
|
"@babel/code-frame": "^7.10.4",
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
@@ -2411,6 +2451,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
@@ -2423,6 +2464,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
||||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^5.0.1",
|
"ansi-regex": "^5.0.1",
|
||||||
"ansi-styles": "^5.0.0",
|
"ansi-styles": "^5.0.0",
|
||||||
@@ -2436,7 +2478,8 @@
|
|||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@testing-library/jest-dom": {
|
"node_modules/@testing-library/jest-dom": {
|
||||||
"version": "6.9.1",
|
"version": "6.9.1",
|
||||||
@@ -2480,7 +2523,8 @@
|
|||||||
"version": "5.0.4",
|
"version": "5.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/istanbul-lib-coverage": {
|
"node_modules/@types/istanbul-lib-coverage": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
@@ -2520,7 +2564,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
|
||||||
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
|
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
@@ -2530,6 +2573,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-19.1.0.tgz",
|
||||||
"integrity": "sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ==",
|
"integrity": "sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
@@ -3026,7 +3070,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.10.38",
|
"baseline-browser-mapping": "^2.10.38",
|
||||||
"caniuse-lite": "^1.0.30001799",
|
"caniuse-lite": "^1.0.30001799",
|
||||||
@@ -3414,6 +3457,15 @@
|
|||||||
"url": "https://opencollective.com/core-js"
|
"url": "https://opencollective.com/core-js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cross-fetch": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-fetch": "^2.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -3428,6 +3480,15 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-in-js-utils": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"hyphenate-style-name": "^1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/css.escape": {
|
"node_modules/css.escape": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
|
||||||
@@ -3564,7 +3625,8 @@
|
|||||||
"version": "0.5.16",
|
"version": "0.5.16",
|
||||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/ee-first": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@@ -3674,7 +3736,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/expo/-/expo-56.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/expo/-/expo-56.0.12.tgz",
|
||||||
"integrity": "sha512-FxgdI/Yqva6iJOThZIHfvxlKPxs4EC4uScUnEswwSArR/Fj9k430O13R590LcOQTsdNsjIs+GBHwjfoAY6vmAQ==",
|
"integrity": "sha512-FxgdI/Yqva6iJOThZIHfvxlKPxs4EC4uScUnEswwSArR/Fj9k430O13R590LcOQTsdNsjIs+GBHwjfoAY6vmAQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.0",
|
"@babel/runtime": "^7.20.0",
|
||||||
"@expo/cli": "^56.1.16",
|
"@expo/cli": "^56.1.16",
|
||||||
@@ -3746,7 +3807,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-56.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-56.0.18.tgz",
|
||||||
"integrity": "sha512-8AMtbDGl/WVPnWlmbpGmvcdnNCy9E4PFnwdVwj600vljkMDPSxcAcjw8GVXEPk3PpZ+ngTqsrkltWyj0UKYAxw==",
|
"integrity": "sha512-8AMtbDGl/WVPnWlmbpGmvcdnNCy9E4PFnwdVwj600vljkMDPSxcAcjw8GVXEPk3PpZ+ngTqsrkltWyj0UKYAxw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/env": "~2.3.0"
|
"@expo/env": "~2.3.0"
|
||||||
},
|
},
|
||||||
@@ -3779,7 +3839,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-56.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-56.0.7.tgz",
|
||||||
"integrity": "sha512-hpU/vRwPzsby9lPGkA4blDqLIIXYzoWnCZHr6PxvcWbY/uPObAiyhh6q+e0WYsB65SthK+PLH95jEnVag7fwEg==",
|
"integrity": "sha512-hpU/vRwPzsby9lPGkA4blDqLIIXYzoWnCZHr6PxvcWbY/uPObAiyhh6q+e0WYsB65SthK+PLH95jEnVag7fwEg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fontfaceobserver": "^2.1.0"
|
"fontfaceobserver": "^2.1.0"
|
||||||
},
|
},
|
||||||
@@ -3805,7 +3864,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-56.0.14.tgz",
|
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-56.0.14.tgz",
|
||||||
"integrity": "sha512-IvVQHWC+Cj4fK5qD3iEVYqpU2a4rLW0IpAAlGJ4MH+H1fyZiHh3eN6qg2WmoclOEPfYATSuEa+dQT6wfgVpXlQ==",
|
"integrity": "sha512-IvVQHWC+Cj4fK5qD3iEVYqpU2a4rLW0IpAAlGJ4MH+H1fyZiHh3eN6qg2WmoclOEPfYATSuEa+dQT6wfgVpXlQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"expo-constants": "~56.0.18",
|
"expo-constants": "~56.0.18",
|
||||||
"invariant": "^2.2.4"
|
"invariant": "^2.2.4"
|
||||||
@@ -3952,31 +4010,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-router/node_modules/@expo/metro-runtime": {
|
|
||||||
"version": "56.0.15",
|
|
||||||
"resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-56.0.15.tgz",
|
|
||||||
"integrity": "sha512-WIWeVsL6kCSB57oYZdUA4MTkH7c67UFMIjdNoQzKXwxZYwBFE/xL2cGPDC3z8RWt0femzJTVxAVZUOW/hiqRzA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@expo/log-box": "^56.0.13",
|
|
||||||
"anser": "^1.4.9",
|
|
||||||
"pretty-format": "^29.7.0",
|
|
||||||
"stacktrace-parser": "^0.1.10",
|
|
||||||
"whatwg-fetch": "^3.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@expo/log-box": "^56.0.13",
|
|
||||||
"expo": "*",
|
|
||||||
"react": "*",
|
|
||||||
"react-dom": "*",
|
|
||||||
"react-native": "*"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"react-dom": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/expo-router/node_modules/@expo/ui": {
|
"node_modules/expo-router/node_modules/@expo/ui": {
|
||||||
"version": "56.0.18",
|
"version": "56.0.18",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/ui/-/ui-56.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/ui/-/ui-56.0.18.tgz",
|
||||||
@@ -4060,11 +4093,24 @@
|
|||||||
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-56.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-56.0.5.tgz",
|
||||||
"integrity": "sha512-SmM2p2g3Jrktpiazcst+OxhjSzOHXKAY4BPURHYHXvApzzoybMmrNF4IEZ8DKZ145BhSe4ydAmlEFCRTsdtgUQ==",
|
"integrity": "sha512-SmM2p2g3Jrktpiazcst+OxhjSzOHXKAY4BPURHYHXvApzzoybMmrNF4IEZ8DKZ145BhSe4ydAmlEFCRTsdtgUQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.16.0"
|
"node": ">=20.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-splash-screen": {
|
||||||
|
"version": "56.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-56.0.10.tgz",
|
||||||
|
"integrity": "sha512-vDIlo8hzt9HlCZQ0kSY66v83D1WEXOJbVMeyPDfXDu9tbDdPMNUyDpi4WGJXikAjxnAKfbt5Mv5NnEbxINy+VA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@expo/config-plugins": "~56.0.8",
|
||||||
|
"@expo/image-utils": "^0.10.1",
|
||||||
|
"xml2js": "0.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-status-bar": {
|
"node_modules/expo-status-bar": {
|
||||||
"version": "56.0.4",
|
"version": "56.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-56.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-56.0.4.tgz",
|
||||||
@@ -4092,6 +4138,26 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-system-ui": {
|
||||||
|
"version": "56.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-system-ui/-/expo-system-ui-56.0.5.tgz",
|
||||||
|
"integrity": "sha512-n1MmnUArV4cc3gVed9fGtluPme00PE9axKVx+NHbKxHFMam5l4GcOI7PxbYKFNx8o7WA1LRD7eLW33agmZrxGg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-native/normalize-colors": "0.85.3",
|
||||||
|
"debug": "^4.3.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*",
|
||||||
|
"react-native": "*",
|
||||||
|
"react-native-web": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-native-web": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo/node_modules/@expo/cli": {
|
"node_modules/expo/node_modules/@expo/cli": {
|
||||||
"version": "56.1.16",
|
"version": "56.1.16",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-56.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-56.1.16.tgz",
|
||||||
@@ -4370,7 +4436,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||||
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
},
|
},
|
||||||
@@ -4457,6 +4522,36 @@
|
|||||||
"bser": "2.1.1"
|
"bser": "2.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fbjs": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cross-fetch": "^3.1.5",
|
||||||
|
"fbjs-css-vars": "^1.0.0",
|
||||||
|
"loose-envify": "^1.0.0",
|
||||||
|
"object-assign": "^4.1.0",
|
||||||
|
"promise": "^7.1.1",
|
||||||
|
"setimmediate": "^1.0.5",
|
||||||
|
"ua-parser-js": "^1.0.35"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fbjs-css-vars": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/fbjs/node_modules/promise": {
|
||||||
|
"version": "7.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
|
||||||
|
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"asap": "~2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fetch-nodeshim": {
|
"node_modules/fetch-nodeshim": {
|
||||||
"version": "0.4.10",
|
"version": "0.4.10",
|
||||||
"resolved": "https://registry.npmjs.org/fetch-nodeshim/-/fetch-nodeshim-0.4.10.tgz",
|
"resolved": "https://registry.npmjs.org/fetch-nodeshim/-/fetch-nodeshim-0.4.10.tgz",
|
||||||
@@ -4734,6 +4829,12 @@
|
|||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hyphenate-style-name": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@@ -4773,6 +4874,15 @@
|
|||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/inline-style-prefixer": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"css-in-js-utils": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/invariant": {
|
"node_modules/invariant": {
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||||
@@ -4986,7 +5096,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
@@ -5489,6 +5598,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"lz-string": "bin/bin.js"
|
"lz-string": "bin/bin.js"
|
||||||
}
|
}
|
||||||
@@ -5968,6 +6078,26 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-fetch": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-url": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "4.x || >=6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"encoding": "^0.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"encoding": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-forge": {
|
"node_modules/node-forge": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz",
|
||||||
@@ -6358,7 +6488,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.12",
|
"nanoid": "^3.3.12",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -6619,11 +6748,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.2.7",
|
"version": "19.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||||
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
|
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -6639,16 +6767,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "19.2.7",
|
"version": "19.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||||
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
|
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^19.2.7"
|
"react": "^19.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-fast-compare": {
|
"node_modules/react-fast-compare": {
|
||||||
@@ -6680,7 +6807,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.85.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.85.3.tgz",
|
||||||
"integrity": "sha512-HN/fGC+3nZVcDNcw7gfbM/DuqZAvI9Mz+/SxuhODaua4JY0BPzhfTzWXRyTR4mRgMHmShTPpH2PYMTxvZrsdZA==",
|
"integrity": "sha512-HN/fGC+3nZVcDNcw7gfbM/DuqZAvI9Mz+/SxuhODaua4JY0BPzhfTzWXRyTR4mRgMHmShTPpH2PYMTxvZrsdZA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-native/assets-registry": "0.85.3",
|
"@react-native/assets-registry": "0.85.3",
|
||||||
"@react-native/codegen": "0.85.3",
|
"@react-native/codegen": "0.85.3",
|
||||||
@@ -7052,7 +7178,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.3.1.tgz",
|
||||||
"integrity": "sha512-KhGsS0YkCA+gusgyzlf9hnqzVPIR398KTpqXyqq/+yYJJPAvyEEPKcxlB0xtOOXSMrR2A9uRKVARVQhZwrOh+Q==",
|
"integrity": "sha512-KhGsS0YkCA+gusgyzlf9hnqzVPIR398KTpqXyqq/+yYJJPAvyEEPKcxlB0xtOOXSMrR2A9uRKVARVQhZwrOh+Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-native-is-edge-to-edge": "^1.3.1",
|
"react-native-is-edge-to-edge": "^1.3.1",
|
||||||
"semver": "^7.7.3"
|
"semver": "^7.7.3"
|
||||||
@@ -7068,7 +7193,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.7.0.tgz",
|
||||||
"integrity": "sha512-/9/MtQz8ODphjsLdZ+GZAIcC/RtoqW9EeShf7Uvnfgm/pzYrJ75y3PV/J1wuAV1T5Dye5ygq4EAW20RoBq0ABQ==",
|
"integrity": "sha512-/9/MtQz8ODphjsLdZ+GZAIcC/RtoqW9EeShf7Uvnfgm/pzYrJ75y3PV/J1wuAV1T5Dye5ygq4EAW20RoBq0ABQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "*",
|
"react": "*",
|
||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
@@ -7088,12 +7212,43 @@
|
|||||||
"react-native": ">=0.82.0"
|
"react-native": ">=0.82.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-web": {
|
||||||
|
"version": "0.21.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
|
||||||
|
"integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.18.6",
|
||||||
|
"@react-native/normalize-colors": "^0.74.1",
|
||||||
|
"fbjs": "^3.0.4",
|
||||||
|
"inline-style-prefixer": "^7.0.1",
|
||||||
|
"memoize-one": "^6.0.0",
|
||||||
|
"nullthrows": "^1.1.1",
|
||||||
|
"postcss-value-parser": "^4.2.0",
|
||||||
|
"styleq": "^0.1.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-native-web/node_modules/@react-native/normalize-colors": {
|
||||||
|
"version": "0.74.89",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.74.89.tgz",
|
||||||
|
"integrity": "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/react-native-web/node_modules/memoize-one": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/react-native-worklets": {
|
"node_modules/react-native-worklets": {
|
||||||
"version": "0.8.3",
|
"version": "0.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.8.3.tgz",
|
||||||
"integrity": "sha512-oCBJROyLU7yG/1R8s0INMflygTH71bx+5XcYkH0CM938TlhSoVbiunE1WVW5FZa51vwYqfLie/IXMX2s1Kh3eg==",
|
"integrity": "sha512-oCBJROyLU7yG/1R8s0INMflygTH71bx+5XcYkH0CM938TlhSoVbiunE1WVW5FZa51vwYqfLie/IXMX2s1Kh3eg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-transform-arrow-functions": "^7.27.1",
|
"@babel/plugin-transform-arrow-functions": "^7.27.1",
|
||||||
"@babel/plugin-transform-class-properties": "^7.27.1",
|
"@babel/plugin-transform-class-properties": "^7.27.1",
|
||||||
@@ -7128,7 +7283,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
||||||
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
|
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -7541,6 +7695,12 @@
|
|||||||
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
|
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/setimmediate": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/setprototypeof": {
|
"node_modules/setprototypeof": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
@@ -7777,6 +7937,12 @@
|
|||||||
"integrity": "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==",
|
"integrity": "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/styleq": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/styleq/-/styleq-0.1.3.tgz",
|
||||||
|
"integrity": "sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/sucrase": {
|
"node_modules/sucrase": {
|
||||||
"version": "3.35.1",
|
"version": "3.35.1",
|
||||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
|
||||||
@@ -7850,7 +8016,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
||||||
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
@@ -7988,7 +8153,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -8029,6 +8193,12 @@
|
|||||||
"integrity": "sha512-FWAPzCIHZHnrE/5/w9MPk0kK25hSQSH2IKhYh9PyjS3SG/+IEMvlwIHbhz+oF7xl54I+ueZlVnMjyzdSwLmAwA==",
|
"integrity": "sha512-FWAPzCIHZHnrE/5/w9MPk0kK25hSQSH2IKhYh9PyjS3SG/+IEMvlwIHbhz+oF7xl54I+ueZlVnMjyzdSwLmAwA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tr46": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
|
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ts-interface-checker": {
|
"node_modules/ts-interface-checker": {
|
||||||
"version": "0.1.13",
|
"version": "0.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||||
@@ -8056,7 +8226,6 @@
|
|||||||
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -8065,6 +8234,32 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ua-parser-js": {
|
||||||
|
"version": "1.0.41",
|
||||||
|
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz",
|
||||||
|
"integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/ua-parser-js"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paypal",
|
||||||
|
"url": "https://paypal.me/faisalman"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/faisalman"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"ua-parser-js": "script/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "8.3.0",
|
"version": "8.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
|
||||||
@@ -8288,12 +8483,28 @@
|
|||||||
"defaults": "^1.0.3"
|
"defaults": "^1.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/webidl-conversions": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
"node_modules/whatwg-fetch": {
|
"node_modules/whatwg-fetch": {
|
||||||
"version": "3.6.20",
|
"version": "3.6.20",
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
|
||||||
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
|
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/whatwg-url": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "~0.0.3",
|
||||||
|
"webidl-conversions": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/whatwg-url-minimum": {
|
"node_modules/whatwg-url-minimum": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-url-minimum/-/whatwg-url-minimum-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-url-minimum/-/whatwg-url-minimum-0.1.2.tgz",
|
||||||
|
|||||||
@@ -3,23 +3,30 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "expo-router/entry",
|
"main": "expo-router/entry",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@expo/metro-runtime": "~56.0.15",
|
||||||
"@expo/vector-icons": "^15.1.1",
|
"@expo/vector-icons": "^15.1.1",
|
||||||
"babel-preset-expo": "~56.0.0",
|
"babel-preset-expo": "~56.0.0",
|
||||||
"expo": "~56.0.12",
|
"expo": "~56.0.12",
|
||||||
"expo-constants": "~56.0.18",
|
"expo-constants": "~56.0.18",
|
||||||
"expo-document-picker": "~56.0.4",
|
"expo-document-picker": "~56.0.4",
|
||||||
"expo-file-system": "~56.0.8",
|
"expo-file-system": "~56.0.8",
|
||||||
|
"expo-font": "~56.0.7",
|
||||||
"expo-linking": "~56.0.14",
|
"expo-linking": "~56.0.14",
|
||||||
"expo-notifications": "~56.0.18",
|
"expo-notifications": "~56.0.18",
|
||||||
"expo-router": "~56.2.11",
|
"expo-router": "~56.2.11",
|
||||||
"expo-secure-store": "~56.0.4",
|
"expo-secure-store": "~56.0.4",
|
||||||
|
"expo-splash-screen": "~56.0.10",
|
||||||
"expo-status-bar": "~56.0.4",
|
"expo-status-bar": "~56.0.4",
|
||||||
|
"expo-system-ui": "~56.0.5",
|
||||||
"nativewind": "^4.2.6",
|
"nativewind": "^4.2.6",
|
||||||
"react": "19.2.7",
|
"react": "19.2.3",
|
||||||
|
"react-dom": "^19.2.3",
|
||||||
"react-native": "0.85.3",
|
"react-native": "0.85.3",
|
||||||
"react-native-reanimated": "4.3.1",
|
"react-native-reanimated": "4.3.1",
|
||||||
"react-native-safe-area-context": "~5.7.0",
|
"react-native-safe-area-context": "~5.7.0",
|
||||||
"react-native-screens": "4.25.2"
|
"react-native-screens": "4.25.2",
|
||||||
|
"react-native-web": "^0.21.2",
|
||||||
|
"react-native-worklets": "^0.8.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "~19.2.2",
|
"@types/react": "~19.2.2",
|
||||||
|
|||||||