import { Feather } from "@expo/vector-icons"; import axios from "axios"; import * as Haptics from "expo-haptics"; import React, { useEffect, useState } from "react"; import { ActivityIndicator, Alert, Platform, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, } from "react-native"; import { KeyboardAwareScrollView } from "react-native-keyboard-controller"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { usePostiz, DEFAULT_BASE_URL } from "@/context/PostizContext"; import { useColors } from "@/hooks/useColors"; import { extractError } from "@/lib/extractError"; export default function SettingsScreen() { const colors = useColors(); const insets = useSafeAreaInsets(); const { apiKey, baseUrl, isConfigured, saveSettings, clearSettings } = usePostiz(); const [inputKey, setInputKey] = useState(apiKey); const [inputUrl, setInputUrl] = useState(baseUrl || DEFAULT_BASE_URL); const [showKey, setShowKey] = useState(false); const [validating, setValidating] = useState(false); const [saving, setSaving] = useState(false); const [validationStatus, setValidationStatus] = useState<"idle" | "ok" | "error">("idle"); const [errorDetail, setErrorDetail] = useState(""); useEffect(() => { setInputKey(apiKey); setInputUrl(baseUrl || DEFAULT_BASE_URL); }, [apiKey, baseUrl]); const handleValidate = async () => { if (!inputKey.trim() || !inputUrl.trim()) { Alert.alert("Missing fields", "Please enter both API key and base URL."); return; } setValidating(true); setValidationStatus("idle"); setErrorDetail(""); const cleanUrl = inputUrl.trim().replace(/\/$/, ""); const authVariants = [ inputKey.trim(), `Bearer ${inputKey.trim()}`, ]; let lastError: string = ""; for (const authHeader of authVariants) { try { await axios.get(`${cleanUrl}/integrations`, { headers: { Authorization: authHeader }, timeout: 10000, maxRedirects: 0, }); setValidationStatus("ok"); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); setValidating(false); return; } catch (err: unknown) { if (axios.isAxiosError(err)) { const status = err.response?.status; if (status === 307 || status === 301 || status === 302 || status === 308) { const location = err.response?.headers?.location ?? "unknown"; lastError = `HTTP ${status} redirect → ${location}. The API rejected the request and redirected to login. Check the Authorization header format or the base URL.`; continue; } if (status === 401 || status === 403) { lastError = `HTTP ${status}: Invalid or expired API key.`; continue; } } lastError = extractError(err); } } setErrorDetail(lastError); setValidationStatus("error"); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); setValidating(false); }; const handleSave = async () => { if (!inputKey.trim() || !inputUrl.trim()) { Alert.alert("Missing fields", "Please enter both API key and base URL."); return; } setSaving(true); try { await saveSettings(inputKey.trim(), inputUrl.trim().replace(/\/$/, "")); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); Alert.alert("Saved", "Settings saved successfully."); } catch (err: unknown) { Alert.alert("Error", `Failed to save settings.\n${extractError(err)}`); } finally { setSaving(false); } }; const handleClear = () => { Alert.alert( "Disconnect", "Remove your API key and disconnect from Postiz?", [ { text: "Cancel", style: "cancel" }, { text: "Disconnect", style: "destructive", onPress: async () => { await clearSettings(); setInputKey(""); setInputUrl(DEFAULT_BASE_URL); setValidationStatus("idle"); setErrorDetail(""); }, }, ] ); }; return ( {!isConfigured && ( Connect to your Postiz instance to get started )} {isConfigured && ( Connected to Postiz )} BASE URL { setInputUrl(t); setValidationStatus("idle"); setErrorDetail(""); }} autoCapitalize="none" autoCorrect={false} keyboardType="url" /> API KEY { setInputKey(t); setValidationStatus("idle"); setErrorDetail(""); }} secureTextEntry={!showKey} autoCapitalize="none" autoCorrect={false} /> setShowKey((v) => !v)} activeOpacity={0.7}> {validationStatus === "ok" && ( Connection successful )} {validationStatus === "error" && ( Could not connect {!!errorDetail && ( {errorDetail} )} )} {validating ? ( ) : ( <> Test Connection )} {saving ? ( ) : ( <> Save Settings )} {isConfigured && ( Disconnect )} Your API key is stored securely on this device and never transmitted to third parties. ); } const styles = StyleSheet.create({ container: { paddingHorizontal: 20, gap: 16, }, banner: { flexDirection: "row", alignItems: "center", gap: 10, paddingHorizontal: 14, paddingVertical: 12, borderRadius: 12, borderWidth: 1, }, bannerText: { fontSize: 13, fontFamily: "Inter_500Medium", flex: 1, }, connectedBadge: { flexDirection: "row", alignItems: "center", gap: 6, paddingHorizontal: 12, paddingVertical: 8, borderRadius: 10, borderWidth: 1, alignSelf: "flex-start", }, connectedText: { fontSize: 12, fontFamily: "Inter_600SemiBold", }, section: { gap: 8, }, label: { fontSize: 11, fontFamily: "Inter_600SemiBold", letterSpacing: 0.8, marginLeft: 2, }, inputWrap: { flexDirection: "row", alignItems: "center", borderRadius: 12, borderWidth: 1, paddingHorizontal: 14, paddingVertical: 12, gap: 10, }, inputIcon: { flexShrink: 0, }, input: { flex: 1, fontSize: 14, fontFamily: "Inter_400Regular", }, validationRow: { flexDirection: "row", alignItems: "center", gap: 6, marginLeft: 2, }, validationText: { fontSize: 12, fontFamily: "Inter_400Regular", }, errorBox: { borderRadius: 10, borderWidth: 1, padding: 12, gap: 6, }, errorHeader: { flexDirection: "row", alignItems: "center", gap: 6, }, errorTitle: { fontSize: 12, fontFamily: "Inter_600SemiBold", }, errorScroll: { maxHeight: 80, }, errorDetail: { fontSize: 11, fontFamily: "Inter_400Regular", lineHeight: 16, }, validateBtn: { flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 8, paddingVertical: 12, borderRadius: 12, borderWidth: 1, }, validateText: { fontSize: 14, fontFamily: "Inter_600SemiBold", }, saveBtn: { flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 8, paddingVertical: 14, borderRadius: 14, }, saveText: { fontSize: 15, fontFamily: "Inter_600SemiBold", }, clearBtn: { flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 8, paddingVertical: 12, borderRadius: 12, borderWidth: 1, }, clearText: { fontSize: 14, fontFamily: "Inter_500Medium", }, footer: { marginTop: 8, }, footerText: { fontSize: 12, fontFamily: "Inter_400Regular", textAlign: "center", lineHeight: 18, }, });