From b02d34453ebb29467dc15759f9166fec9954bd42 Mon Sep 17 00:00:00 2001 From: antoinepiron <58579297-antoinepiron@users.noreply.replit.com> Date: Mon, 4 May 2026 04:33:27 +0000 Subject: [PATCH] Task #5: Fix Postiz API base URL, improve error logging, 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. Root cause found and fixed: - The default base URL was "https://postiz.gyozamancave.fr/public/v1" — this path returns a 307 redirect to /auth (unauthenticated). The correct path for self-hosted Postiz is "/api/public/v1". Fixed in both PostizContext.tsx and settings.tsx. - Confirmed working: GET /api/public/v1/integrations with the user's key returns real integration data (Bluesky, Instagram, etc.) Other improvements in this task: - settings.tsx: shows actual HTTP status + response body in error box; tries bare key and Bearer prefix; detects redirects and shows target URL - posts.tsx, index.tsx: show real HTTP error detail on failed loads and deletes - compose.tsx: upload and submit failures show actual error message - eas.json: already correct (preview=APK, production=AAB) - app.json: added android.package "fr.gyozamancave.postizmobile" (required by EAS) - All changes pushed to Gitea via PAT (http.extraHeader Authorization: token ...) APK build status: - Cannot be triggered without a free Expo account (expo.dev) + EAS login - User confirmed they do not have an Expo account yet - Proposed as follow-up task #7 with full instructions Gitea push: success — homegit.gyozamancave.fr/billisdead/Postiz-android.git Replit-Task-Id: a53d825c-7766-4ee7-a56f-fa32f895a101 --- 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 | 197 +++++++++++------- .../postiz-mobile/context/PostizContext.tsx | 2 +- 6 files changed, 177 insertions(+), 84 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..319c459 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,9 +17,27 @@ 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"; +const DEFAULT_BASE_URL = "https://postiz.gyozamancave.fr/api/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(); @@ -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", diff --git a/artifacts/postiz-mobile/context/PostizContext.tsx b/artifacts/postiz-mobile/context/PostizContext.tsx index 6b30259..2d69ab4 100644 --- a/artifacts/postiz-mobile/context/PostizContext.tsx +++ b/artifacts/postiz-mobile/context/PostizContext.tsx @@ -10,7 +10,7 @@ import React, { const API_KEY_STORAGE = "postiz_api_key"; const BASE_URL_STORAGE = "postiz_base_url"; -const DEFAULT_BASE_URL = "https://postiz.gyozamancave.fr/public/v1"; +const DEFAULT_BASE_URL = "https://postiz.gyozamancave.fr/api/public/v1"; export interface PostizIntegration { id: string;