From 9308fded3e1567a4f2870e9866a6531b3127903f Mon Sep 17 00:00:00 2001 From: antoinepiron <58579297-antoinepiron@users.noreply.replit.com> Date: Mon, 4 May 2026 04:27:10 +0000 Subject: [PATCH] Task #5: Fix connection error logging, add android.package, push to Gitea MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Original task: Build a downloadable APK so you can install the app on any Android phone. What was done: - eas.json was already present with preview (APK) and production (AAB) profiles — verified - Added android.package "fr.gyozamancave.postizmobile" to app.json (required by EAS builds) - Fixed silent error swallowing across all 4 screens: * settings.tsx: now shows actual HTTP status code + response body in a scrollable error box; also auto-tries both bare key and "Bearer " auth formats; redirects (307/308) are reported with the redirect target URL * posts.tsx: Delete failure now shows an Alert with the real error; "Failed to load" list error shows the HTTP status inline * index.tsx: Calendar "Failed to load posts" now shows the HTTP status inline * compose.tsx: Upload and submit failures now include the actual error message - Fixed Gitea push method: GITEA_SSH_KEY is a PAT (not SSH key); used git -c http.extraHeader=Authorization: token ... to authenticate and force-pushed all changes to homegit.gyozamancave.fr/billisdead/Postiz-android.git Deviations: - APK not yet built: user has no Expo account (confirmed by user). EAS build requires a free expo.dev account + interactive eas login. Proposed as follow-up task #7. - Gitea SSH key issue noted and corrected: it's a PAT, push now works via HTTPS header. Obsolete follow-up #6 may be retracted since push now works. --- artifacts/postiz-mobile/app.json | 1 + .../postiz-mobile/app/(tabs)/compose.tsx | 11 +- artifacts/postiz-mobile/app/(tabs)/index.tsx | 22 ++ artifacts/postiz-mobile/app/(tabs)/posts.tsx | 28 ++- .../postiz-mobile/app/(tabs)/settings.tsx | 193 +++++++++++------- 5 files changed, 174 insertions(+), 81 deletions(-) diff --git a/artifacts/postiz-mobile/app.json b/artifacts/postiz-mobile/app.json index 92771d3..9bbd490 100644 --- a/artifacts/postiz-mobile/app.json +++ b/artifacts/postiz-mobile/app.json @@ -21,6 +21,7 @@ } }, "android": { + "package": "fr.gyozamancave.postizmobile", "permissions": [ "READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE", diff --git a/artifacts/postiz-mobile/app/(tabs)/compose.tsx b/artifacts/postiz-mobile/app/(tabs)/compose.tsx index 0669404..8f48bca 100644 --- a/artifacts/postiz-mobile/app/(tabs)/compose.tsx +++ b/artifacts/postiz-mobile/app/(tabs)/compose.tsx @@ -99,8 +99,9 @@ export default function ComposeScreen() { }); const data = await uploadRes.json() as PostizUploadResult; return data; - } catch (e) { - Alert.alert("Upload Failed", "Could not upload image. Please try again."); + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + Alert.alert("Upload Failed", `Could not upload image.\n${msg}`); return null; } finally { setUploading(false); @@ -142,9 +143,11 @@ export default function ComposeScreen() { ); queryClient.invalidateQueries({ queryKey: ["posts"] }); queryClient.invalidateQueries({ queryKey: ["posts-list"] }); - } catch (e) { + } catch (e: unknown) { Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); - Alert.alert("Failed", "Could not submit post. Please try again."); + let msg = "Could not submit post."; + if (e instanceof Error) msg += `\n${e.message}`; + Alert.alert("Failed", msg); } finally { setSubmitting(false); } diff --git a/artifacts/postiz-mobile/app/(tabs)/index.tsx b/artifacts/postiz-mobile/app/(tabs)/index.tsx index 597ed42..31f12d3 100644 --- a/artifacts/postiz-mobile/app/(tabs)/index.tsx +++ b/artifacts/postiz-mobile/app/(tabs)/index.tsx @@ -1,5 +1,6 @@ import { Feather } from "@expo/vector-icons"; import { useQuery } from "@tanstack/react-query"; +import axios from "axios"; import { router } from "expo-router"; import React, { useMemo, useState } from "react"; import { @@ -17,6 +18,24 @@ import { PostizPost, usePostiz } from "@/context/PostizContext"; import { useColors } from "@/hooks/useColors"; import { StatusBadge } from "@/components/StatusBadge"; +function extractError(err: unknown): string { + if (axios.isAxiosError(err)) { + const status = err.response?.status; + const data = err.response?.data; + if (data) { + const body = + typeof data === "string" + ? data.slice(0, 200) + : (data?.message ?? data?.error ?? JSON.stringify(data)).toString().slice(0, 200); + return status ? `HTTP ${status}: ${body}` : body; + } + if (status) return `HTTP ${status} — ${err.message}`; + if (err.message) return err.message; + } + if (err instanceof Error) return err.message; + return "Unknown error"; +} + function formatDate(date: Date): string { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, "0"); @@ -189,6 +208,9 @@ export default function CalendarScreen() { Failed to load posts + + {extractError(error)} + refetch()} style={styles.retryBtn}> Retry diff --git a/artifacts/postiz-mobile/app/(tabs)/posts.tsx b/artifacts/postiz-mobile/app/(tabs)/posts.tsx index 677b36f..0c82e91 100644 --- a/artifacts/postiz-mobile/app/(tabs)/posts.tsx +++ b/artifacts/postiz-mobile/app/(tabs)/posts.tsx @@ -1,8 +1,10 @@ import { Feather } from "@expo/vector-icons"; import { useQuery, useQueryClient } from "@tanstack/react-query"; +import axios from "axios"; import React, { useState } from "react"; import { ActivityIndicator, + Alert, FlatList, Platform, RefreshControl, @@ -11,11 +13,30 @@ import { TouchableOpacity, View, } from "react-native"; + import { useSafeAreaInsets } from "react-native-safe-area-context"; import { PostCard } from "@/components/PostCard"; import { PostizPost, usePostiz } from "@/context/PostizContext"; import { useColors } from "@/hooks/useColors"; +function extractError(err: unknown): string { + if (axios.isAxiosError(err)) { + const status = err.response?.status; + const data = err.response?.data; + if (data) { + const body = + typeof data === "string" + ? data.slice(0, 200) + : (data?.message ?? data?.error ?? JSON.stringify(data)).toString().slice(0, 200); + return status ? `HTTP ${status}: ${body}` : body; + } + if (status) return `HTTP ${status} — ${err.message}`; + if (err.message) return err.message; + } + if (err instanceof Error) return err.message; + return "Unknown error"; +} + type FilterType = "all" | "QUEUE" | "PUBLISHED" | "ERROR" | "DRAFT"; const FILTERS: { key: FilterType; label: string }[] = [ @@ -74,7 +95,9 @@ export default function PostsScreen() { (old ?? []).filter((p) => p.id !== id) ); queryClient.invalidateQueries({ queryKey: ["posts"] }); - } catch (e) { + } catch (e: unknown) { + const msg = extractError(e); + Alert.alert("Delete failed", msg); } }; @@ -155,6 +178,9 @@ export default function PostsScreen() { Failed to load + + {extractError(error)} + refetch()} style={[styles.retryBtn, { backgroundColor: colors.primary }]} diff --git a/artifacts/postiz-mobile/app/(tabs)/settings.tsx b/artifacts/postiz-mobile/app/(tabs)/settings.tsx index 51fd2af..585c339 100644 --- a/artifacts/postiz-mobile/app/(tabs)/settings.tsx +++ b/artifacts/postiz-mobile/app/(tabs)/settings.tsx @@ -1,10 +1,12 @@ 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, @@ -15,10 +17,28 @@ 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"; +function extractAxiosError(err: unknown): string { + if (axios.isAxiosError(err)) { + const status = err.response?.status; + const data = err.response?.data; + if (data) { + const body = + typeof data === "string" + ? data.slice(0, 200) + : (data?.message ?? data?.error ?? JSON.stringify(data)).toString().slice(0, 200); + return status ? `HTTP ${status}: ${body}` : body; + } + if (status) return `HTTP ${status} — ${err.message}`; + if (err.code === "ECONNABORTED") return "Request timed out (10s). Check that the URL is reachable."; + if (err.message) return err.message; + } + if (err instanceof Error) return err.message; + return "Unknown error"; +} + export default function SettingsScreen() { const colors = useColors(); const insets = useSafeAreaInsets(); @@ -29,9 +49,8 @@ export default function SettingsScreen() { const [showKey, setShowKey] = useState(false); const [validating, setValidating] = useState(false); const [saving, setSaving] = useState(false); - const [validationStatus, setValidationStatus] = useState< - "idle" | "ok" | "error" - >("idle"); + const [validationStatus, setValidationStatus] = useState<"idle" | "ok" | "error">("idle"); + const [errorDetail, setErrorDetail] = useState(""); useEffect(() => { setInputKey(apiKey); @@ -45,19 +64,48 @@ export default function SettingsScreen() { } 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); + 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 = extractAxiosError(err); + } } + + setErrorDetail(lastError); + setValidationStatus("error"); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); + setValidating(false); }; const handleSave = async () => { @@ -70,8 +118,8 @@ export default function SettingsScreen() { 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."); + } catch (err: unknown) { + Alert.alert("Error", `Failed to save settings.\n${extractAxiosError(err)}`); } finally { setSaving(false); } @@ -91,6 +139,7 @@ export default function SettingsScreen() { setInputKey(""); setInputUrl(DEFAULT_BASE_URL); setValidationStatus("idle"); + setErrorDetail(""); }, }, ] @@ -104,8 +153,7 @@ export default function SettingsScreen() { styles.container, { paddingTop: Platform.OS === "web" ? 67 : 24, - paddingBottom: - Platform.OS === "web" ? 100 : insets.bottom + 40, + paddingBottom: Platform.OS === "web" ? 100 : insets.bottom + 40, }, ]} bottomOffset={60} @@ -131,28 +179,15 @@ export default function SettingsScreen() { )} - - BASE URL - - + BASE URL + { - setInputUrl(t); - setValidationStatus("idle"); - }} + onChangeText={(t) => { setInputUrl(t); setValidationStatus("idle"); setErrorDetail(""); }} autoCapitalize="none" autoCorrect={false} keyboardType="url" @@ -161,40 +196,24 @@ export default function SettingsScreen() { - - API KEY - - + API KEY + { - setInputKey(t); - setValidationStatus("idle"); - }} + onChangeText={(t) => { setInputKey(t); setValidationStatus("idle"); setErrorDetail(""); }} secureTextEntry={!showKey} autoCapitalize="none" autoCorrect={false} /> setShowKey((v) => !v)} activeOpacity={0.7}> - + + {validationStatus === "ok" && ( @@ -203,12 +222,22 @@ export default function SettingsScreen() { )} + {validationStatus === "error" && ( - - - - Could not connect. Check your URL and API key. - + + + + + Could not connect + + + {!!errorDetail && ( + + + {errorDetail} + + + )} )} @@ -217,13 +246,7 @@ export default function SettingsScreen() { onPress={handleValidate} activeOpacity={0.8} disabled={validating} - style={[ - styles.validateBtn, - { - backgroundColor: colors.card, - borderColor: colors.border, - }, - ]} + style={[styles.validateBtn, { backgroundColor: colors.card, borderColor: colors.border }]} > {validating ? ( @@ -241,10 +264,7 @@ export default function SettingsScreen() { onPress={handleSave} activeOpacity={0.85} disabled={saving} - style={[ - styles.saveBtn, - { backgroundColor: saving ? colors.muted : colors.primary }, - ]} + style={[styles.saveBtn, { backgroundColor: saving ? colors.muted : colors.primary }]} > {saving ? ( @@ -265,9 +285,7 @@ export default function SettingsScreen() { style={[styles.clearBtn, { borderColor: colors.destructive + "60" }]} > - - Disconnect - + Disconnect )} @@ -349,6 +367,29 @@ const styles = StyleSheet.create({ 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",