237 lines
9.3 KiB
TypeScript
237 lines
9.3 KiB
TypeScript
import { useState, useCallback, useEffect } from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
TouchableOpacity,
|
|
TextInput,
|
|
KeyboardAvoidingView,
|
|
ScrollView,
|
|
} from "react-native";
|
|
import { SafeAreaView } from "react-native-safe-area-context";
|
|
import { router } from "expo-router";
|
|
import { MaterialIcons } from "@expo/vector-icons";
|
|
import {
|
|
saveClaudeSessionKey,
|
|
saveClaudeLastActiveOrg,
|
|
clearClaudeLastActiveOrg,
|
|
loadClaudeSessionKey,
|
|
loadClaudeLastActiveOrg,
|
|
} from "@/lib/storage";
|
|
import {
|
|
isClaudeSessionKeyValid,
|
|
validateClaudeSessionKey,
|
|
} from "@/lib/claudeCredentials";
|
|
import { COLORS } from "@/lib/constants";
|
|
|
|
export default function OnboardingClaudeScreen() {
|
|
const [savedKey, setSavedKey] = useState<string | null>(null);
|
|
const [savedOrg, setSavedOrg] = useState<string | null>(null);
|
|
const [pendingKey, setPendingKey] = useState("");
|
|
const [pendingOrg, setPendingOrg] = useState("");
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
Promise.all([loadClaudeSessionKey(), loadClaudeLastActiveOrg()]).then(
|
|
([k, o]) => {
|
|
if (k) setSavedKey(k);
|
|
if (o) setSavedOrg(o);
|
|
}
|
|
);
|
|
}, []);
|
|
|
|
const canSave = isClaudeSessionKeyValid(pendingKey);
|
|
const inlineError =
|
|
error ?? (pendingKey ? validateClaudeSessionKey(pendingKey) : null);
|
|
|
|
const handleSave = useCallback(async () => {
|
|
const trimmed = pendingKey.trim();
|
|
const validationError = validateClaudeSessionKey(trimmed);
|
|
if (validationError) {
|
|
setError(validationError);
|
|
return;
|
|
}
|
|
const orgTrimmed = pendingOrg.trim() || undefined;
|
|
await saveClaudeSessionKey(trimmed);
|
|
if (orgTrimmed) await saveClaudeLastActiveOrg(orgTrimmed);
|
|
else await clearClaudeLastActiveOrg();
|
|
setSavedKey(trimmed);
|
|
setSavedOrg(orgTrimmed ?? null);
|
|
setPendingKey("");
|
|
setPendingOrg("");
|
|
setError(null);
|
|
}, [pendingKey, pendingOrg]);
|
|
|
|
return (
|
|
<SafeAreaView className="flex-1 bg-neutral-950">
|
|
<KeyboardAvoidingView
|
|
className="flex-1"
|
|
behavior={process.env.EXPO_OS === "ios" ? "padding" : "height"}
|
|
>
|
|
<ScrollView
|
|
className="flex-1"
|
|
contentContainerStyle={{ flexGrow: 1 }}
|
|
keyboardShouldPersistTaps="handled"
|
|
>
|
|
<View className="flex-1 px-8 justify-between py-8">
|
|
{/* Top: back + step indicator */}
|
|
<View className="flex-row items-center justify-between">
|
|
<TouchableOpacity
|
|
onPress={() => router.back()}
|
|
className="w-9 h-9 rounded-xl bg-neutral-900 items-center justify-center"
|
|
>
|
|
<MaterialIcons name="arrow-back" size={18} color="#a3a3a3" />
|
|
</TouchableOpacity>
|
|
<View className="flex-row items-center gap-x-2">
|
|
<View
|
|
className="w-6 h-6 rounded-full items-center justify-center"
|
|
style={{ backgroundColor: COLORS.codex }}
|
|
>
|
|
<MaterialIcons name="check" size={14} color="white" />
|
|
</View>
|
|
<View className="w-12 h-0.5" style={{ backgroundColor: COLORS.claude }} />
|
|
<View
|
|
className="w-6 h-6 rounded-full items-center justify-center"
|
|
style={{ backgroundColor: COLORS.claude }}
|
|
>
|
|
<Text className="text-white text-xs font-bold">2</Text>
|
|
</View>
|
|
</View>
|
|
<View className="w-9" />
|
|
</View>
|
|
|
|
{/* Content */}
|
|
<View>
|
|
<View
|
|
className="w-16 h-16 rounded-2xl items-center justify-center mb-6"
|
|
style={{ backgroundColor: `${COLORS.claude}22` }}
|
|
>
|
|
<MaterialIcons name="psychology" size={32} color={COLORS.claude} />
|
|
</View>
|
|
<Text className="text-3xl font-bold text-white mb-3">
|
|
Connect Claude.ai
|
|
</Text>
|
|
<Text className="text-neutral-400 text-base leading-relaxed mb-8">
|
|
Enter your session key to monitor your Claude.ai usage windows.
|
|
</Text>
|
|
|
|
{savedKey ? (
|
|
<View className="bg-neutral-900 rounded-2xl border border-neutral-800 mb-4 overflow-hidden">
|
|
<View className="flex-row items-center gap-x-3 p-4">
|
|
<View
|
|
className="w-10 h-10 rounded-xl items-center justify-center"
|
|
style={{ backgroundColor: `${COLORS.claude}22` }}
|
|
>
|
|
<MaterialIcons name="check-circle" size={22} color={COLORS.claude} />
|
|
</View>
|
|
<View className="flex-1">
|
|
<Text className="text-white font-semibold">Connected</Text>
|
|
<Text className="text-neutral-500 text-sm font-mono">
|
|
{savedKey.slice(0, 16)}…
|
|
</Text>
|
|
</View>
|
|
<TouchableOpacity
|
|
onPress={() => {
|
|
setSavedKey(null);
|
|
setSavedOrg(null);
|
|
setPendingKey("");
|
|
setPendingOrg("");
|
|
}}
|
|
>
|
|
<Text className="text-neutral-400 text-xs">Change</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
{savedOrg && (
|
|
<View className="border-t border-neutral-800 px-4 py-3">
|
|
<Text className="text-neutral-500 text-xs font-mono">
|
|
org: {savedOrg.slice(0, 8)}…
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
) : (
|
|
<View>
|
|
{inlineError && (
|
|
<View className="bg-red-950 border border-red-900 rounded-xl px-4 py-3 mb-4">
|
|
<Text selectable className="text-red-400 text-sm">{inlineError}</Text>
|
|
</View>
|
|
)}
|
|
<TextInput
|
|
value={pendingKey}
|
|
onChangeText={(t) => {
|
|
setPendingKey(t);
|
|
setError(null);
|
|
}}
|
|
placeholder="sk-ant-… (required)"
|
|
placeholderTextColor="#525252"
|
|
autoCapitalize="none"
|
|
autoCorrect={false}
|
|
secureTextEntry
|
|
className="border border-neutral-800 rounded-xl px-4 py-3.5 text-sm text-white bg-neutral-900 mb-3"
|
|
/>
|
|
<TextInput
|
|
value={pendingOrg}
|
|
onChangeText={setPendingOrg}
|
|
placeholder="lastActiveOrg UUID (optional)"
|
|
placeholderTextColor="#525252"
|
|
autoCapitalize="none"
|
|
autoCorrect={false}
|
|
className="border border-neutral-800 rounded-xl px-4 py-3.5 text-sm text-white bg-neutral-900 mb-3"
|
|
/>
|
|
<TouchableOpacity
|
|
onPress={handleSave}
|
|
disabled={!canSave}
|
|
className="rounded-2xl py-4 items-center mb-4"
|
|
style={{
|
|
backgroundColor: canSave ? COLORS.claude : "#1a1a1a",
|
|
}}
|
|
>
|
|
<Text
|
|
className="font-bold text-base"
|
|
style={{ color: canSave ? "white" : "#525252" }}
|
|
>
|
|
Save Session Key
|
|
</Text>
|
|
</TouchableOpacity>
|
|
<View className="bg-neutral-900 rounded-xl p-4 border border-neutral-800">
|
|
<Text className="text-neutral-400 text-xs font-semibold uppercase tracking-widest mb-2">
|
|
How to find your credentials
|
|
</Text>
|
|
<Text className="text-neutral-500 text-sm leading-relaxed">
|
|
Open Claude.ai in Chrome → DevTools (F12) → Application → Cookies → claude.ai{"\n"}
|
|
Copy <Text className="text-neutral-300 font-mono">sessionKey</Text> and optionally <Text className="text-neutral-300 font-mono">lastActiveOrg</Text>
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
{/* Bottom actions */}
|
|
<View className="gap-y-3">
|
|
<TouchableOpacity
|
|
onPress={() => router.push("/onboarding/done")}
|
|
className="rounded-2xl py-4 items-center"
|
|
style={{
|
|
backgroundColor: savedKey ? COLORS.claude : "#1a1a1a",
|
|
}}
|
|
>
|
|
<Text
|
|
className="font-bold text-base"
|
|
style={{ color: savedKey ? "white" : "#a3a3a3" }}
|
|
>
|
|
Continue
|
|
</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
onPress={() => router.push("/onboarding/done")}
|
|
className="py-3 items-center"
|
|
>
|
|
<Text className="text-neutral-600 text-sm">Skip for now</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</ScrollView>
|
|
</KeyboardAvoidingView>
|
|
</SafeAreaView>
|
|
);
|
|
}
|