Files
Postiz-android/artifacts/postiz-mobile/context/PostizContext.tsx
T
billisdead 7aacb9a53e feat: UX improvements, security hardening, and code cleanup
- 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 <noreply@anthropic.com>
2026-06-07 22:20:56 +02:00

181 lines
4.6 KiB
TypeScript

import axios, { AxiosInstance } from "axios";
import * as SecureStore from "expo-secure-store";
import React, {
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from "react";
const API_KEY_STORAGE = "postiz_api_key";
const BASE_URL_STORAGE = "postiz_base_url";
export const DEFAULT_BASE_URL = "https://postiz.gyozamancave.fr/api/public/v1";
export interface PostizIntegration {
id: string;
name: string;
type: string;
picture?: string;
identifier?: string;
internalType?: string;
}
export interface PostizMediaItem {
id: string;
path: string;
}
export interface PostizPost {
id: string;
content: string;
state: "QUEUE" | "PUBLISHED" | "ERROR" | "DRAFT";
publishDate: string;
integration?: PostizIntegration;
integrations?: PostizIntegration[];
image?: PostizMediaItem[];
group?: string;
errorMessage?: string;
}
export interface PostizUploadResult {
id: string;
path: string;
}
interface PostizContextValue {
apiKey: string;
baseUrl: string;
isConfigured: boolean;
isLoading: boolean;
unauthorized: boolean;
clearUnauthorized: () => void;
client: AxiosInstance | null;
saveSettings: (apiKey: string, baseUrl: string) => Promise<void>;
clearSettings: () => Promise<void>;
}
const PostizContext = createContext<PostizContextValue>({
apiKey: "",
baseUrl: DEFAULT_BASE_URL,
isConfigured: false,
isLoading: true,
unauthorized: false,
clearUnauthorized: () => {},
client: null,
saveSettings: async () => {},
clearSettings: async () => {},
});
function createClient(
apiKey: string,
baseUrl: string,
onUnauthorized?: () => void
): AxiosInstance {
const normalizedUrl = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
const instance = axios.create({
baseURL: normalizedUrl,
headers: {
Authorization: apiKey,
"Content-Type": "application/json",
},
timeout: 15000,
});
instance.interceptors.request.use((config) => {
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;
}
export function PostizProvider({ children }: { children: React.ReactNode }) {
const [apiKey, setApiKey] = useState("");
const [baseUrl, setBaseUrl] = useState(DEFAULT_BASE_URL);
const [isLoading, setIsLoading] = useState(true);
const [client, setClient] = useState<AxiosInstance | null>(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 () => {
try {
const storedKey = await SecureStore.getItemAsync(API_KEY_STORAGE);
const storedUrl = await SecureStore.getItemAsync(BASE_URL_STORAGE);
if (storedKey) {
const url = (storedUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
setApiKey(storedKey);
setBaseUrl(url);
setClient(() => createClient(storedKey, url, handleUnauthorized));
}
} catch {
} finally {
setIsLoading(false);
}
})();
}, [handleUnauthorized]);
const saveSettings = useCallback(
async (newApiKey: string, newBaseUrl: string) => {
await SecureStore.setItemAsync(API_KEY_STORAGE, newApiKey);
await SecureStore.setItemAsync(BASE_URL_STORAGE, newBaseUrl);
setApiKey(newApiKey);
setBaseUrl(newBaseUrl);
clearUnauthorized();
setClient(() => createClient(newApiKey, newBaseUrl, handleUnauthorized));
},
[handleUnauthorized, clearUnauthorized]
);
const clearSettings = useCallback(async () => {
await SecureStore.deleteItemAsync(API_KEY_STORAGE);
await SecureStore.deleteItemAsync(BASE_URL_STORAGE);
setApiKey("");
setBaseUrl(DEFAULT_BASE_URL);
setClient(null);
clearUnauthorized();
}, [clearUnauthorized]);
return (
<PostizContext.Provider
value={{
apiKey,
baseUrl,
isConfigured: !!apiKey,
isLoading,
unauthorized,
clearUnauthorized,
client,
saveSettings,
clearSettings,
}}
>
{children}
</PostizContext.Provider>
);
}
export function usePostiz() {
return useContext(PostizContext);
}