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;