From 7aacb9a53ef6703f821cefb5fec996778322b50b Mon Sep 17 00:00:00 2001 From: billisdead Date: Sun, 7 Jun 2026 22:20:56 +0200 Subject: [PATCH] feat: UX improvements, security hardening, and code cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract shared extractError utility (lib/extractError.ts), remove 3 duplicate copies - Export DEFAULT_BASE_URL from PostizContext, remove duplicate in settings - Add 401 interceptor in axios client: fires UnauthorizedHandler in _layout → alert + redirect to Settings - Calendar day items now tappable: tap opens context menu (Copy / Edit / Repost) - Persist sort order (newest/oldest) across sessions via AsyncStorage - Filter chips show post count per status (Queue 3, Error 1, etc.) - Copy text action now shows a brief "Copied" toast + haptic feedback - PostCard: swipe right → Reschedule action (QUEUE posts only, amber color) - Compose: per-network char limit (Twitter 280, Instagram 2200…) with color warning at 90% - Compose: local draft save/restore via AsyncStorage with restore banner on open - Compose: prefillImagePath/prefillImageId params allow Edit/Repost to carry over existing media Co-Authored-By: Claude Sonnet 4.6 --- .../postiz-mobile/app/(tabs)/compose.tsx | 188 ++++++++++++++++-- artifacts/postiz-mobile/app/(tabs)/index.tsx | 57 ++++-- artifacts/postiz-mobile/app/(tabs)/posts.tsx | 123 +++++++----- .../postiz-mobile/app/(tabs)/settings.tsx | 28 +-- artifacts/postiz-mobile/app/_layout.tsx | 28 ++- .../postiz-mobile/components/PostCard.tsx | 38 +++- .../postiz-mobile/context/PostizContext.tsx | 49 ++++- artifacts/postiz-mobile/lib/extractError.ts | 20 ++ 8 files changed, 419 insertions(+), 112 deletions(-) create mode 100644 artifacts/postiz-mobile/lib/extractError.ts diff --git a/artifacts/postiz-mobile/app/(tabs)/compose.tsx b/artifacts/postiz-mobile/app/(tabs)/compose.tsx index d016564..cdebd8c 100644 --- a/artifacts/postiz-mobile/app/(tabs)/compose.tsx +++ b/artifacts/postiz-mobile/app/(tabs)/compose.tsx @@ -1,6 +1,7 @@ import { Feather } from "@expo/vector-icons"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import DateTimePicker from "@react-native-community/datetimepicker"; +import AsyncStorage from "@react-native-async-storage/async-storage"; import * as Haptics from "expo-haptics"; import { Image } from "expo-image"; import * as ImagePicker from "expo-image-picker"; @@ -24,16 +25,29 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import { ChannelChip } from "@/components/ChannelChip"; import { PostizIntegration, PostizUploadResult, usePostiz } from "@/context/PostizContext"; import { useColors } from "@/hooks/useColors"; +const DRAFT_STORAGE_KEY = "postiz_local_draft"; + +const NETWORK_CHAR_LIMITS: Record = { + twitter: 280, x: 280, + instagram: 2200, + linkedin: 3000, + facebook: 63206, + youtube: 5000, + tiktok: 2200, +}; export default function ComposeScreen() { const colors = useColors(); const insets = useSafeAreaInsets(); const { client, isConfigured, apiKey, baseUrl } = usePostiz(); const queryClient = useQueryClient(); - const { prefillContent, prefillIntegrationIds } = useLocalSearchParams<{ - prefillContent?: string; - prefillIntegrationIds?: string; - }>(); + const { prefillContent, prefillIntegrationIds, prefillImagePath, prefillImageId } = + useLocalSearchParams<{ + prefillContent?: string; + prefillIntegrationIds?: string; + prefillImagePath?: string; + prefillImageId?: string; + }>(); const [content, setContent] = useState(""); const [selectedChannels, setSelectedChannels] = useState([]); const [postNow, setPostNow] = useState(false); @@ -43,15 +57,34 @@ export default function ComposeScreen() { const [showDatePicker, setShowDatePicker] = useState(false); const [showTimePicker, setShowTimePicker] = useState(false); const [imageUri, setImageUri] = useState(null); + const [existingMedia, setExistingMedia] = useState>([]); const [uploading, setUploading] = useState(false); const [submitting, setSubmitting] = useState(false); + const [draftBanner, setDraftBanner] = useState(false); useEffect(() => { - if (prefillContent) setContent(String(prefillContent)); + if (prefillContent) { + setContent(String(prefillContent)); + } if (prefillIntegrationIds) { setSelectedChannels(String(prefillIntegrationIds).split(",").filter(Boolean)); } - }, [prefillContent, prefillIntegrationIds]); + if (prefillImagePath && prefillImageId) { + setExistingMedia([{ id: String(prefillImageId), path: String(prefillImagePath) }]); + setImageUri(String(prefillImagePath)); + } + }, [prefillContent, prefillIntegrationIds, prefillImagePath, prefillImageId]); + + useEffect(() => { + if (prefillContent) return; + AsyncStorage.getItem(DRAFT_STORAGE_KEY).then((raw) => { + if (!raw) return; + try { + const draft = JSON.parse(raw); + if (draft?.content) setDraftBanner(true); + } catch {} + }); + }, [prefillContent]); const { data: integrations, isLoading: loadingIntegrations } = useQuery({ @@ -65,6 +98,39 @@ export default function ComposeScreen() { staleTime: 60000, }); + const effectiveCharLimit = (() => { + if (selectedChannels.length === 0 || !integrations) return 3000; + const selected = integrations.filter((i) => selectedChannels.includes(i.id)); + const limits = selected.map((i) => { + const t = (i.type ?? i.internalType ?? "").toLowerCase(); + for (const [key, limit] of Object.entries(NETWORK_CHAR_LIMITS)) { + if (t.includes(key)) return limit; + } + return 3000; + }); + return Math.min(...limits); + })(); + + const saveDraft = async () => { + const draft = { content, integrationIds: selectedChannels }; + await AsyncStorage.setItem(DRAFT_STORAGE_KEY, JSON.stringify(draft)); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + Alert.alert("Draft saved", "Your draft has been saved locally."); + }; + + const restoreDraft = async () => { + const raw = await AsyncStorage.getItem(DRAFT_STORAGE_KEY); + if (!raw) return; + try { + const draft = JSON.parse(raw); + if (draft.content) setContent(draft.content); + if (draft.integrationIds?.length) setSelectedChannels(draft.integrationIds); + setDraftBanner(false); + } catch {} + }; + + const dismissDraft = () => setDraftBanner(false); + const toggleChannel = (id: string) => { setSelectedChannels((prev) => prev.includes(id) ? prev.filter((c) => c !== id) : [...prev, id] @@ -84,10 +150,14 @@ export default function ComposeScreen() { }); if (!result.canceled && result.assets[0]) { setImageUri(result.assets[0].uri); + setExistingMedia([]); } }; - const removeImage = () => setImageUri(null); + const removeImage = () => { + setImageUri(null); + setExistingMedia([]); + }; const uploadImage = async (): Promise => { setUploading(true); @@ -134,9 +204,12 @@ export default function ComposeScreen() { setSubmitting(true); try { let media: Array<{ id: string; path: string }> = []; - if (imageUri) { + const isLocalFile = imageUri && !imageUri.startsWith("http"); + if (imageUri && isLocalFile) { const uploaded = await uploadImage(); media = [{ id: uploaded.id, path: uploaded.path }]; + } else if (existingMedia.length > 0) { + media = existingMedia; } const payload = { type: postNow ? "now" : "schedule", @@ -176,6 +249,7 @@ export default function ComposeScreen() { } Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + await AsyncStorage.removeItem(DRAFT_STORAGE_KEY); Alert.alert( "Posted!", postNow ? "Your post has been published." : "Post scheduled successfully.", @@ -197,6 +271,8 @@ export default function ComposeScreen() { setSelectedChannels([]); setPostNow(false); setImageUri(null); + setExistingMedia([]); + setDraftBanner(false); setScheduleDate(new Date(Date.now() + 60 * 60 * 1000)); }; @@ -238,6 +314,21 @@ export default function ComposeScreen() { keyboardShouldPersistTaps="handled" showsVerticalScrollIndicator={false} > + {draftBanner && ( + + + + You have a saved draft + + + Restore + + + + + + )} + - - {content.length}/3000 - + + {effectiveCharLimit < 3000 && selectedChannels.length > 0 && ( + + limit: {effectiveCharLimit} + + )} + = effectiveCharLimit + ? colors.error + : content.length > effectiveCharLimit * 0.9 + ? colors.warning + : colors.mutedForeground, + }, + ]} + > + {content.length}/{effectiveCharLimit} + + {imageUri && ( @@ -410,6 +520,16 @@ export default function ComposeScreen() { /> )} + + + Save Draft + + { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + const preview = post.content.slice(0, 60) + (post.content.length > 60 ? "…" : ""); + const integrations = post.integrations ?? (post.integration ? [post.integration] : []); + Alert.alert( + post.state === "PUBLISHED" ? "Published post" : + post.state === "QUEUE" ? "Scheduled post" : + post.state === "ERROR" ? "Failed post" : "Draft", + preview, + [ + { + text: "Copy text", + onPress: () => { + Clipboard.setStringAsync(post.content); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + }, + }, + { + text: post.state === "PUBLISHED" ? "Repost" : "Edit", + onPress: () => + router.push({ + pathname: "/(tabs)/compose", + params: { + prefillContent: post.content, + prefillIntegrationIds: integrations.map((i) => i.id).join(","), + }, + }), + }, + { text: "Cancel", style: "cancel" }, + ] + ); + }; + const now = new Date(); const [currentMonth, setCurrentMonth] = useState({ year: now.getFullYear(), @@ -255,6 +273,7 @@ export default function CalendarScreen() { showContextMenu(item)} > diff --git a/artifacts/postiz-mobile/app/(tabs)/posts.tsx b/artifacts/postiz-mobile/app/(tabs)/posts.tsx index 96ea106..a2ce60f 100644 --- a/artifacts/postiz-mobile/app/(tabs)/posts.tsx +++ b/artifacts/postiz-mobile/app/(tabs)/posts.tsx @@ -1,11 +1,11 @@ import { Feather } from "@expo/vector-icons"; import DateTimePicker from "@react-native-community/datetimepicker"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import axios from "axios"; +import AsyncStorage from "@react-native-async-storage/async-storage"; import * as Clipboard from "expo-clipboard"; import * as Haptics from "expo-haptics"; import { useRouter } from "expo-router"; -import React, { useMemo, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { ActivityIndicator, Alert, @@ -21,24 +21,9 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import { PostCard } from "@/components/PostCard"; import { PostizPost, usePostiz } from "@/context/PostizContext"; import { useColors } from "@/hooks/useColors"; +import { extractError } from "@/lib/extractError"; -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"; -} +const SORT_STORAGE_KEY = "postiz_posts_sort"; type FilterType = "all" | "QUEUE" | "PUBLISHED" | "ERROR" | "DRAFT"; @@ -59,6 +44,19 @@ export default function PostsScreen() { const [filter, setFilter] = useState("all"); const [sortOrder, setSortOrder] = useState<"desc" | "asc">("desc"); const [refreshing, setRefreshing] = useState(false); + const [copyToast, setCopyToast] = useState(false); + + useEffect(() => { + AsyncStorage.getItem(SORT_STORAGE_KEY).then((v) => { + if (v === "asc" || v === "desc") setSortOrder(v); + }); + }, []); + + const toggleSort = () => { + const next = sortOrder === "desc" ? "asc" : "desc"; + setSortOrder(next); + AsyncStorage.setItem(SORT_STORAGE_KEY, next); + }; // reschedule state const [reschedulePost, setReschedulePost] = useState(null); @@ -98,6 +96,17 @@ export default function PostsScreen() { }); }, [posts, filter, sortOrder]); + const filterCounts = useMemo(() => { + const all = posts ?? []; + return { + all: all.length, + QUEUE: all.filter((p) => p.state === "QUEUE").length, + PUBLISHED: all.filter((p) => p.state === "PUBLISHED").length, + DRAFT: all.filter((p) => p.state === "DRAFT").length, + ERROR: all.filter((p) => p.state === "ERROR").length, + }; + }, [posts]); + const handleRefresh = async () => { setRefreshing(true); await refetch(); @@ -190,7 +199,12 @@ export default function PostsScreen() { buttons.push({ text: "Copy text", - onPress: () => Clipboard.setStringAsync(post.content), + onPress: async () => { + await Clipboard.setStringAsync(post.content); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + setCopyToast(true); + setTimeout(() => setCopyToast(false), 2000); + }, }); if (post.state === "ERROR") { @@ -258,39 +272,37 @@ export default function PostsScreen() { keyExtractor={(item) => item.key} showsHorizontalScrollIndicator={false} contentContainerStyle={styles.filterList} - renderItem={({ item }) => ( - setFilter(item.key)} - activeOpacity={0.7} - style={[ - styles.filterChip, - { - backgroundColor: - filter === item.key ? colors.primary : colors.secondary, - borderColor: - filter === item.key ? colors.primary : colors.border, - }, - ]} - > - { + const count = posts ? filterCounts[item.key] : undefined; + const active = filter === item.key; + return ( + setFilter(item.key)} + activeOpacity={0.7} style={[ - styles.filterText, + styles.filterChip, { - color: - filter === item.key - ? colors.primaryForeground - : colors.mutedForeground, + backgroundColor: active ? colors.primary : colors.secondary, + borderColor: active ? colors.primary : colors.border, }, ]} > - {item.label} - - - )} + + {item.label} + {count !== undefined && count > 0 ? ` ${count}` : ""} + + + ); + }} style={styles.filterBar} /> setSortOrder((o) => (o === "desc" ? "asc" : "desc"))} + onPress={toggleSort} activeOpacity={0.7} style={[styles.sortBtn, { borderColor: colors.border, backgroundColor: colors.secondary }]} > @@ -302,6 +314,13 @@ export default function PostsScreen() { + {copyToast && ( + + + Copied + + )} + {isLoading ? ( @@ -333,6 +352,7 @@ export default function PostsScreen() { post={item} onDelete={handleDelete} onLongPress={showContextMenu} + onReschedule={startReschedule} /> )} refreshControl={ @@ -415,4 +435,17 @@ const styles = StyleSheet.create({ emptyText: { fontSize: 14, fontFamily: "Inter_400Regular", textAlign: "center" }, retryBtn: { marginTop: 4, paddingHorizontal: 20, paddingVertical: 10, borderRadius: 10 }, retryText: { fontSize: 14, fontFamily: "Inter_600SemiBold" }, + toast: { + position: "absolute", + bottom: 100, + alignSelf: "center", + flexDirection: "row", + alignItems: "center", + gap: 6, + paddingHorizontal: 14, + paddingVertical: 8, + borderRadius: 20, + zIndex: 999, + }, + toastText: { fontSize: 13, fontFamily: "Inter_600SemiBold", color: "#fff" }, }); diff --git a/artifacts/postiz-mobile/app/(tabs)/settings.tsx b/artifacts/postiz-mobile/app/(tabs)/settings.tsx index 319c459..253eabf 100644 --- a/artifacts/postiz-mobile/app/(tabs)/settings.tsx +++ b/artifacts/postiz-mobile/app/(tabs)/settings.tsx @@ -15,29 +15,9 @@ import { } from "react-native"; import { KeyboardAwareScrollView } from "react-native-keyboard-controller"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { usePostiz } from "@/context/PostizContext"; +import { usePostiz, DEFAULT_BASE_URL } from "@/context/PostizContext"; import { useColors } from "@/hooks/useColors"; - -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"; -} +import { extractError } from "@/lib/extractError"; export default function SettingsScreen() { const colors = useColors(); @@ -98,7 +78,7 @@ export default function SettingsScreen() { continue; } } - lastError = extractAxiosError(err); + lastError = extractError(err); } } @@ -119,7 +99,7 @@ export default function SettingsScreen() { Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); Alert.alert("Saved", "Settings saved successfully."); } catch (err: unknown) { - Alert.alert("Error", `Failed to save settings.\n${extractAxiosError(err)}`); + Alert.alert("Error", `Failed to save settings.\n${extractError(err)}`); } finally { setSaving(false); } diff --git a/artifacts/postiz-mobile/app/_layout.tsx b/artifacts/postiz-mobile/app/_layout.tsx index 61683ce..61c3960 100644 --- a/artifacts/postiz-mobile/app/_layout.tsx +++ b/artifacts/postiz-mobile/app/_layout.tsx @@ -6,15 +6,16 @@ import { useFonts, } from "@expo-google-fonts/inter"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { Stack } from "expo-router"; +import { router, Stack } from "expo-router"; import * as SplashScreen from "expo-splash-screen"; import React, { useEffect } from "react"; +import { Alert } from "react-native"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import { KeyboardProvider } from "react-native-keyboard-controller"; import { SafeAreaProvider } from "react-native-safe-area-context"; import { ErrorBoundary } from "@/components/ErrorBoundary"; -import { PostizProvider } from "@/context/PostizContext"; +import { PostizProvider, usePostiz } from "@/context/PostizContext"; import { useNotifications } from "@/hooks/useNotifications"; SplashScreen.preventAutoHideAsync(); @@ -33,6 +34,28 @@ function NotificationBootstrap() { return null; } +function UnauthorizedHandler() { + const { unauthorized, clearUnauthorized } = usePostiz(); + useEffect(() => { + if (!unauthorized) return; + Alert.alert( + "API key invalid", + "Your API key was rejected (401). Update it in Settings.", + [ + { + text: "Open Settings", + onPress: () => { + clearUnauthorized(); + router.push("/(tabs)/settings"); + }, + }, + { text: "Dismiss", style: "cancel", onPress: clearUnauthorized }, + ] + ); + }, [unauthorized, clearUnauthorized]); + return null; +} + function RootLayoutNav() { return ( @@ -63,6 +86,7 @@ export default function RootLayout() { + diff --git a/artifacts/postiz-mobile/components/PostCard.tsx b/artifacts/postiz-mobile/components/PostCard.tsx index f103f33..fe74f0e 100644 --- a/artifacts/postiz-mobile/components/PostCard.tsx +++ b/artifacts/postiz-mobile/components/PostCard.tsx @@ -18,6 +18,7 @@ interface PostCardProps { post: PostizPost; onDelete: (id: string) => Promise; onLongPress: (post: PostizPost) => void; + onReschedule?: (post: PostizPost) => void; } function formatDate(dateStr: string): string { @@ -44,7 +45,7 @@ function getNetworkIcon(type?: string): React.ComponentProps["na return "globe"; } -export function PostCard({ post, onDelete, onLongPress }: PostCardProps) { +export function PostCard({ post, onDelete, onLongPress, onReschedule }: PostCardProps) { const colors = useColors(); const swipeRef = useRef(null); @@ -88,6 +89,34 @@ export function PostCard({ post, onDelete, onLongPress }: PostCardProps) { ); }; + const renderLeftActions = + post.state === "QUEUE" && onReschedule + ? ( + _progress: Animated.AnimatedInterpolation, + dragX: Animated.AnimatedInterpolation + ) => { + const scale = dragX.interpolate({ + inputRange: [0, 80], + outputRange: [0.8, 1], + extrapolate: "clamp", + }); + return ( + { + swipeRef.current?.close(); + onReschedule(post); + }} + activeOpacity={0.8} + > + + + + + ); + } + : undefined; + const integrations = post.integrations ?? (post.integration ? [post.integration] : []); const truncatedContent = post.content.length > 140 @@ -98,7 +127,9 @@ export function PostCard({ post, onDelete, onLongPress }: PostCardProps) { void; client: AxiosInstance | null; saveSettings: (apiKey: string, baseUrl: string) => Promise; clearSettings: () => Promise; @@ -58,12 +61,18 @@ const PostizContext = createContext({ baseUrl: DEFAULT_BASE_URL, isConfigured: false, isLoading: true, + unauthorized: false, + clearUnauthorized: () => {}, client: null, saveSettings: async () => {}, clearSettings: async () => {}, }); -function createClient(apiKey: string, baseUrl: string): AxiosInstance { +function createClient( + apiKey: string, + baseUrl: string, + onUnauthorized?: () => void +): AxiosInstance { const normalizedUrl = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/"; const instance = axios.create({ baseURL: normalizedUrl, @@ -77,6 +86,15 @@ function createClient(apiKey: string, baseUrl: string): AxiosInstance { console.log(">>> REQUEST:", config.method?.toUpperCase(), (config.baseURL || "") + (config.url || "")); return config; }); + instance.interceptors.response.use( + (res) => res, + (err) => { + if (axios.isAxiosError(err) && err.response?.status === 401) { + onUnauthorized?.(); + } + return Promise.reject(err); + } + ); return instance; } @@ -85,6 +103,19 @@ export function PostizProvider({ children }: { children: React.ReactNode }) { const [baseUrl, setBaseUrl] = useState(DEFAULT_BASE_URL); const [isLoading, setIsLoading] = useState(true); const [client, setClient] = useState(null); + const [unauthorized, setUnauthorized] = useState(false); + const unauthorizedFiredRef = useRef(false); + + const handleUnauthorized = useCallback(() => { + if (unauthorizedFiredRef.current) return; + unauthorizedFiredRef.current = true; + setUnauthorized(true); + }, []); + + const clearUnauthorized = useCallback(() => { + unauthorizedFiredRef.current = false; + setUnauthorized(false); + }, []); useEffect(() => { (async () => { @@ -95,14 +126,14 @@ export function PostizProvider({ children }: { children: React.ReactNode }) { const url = (storedUrl || DEFAULT_BASE_URL).replace(/\/$/, ""); setApiKey(storedKey); setBaseUrl(url); - setClient(() => createClient(storedKey, url)); + setClient(() => createClient(storedKey, url, handleUnauthorized)); } } catch { } finally { setIsLoading(false); } })(); - }, []); + }, [handleUnauthorized]); const saveSettings = useCallback( async (newApiKey: string, newBaseUrl: string) => { @@ -110,9 +141,10 @@ export function PostizProvider({ children }: { children: React.ReactNode }) { await SecureStore.setItemAsync(BASE_URL_STORAGE, newBaseUrl); setApiKey(newApiKey); setBaseUrl(newBaseUrl); - setClient(() => createClient(newApiKey, newBaseUrl)); + clearUnauthorized(); + setClient(() => createClient(newApiKey, newBaseUrl, handleUnauthorized)); }, - [] + [handleUnauthorized, clearUnauthorized] ); const clearSettings = useCallback(async () => { @@ -121,7 +153,8 @@ export function PostizProvider({ children }: { children: React.ReactNode }) { setApiKey(""); setBaseUrl(DEFAULT_BASE_URL); setClient(null); - }, []); + clearUnauthorized(); + }, [clearUnauthorized]); return (