import { Feather } from "@expo/vector-icons"; import axios from "axios"; import * as Haptics from "expo-haptics"; import React, { 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 { PostizWorkspace, DEFAULT_BASE_URL, usePostiz } from "@/context/PostizContext"; import { useColors } from "@/hooks/useColors"; import { extractError } from "@/lib/extractError"; type FormState = { id?: string; name: string; url: string; key: string; }; const EMPTY_FORM: FormState = { name: "", url: DEFAULT_BASE_URL, key: "" }; export default function SettingsScreen() { const colors = useColors(); const insets = useSafeAreaInsets(); const { workspaces, isConfigured, addWorkspace, updateWorkspace, removeWorkspace } = usePostiz(); const [form, setForm] = useState(null); 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(""); const openAdd = () => { setForm(EMPTY_FORM); setShowKey(false); resetValidation(); }; const openEdit = (ws: PostizWorkspace) => { setForm({ id: ws.id, name: ws.name, url: ws.baseUrl, key: ws.apiKey }); setShowKey(false); resetValidation(); }; const closeForm = () => { setForm(null); resetValidation(); }; const resetValidation = () => { setValidationStatus("idle"); setErrorDetail(""); }; const patchForm = (patch: Partial) => { setForm((prev) => (prev ? { ...prev, ...patch } : prev)); resetValidation(); }; const handleValidate = async () => { if (!form?.key.trim() || !form?.url.trim()) { Alert.alert("Missing fields", "Please enter both API key and base URL."); return; } setValidating(true); resetValidation(); const cleanUrl = form.url.trim().replace(/\/$/, ""); const variants = [form.key.trim(), `Bearer ${form.key.trim()}`]; let lastError = ""; for (const auth of variants) { try { await axios.get(`${cleanUrl}/integrations`, { headers: { Authorization: auth }, timeout: 10000, maxRedirects: 0, }); setValidationStatus("ok"); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); setValidating(false); return; } catch (err: unknown) { if (axios.isAxiosError(err)) { const s = err.response?.status; if (s === 307 || s === 301 || s === 302 || s === 308) { const loc = err.response?.headers?.location ?? "unknown"; lastError = `HTTP ${s} redirect → ${loc}. Check the Authorization format or base URL.`; continue; } if (s === 401 || s === 403) { lastError = `HTTP ${s}: Invalid or expired API key.`; continue; } } lastError = extractError(err); } } setErrorDetail(lastError); setValidationStatus("error"); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); setValidating(false); }; const handleSave = async () => { if (!form) return; if (!form.name.trim()) { Alert.alert("Missing name", "Please enter a name for this workspace."); return; } if (!form.key.trim() || !form.url.trim()) { Alert.alert("Missing fields", "Please enter both API key and base URL."); return; } setSaving(true); try { const ws = { name: form.name.trim(), apiKey: form.key.trim(), baseUrl: form.url.trim().replace(/\/$/, "") }; if (form.id) { await updateWorkspace({ ...ws, id: form.id }); } else { await addWorkspace(ws); } Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); closeForm(); } catch (err: unknown) { Alert.alert("Error", `Failed to save.\n${extractError(err)}`); } finally { setSaving(false); } }; const handleDelete = (ws: PostizWorkspace) => { Alert.alert( "Remove workspace", `Remove "${ws.name}"? Channels from this workspace will no longer be available.`, [ { text: "Cancel", style: "cancel" }, { text: "Remove", style: "destructive", onPress: async () => { if (form?.id === ws.id) closeForm(); await removeWorkspace(ws.id); }, }, ] ); }; return ( {/* Status banner */} {!isConfigured ? ( Add a workspace to get started ) : ( {workspaces.length} workspace{workspaces.length > 1 ? "s" : ""} configured )} {/* Workspace cards */} {workspaces.map((ws) => ( {ws.name} {ws.baseUrl.replace(/^https?:\/\//, "").replace(/\/api.*$/, "")} openEdit(ws)} activeOpacity={0.7} style={styles.iconBtn}> handleDelete(ws)} activeOpacity={0.7} style={styles.iconBtn}> ))} {/* Add workspace button */} {!form && ( Add workspace )} {/* Add / Edit form */} {form && ( {form.id ? "Edit workspace" : "Add workspace"} {/* Name */} NAME patchForm({ name: t })} autoCorrect={false} /> {/* Base URL */} BASE URL patchForm({ url: t })} autoCapitalize="none" autoCorrect={false} keyboardType="url" /> {/* API Key */} API KEY patchForm({ key: t })} secureTextEntry={!showKey} autoCapitalize="none" autoCorrect={false} /> setShowKey((v) => !v)} activeOpacity={0.7}> {validationStatus === "ok" && ( Connection successful )} {validationStatus === "error" && ( Could not connect {!!errorDetail && ( {errorDetail} )} )} {/* Form actions */} {validating ? ( ) : ( <> Test Connection )} Cancel {saving ? ( ) : ( <> {form.id ? "Update" : "Save"} )} )} API keys are stored securely on this device and never transmitted to third parties. ); } const styles = StyleSheet.create({ container: { paddingHorizontal: 20, gap: 14 }, 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" }, wsCard: { borderRadius: 14, borderWidth: 1, overflow: "hidden" }, wsCardHeader: { flexDirection: "row", alignItems: "center", paddingHorizontal: 14, paddingVertical: 12 }, wsCardLeft: { flex: 1, flexDirection: "row", alignItems: "center", gap: 12 }, wsIcon: { width: 32, height: 32, borderRadius: 10, alignItems: "center", justifyContent: "center" }, wsName: { fontSize: 14, fontFamily: "Inter_600SemiBold" }, wsUrl: { fontSize: 11, fontFamily: "Inter_400Regular", marginTop: 1 }, wsCardActions: { flexDirection: "row", gap: 4 }, iconBtn: { padding: 8 }, addBtn: { flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 8, paddingVertical: 13, borderRadius: 14, borderWidth: 1, borderStyle: "dashed", }, addBtnText: { fontSize: 14, fontFamily: "Inter_600SemiBold" }, formCard: { borderRadius: 14, borderWidth: 1, padding: 16, gap: 14 }, formTitle: { fontSize: 15, fontFamily: "Inter_600SemiBold" }, fieldGroup: { gap: 6 }, label: { fontSize: 11, fontFamily: "Inter_600SemiBold", letterSpacing: 0.8, marginLeft: 2 }, inputWrap: { flexDirection: "row", alignItems: "center", borderRadius: 10, borderWidth: 1, paddingHorizontal: 12, paddingVertical: 11, gap: 10, }, input: { flex: 1, fontSize: 14, fontFamily: "Inter_400Regular" }, validRow: { flexDirection: "row", alignItems: "center", gap: 6, marginLeft: 2 }, validText: { 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: 11, borderRadius: 10, borderWidth: 1, }, validateText: { fontSize: 14, fontFamily: "Inter_600SemiBold" }, formBtnsRow: { flexDirection: "row", gap: 10 }, cancelBtn: { flex: 1, paddingVertical: 12, borderRadius: 10, borderWidth: 1, alignItems: "center" }, cancelText: { fontSize: 14, fontFamily: "Inter_500Medium" }, saveBtn: { flex: 2, flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 8, paddingVertical: 12, borderRadius: 10, }, saveText: { fontSize: 14, fontFamily: "Inter_600SemiBold" }, footerText: { fontSize: 12, fontFamily: "Inter_400Regular", textAlign: "center", lineHeight: 18, marginTop: 4 }, });