Files
Postiz-android/artifacts/postiz-mobile/app/(tabs)/settings.tsx
T
antoinepiron bbbcf9f586 Add core functionality for mobile post scheduling app
Adds necessary dependencies including axios and react-native-calendars to pnpm-lock.yaml.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 7b0991ce-c7b8-4c82-9acc-fd3f9e762a01
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: dc1266fa-8375-43e1-aca0-9df31350f647
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/86064bd6-c937-4ca5-a5bf-bbef5749fb60/7b0991ce-c7b8-4c82-9acc-fd3f9e762a01/kWnlAIM
Replit-Helium-Checkpoint-Created: true
2026-05-03 11:41:45 +00:00

400 lines
11 KiB
TypeScript

import { Feather } from "@expo/vector-icons";
import * as Haptics from "expo-haptics";
import React, { useEffect, useState } from "react";
import {
ActivityIndicator,
Alert,
Platform,
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 } from "@/context/PostizContext";
import { useColors } from "@/hooks/useColors";
import axios from "axios";
const DEFAULT_BASE_URL = "https://postiz.gyozamancave.fr/public/v1";
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");
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");
try {
await axios.get(`${inputUrl.replace(/\/$/, "")}/integrations`, {
headers: { Authorization: inputKey.trim() },
timeout: 10000,
});
setValidationStatus("ok");
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
} catch {
setValidationStatus("error");
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
} finally {
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 {
Alert.alert("Error", "Failed to save settings.");
} 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");
},
},
]
);
};
return (
<KeyboardAwareScrollView
style={{ flex: 1, backgroundColor: colors.background }}
contentContainerStyle={[
styles.container,
{
paddingTop: Platform.OS === "web" ? 67 : 24,
paddingBottom:
Platform.OS === "web" ? 100 : insets.bottom + 40,
},
]}
bottomOffset={60}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{!isConfigured && (
<View style={[styles.banner, { backgroundColor: colors.primary + "18", borderColor: colors.primary + "40" }]}>
<Feather name="info" size={16} color={colors.primary} />
<Text style={[styles.bannerText, { color: colors.primary }]}>
Connect to your Postiz instance to get started
</Text>
</View>
)}
{isConfigured && (
<View style={[styles.connectedBadge, { backgroundColor: colors.success + "18", borderColor: colors.success + "40" }]}>
<Feather name="check-circle" size={14} color={colors.success} />
<Text style={[styles.connectedText, { color: colors.success }]}>
Connected to Postiz
</Text>
</View>
)}
<View style={styles.section}>
<Text style={[styles.label, { color: colors.mutedForeground }]}>
BASE URL
</Text>
<View
style={[
styles.inputWrap,
{
backgroundColor: colors.card,
borderColor: colors.border,
},
]}
>
<Feather name="globe" size={16} color={colors.mutedForeground} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.foreground }]}
placeholder="https://postiz.example.com/public/v1"
placeholderTextColor={colors.mutedForeground}
value={inputUrl}
onChangeText={(t) => {
setInputUrl(t);
setValidationStatus("idle");
}}
autoCapitalize="none"
autoCorrect={false}
keyboardType="url"
/>
</View>
</View>
<View style={styles.section}>
<Text style={[styles.label, { color: colors.mutedForeground }]}>
API KEY
</Text>
<View
style={[
styles.inputWrap,
{
backgroundColor: colors.card,
borderColor: colors.border,
},
]}
>
<Feather name="key" size={16} color={colors.mutedForeground} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.foreground }]}
placeholder="Enter your API key"
placeholderTextColor={colors.mutedForeground}
value={inputKey}
onChangeText={(t) => {
setInputKey(t);
setValidationStatus("idle");
}}
secureTextEntry={!showKey}
autoCapitalize="none"
autoCorrect={false}
/>
<TouchableOpacity onPress={() => setShowKey((v) => !v)} activeOpacity={0.7}>
<Feather
name={showKey ? "eye-off" : "eye"}
size={16}
color={colors.mutedForeground}
/>
</TouchableOpacity>
</View>
{validationStatus === "ok" && (
<View style={styles.validationRow}>
<Feather name="check-circle" size={13} color={colors.success} />
<Text style={[styles.validationText, { color: colors.success }]}>
Connection successful
</Text>
</View>
)}
{validationStatus === "error" && (
<View style={styles.validationRow}>
<Feather name="x-circle" size={13} color={colors.error} />
<Text style={[styles.validationText, { color: colors.error }]}>
Could not connect. Check your URL and API key.
</Text>
</View>
)}
</View>
<TouchableOpacity
onPress={handleValidate}
activeOpacity={0.8}
disabled={validating}
style={[
styles.validateBtn,
{
backgroundColor: colors.card,
borderColor: colors.border,
},
]}
>
{validating ? (
<ActivityIndicator color={colors.primary} size="small" />
) : (
<>
<Feather name="wifi" size={15} color={colors.primary} />
<Text style={[styles.validateText, { color: colors.primary }]}>
Test Connection
</Text>
</>
)}
</TouchableOpacity>
<TouchableOpacity
onPress={handleSave}
activeOpacity={0.85}
disabled={saving}
style={[
styles.saveBtn,
{ backgroundColor: saving ? colors.muted : colors.primary },
]}
>
{saving ? (
<ActivityIndicator color={colors.primaryForeground} size="small" />
) : (
<>
<Feather name="save" size={15} color={colors.primaryForeground} />
<Text style={[styles.saveText, { color: colors.primaryForeground }]}>
Save Settings
</Text>
</>
)}
</TouchableOpacity>
{isConfigured && (
<TouchableOpacity
onPress={handleClear}
activeOpacity={0.8}
style={[styles.clearBtn, { borderColor: colors.destructive + "60" }]}
>
<Feather name="log-out" size={14} color={colors.destructive} />
<Text style={[styles.clearText, { color: colors.destructive }]}>
Disconnect
</Text>
</TouchableOpacity>
)}
<View style={styles.footer}>
<Text style={[styles.footerText, { color: colors.mutedForeground }]}>
Your API key is stored securely on this device and never transmitted to third parties.
</Text>
</View>
</KeyboardAwareScrollView>
);
}
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",
},
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,
},
});