proposal-system/mobile/src/hooks/useOfflineDraft.ts

101 lines
2.8 KiB
TypeScript
Raw Normal View History

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);
Add iOS CD pipeline and refactor workflows to org reusable callers (#24) * Add iOS native project for React Native mobile app Xcode project with bundle ID com.seahavenind.proposals, CocoaPods configuration, and app scaffolding. * Add Fastlane configuration for iOS builds and TestFlight distribution Configures match with S3 storage (seahaven-ios-certificates bucket) for code signing and a beta lane for automated TestFlight uploads. * Add mobile CI job and iOS CD workflow (disabled) CI: adds mobile typecheck job on PRs. CD: deploy-mobile.yaml builds and uploads to TestFlight via Fastlane on a macOS runner with OIDC auth for match S3 access. Currently workflow_dispatch only — activate for V1 release. * Refactor workflows to thin wrappers calling org reusable workflows CI jobs now call ci-dotnet, ci-typescript-cdk, and ci-python-sam from the org repo. Deploy calls cd-cdk with post-deploy script for web build/S3/CloudFront. Mobile deploy calls cd-mobile-ios. Adds deploy concurrency groups to both deploy workflows. * Add mobile Dependabot entries and remove assignees Add npm and bundler ecosystems for mobile/. Remove assignees from all entries — convention no longer in use. * Add comprehensive README for the proposal-system monorepo * Fix mobile TypeScript errors and add package-lock.json Fix tsconfig.json (remove rootDir/outDir, add noEmit), fix useRef type error, fix navigation type cast, add @types/react-native-vector-icons, and generate package-lock.json for CI. * Add .npmrc for mobile to resolve peer dependency conflicts react-native-screens@4.x requires react-native >= 0.82 but the project uses 0.79. legacy-peer-deps allows installation until the next React Native upgrade.
2026-05-18 15:30:30 -04:00
const saveTimeout = useRef<ReturnType<typeof setTimeout>>(null);
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;
}