Add mobile core: API client, auth, storage, state, and offline support
Keychain token storage, Axios client with async token injection, Cognito
PKCE auth via react-native-app-auth, Redux auth slice, TanStack Query
config, offline draft queue with NetInfo auto-submit, and SHOC theme.
2026-05-17 15:09:23 -04:00
|
|
|
import { useEffect, useState, useCallback, useRef } from 'react';
|
|
|
|
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
|
|
|
import NetInfo from '@react-native-community/netinfo';
|
|
|
|
|
|
|
|
|
|
const DRAFT_KEY = 'proposal_draft';
|
|
|
|
|
const OFFLINE_QUEUE_KEY = 'proposal_offline_queue';
|
|
|
|
|
|
|
|
|
|
export function useOfflineDraft<T extends Record<string, unknown>>(
|
|
|
|
|
initialData: T,
|
|
|
|
|
) {
|
|
|
|
|
const [draft, setDraft] = useState<T>(initialData);
|
|
|
|
|
const [isLoaded, setIsLoaded] = useState(false);
|
2026-05-18 15:30:30 -04:00
|
|
|
const saveTimeout = useRef<ReturnType<typeof setTimeout>>(null);
|
Add mobile core: API client, auth, storage, state, and offline support
Keychain token storage, Axios client with async token injection, Cognito
PKCE auth via react-native-app-auth, Redux auth slice, TanStack Query
config, offline draft queue with NetInfo auto-submit, and SHOC theme.
2026-05-17 15:09:23 -04:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
AsyncStorage.getItem(DRAFT_KEY).then((stored) => {
|
|
|
|
|
if (stored) {
|
|
|
|
|
try {
|
|
|
|
|
setDraft(JSON.parse(stored));
|
|
|
|
|
} catch {
|
|
|
|
|
// Corrupted draft, use initial data
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
setIsLoaded(true);
|
|
|
|
|
});
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const updateDraft = useCallback(
|
|
|
|
|
(updates: Partial<T>) => {
|
|
|
|
|
setDraft((prev) => {
|
|
|
|
|
const next = { ...prev, ...updates };
|
|
|
|
|
if (saveTimeout.current) clearTimeout(saveTimeout.current);
|
|
|
|
|
saveTimeout.current = setTimeout(() => {
|
|
|
|
|
AsyncStorage.setItem(DRAFT_KEY, JSON.stringify(next));
|
|
|
|
|
}, 1000);
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
[],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const clearDraft = useCallback(async () => {
|
|
|
|
|
if (saveTimeout.current) clearTimeout(saveTimeout.current);
|
|
|
|
|
await AsyncStorage.removeItem(DRAFT_KEY);
|
|
|
|
|
setDraft(initialData);
|
|
|
|
|
}, [initialData]);
|
|
|
|
|
|
|
|
|
|
return { draft, updateDraft, clearDraft, isLoaded };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface QueuedSubmission {
|
|
|
|
|
data: unknown;
|
|
|
|
|
timestamp: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function queueOfflineSubmission(data: unknown): Promise<void> {
|
|
|
|
|
const stored = await AsyncStorage.getItem(OFFLINE_QUEUE_KEY);
|
|
|
|
|
const queue: QueuedSubmission[] = stored ? JSON.parse(stored) : [];
|
|
|
|
|
queue.push({ data, timestamp: new Date().toISOString() });
|
|
|
|
|
await AsyncStorage.setItem(OFFLINE_QUEUE_KEY, JSON.stringify(queue));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function processOfflineQueue(
|
|
|
|
|
submitFn: (data: unknown) => Promise<unknown>,
|
|
|
|
|
): Promise<number> {
|
|
|
|
|
const state = await NetInfo.fetch();
|
|
|
|
|
if (!state.isConnected) return 0;
|
|
|
|
|
|
|
|
|
|
const stored = await AsyncStorage.getItem(OFFLINE_QUEUE_KEY);
|
|
|
|
|
if (!stored) return 0;
|
|
|
|
|
|
|
|
|
|
const queue: QueuedSubmission[] = JSON.parse(stored);
|
|
|
|
|
let processed = 0;
|
|
|
|
|
|
|
|
|
|
for (const item of queue) {
|
|
|
|
|
try {
|
|
|
|
|
await submitFn(item.data);
|
|
|
|
|
processed++;
|
|
|
|
|
} catch {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (processed === queue.length) {
|
|
|
|
|
await AsyncStorage.removeItem(OFFLINE_QUEUE_KEY);
|
|
|
|
|
} else if (processed > 0) {
|
|
|
|
|
await AsyncStorage.setItem(
|
|
|
|
|
OFFLINE_QUEUE_KEY,
|
|
|
|
|
JSON.stringify(queue.slice(processed)),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return processed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getOfflineQueueCount(): Promise<number> {
|
|
|
|
|
const stored = await AsyncStorage.getItem(OFFLINE_QUEUE_KEY);
|
|
|
|
|
if (!stored) return 0;
|
|
|
|
|
return JSON.parse(stored).length;
|
|
|
|
|
}
|