Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ccd417f2a |
11
app/package-lock.json
generated
11
app/package-lock.json
generated
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/ui": "~57.0.9",
|
"@expo/ui": "~57.0.9",
|
||||||
"@react-native-async-storage/async-storage": "2.2.0",
|
"@react-native-async-storage/async-storage": "2.2.0",
|
||||||
|
"@react-native-community/netinfo": "12.0.1",
|
||||||
"expo": "~57.0.11",
|
"expo": "~57.0.11",
|
||||||
"expo-constants": "~57.0.9",
|
"expo-constants": "~57.0.9",
|
||||||
"expo-crypto": "~57.0.1",
|
"expo-crypto": "~57.0.1",
|
||||||
@@ -2194,6 +2195,16 @@
|
|||||||
"react-native": "^0.0.0-0 || >=0.65 <1.0"
|
"react-native": "^0.0.0-0 || >=0.65 <1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-native-community/netinfo": {
|
||||||
|
"version": "12.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-12.0.1.tgz",
|
||||||
|
"integrity": "sha512-P/3caXIvfYSJG8AWJVefukg+ZGRPs+M4Lp3pNJtgcTYoJxCjWrKQGNnCkj/Cz//zWa/avGed0i/wzm0T8vV2IQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": ">=0.59"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@react-native-masked-view/masked-view": {
|
"node_modules/@react-native-masked-view/masked-view": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/@react-native-masked-view/masked-view/-/masked-view-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@react-native-masked-view/masked-view/-/masked-view-0.3.2.tgz",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/ui": "~57.0.9",
|
"@expo/ui": "~57.0.9",
|
||||||
"@react-native-async-storage/async-storage": "2.2.0",
|
"@react-native-async-storage/async-storage": "2.2.0",
|
||||||
|
"@react-native-community/netinfo": "12.0.1",
|
||||||
"expo": "~57.0.11",
|
"expo": "~57.0.11",
|
||||||
"expo-constants": "~57.0.9",
|
"expo-constants": "~57.0.9",
|
||||||
"expo-crypto": "~57.0.1",
|
"expo-crypto": "~57.0.1",
|
||||||
|
|||||||
@@ -1,19 +1,32 @@
|
|||||||
|
import NetInfo from "@react-native-community/netinfo";
|
||||||
import { Link, Stack } from "expo-router";
|
import { Link, Stack } from "expo-router";
|
||||||
import { StatusBar } from "expo-status-bar";
|
import { StatusBar } from "expo-status-bar";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { AppState } from "react-native";
|
import { AppState } from "react-native";
|
||||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||||
import { hydrate, sync } from "../lib/store";
|
import { getState, hydrate, sync } from "../lib/store";
|
||||||
import { C } from "../lib/theme";
|
import { C } from "../lib/theme";
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void hydrate();
|
void hydrate();
|
||||||
// Re-sync whenever the app returns to the foreground.
|
// Re-sync whenever the app returns to the foreground.
|
||||||
const sub = AppState.addEventListener("change", (s) => {
|
const appState = AppState.addEventListener("change", (s) => {
|
||||||
if (s === "active") void sync();
|
if (s === "active") void sync();
|
||||||
});
|
});
|
||||||
return () => sub.remove();
|
// Drain the queue the moment connectivity returns (wifi/cellular).
|
||||||
|
const net = NetInfo.addEventListener((s) => {
|
||||||
|
if (s.isConnected) void sync();
|
||||||
|
});
|
||||||
|
// Slow heartbeat as a belt-and-braces retry while changes are queued.
|
||||||
|
const heartbeat = setInterval(() => {
|
||||||
|
if (getState().pending > 0 || getState().offline) void sync();
|
||||||
|
}, 45_000);
|
||||||
|
return () => {
|
||||||
|
appState.remove();
|
||||||
|
net();
|
||||||
|
clearInterval(heartbeat);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ export default function Home() {
|
|||||||
<Text style={s.overdue}>{overdue.length} overdue →</Text>
|
<Text style={s.overdue}>{overdue.length} overdue →</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)}
|
)}
|
||||||
|
{store.offline && (
|
||||||
|
<Text style={s.offline}>
|
||||||
|
offline{store.pending > 0 ? ` — ${store.pending} change${store.pending === 1 ? "" : "s"} queued` : ""}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
{store.syncError && <Text style={s.error}>{store.syncError}</Text>}
|
{store.syncError && <Text style={s.error}>{store.syncError}</Text>}
|
||||||
</View>
|
</View>
|
||||||
<View style={s.menu}>
|
<View style={s.menu}>
|
||||||
@@ -76,6 +81,7 @@ const s = StyleSheet.create({
|
|||||||
viewAll: { color: C.muted, fontSize: 14, padding: 16 },
|
viewAll: { color: C.muted, fontSize: 14, padding: 16 },
|
||||||
overdue: { color: C.danger, fontSize: 14, paddingHorizontal: 16, paddingBottom: 8 },
|
overdue: { color: C.danger, fontSize: 14, paddingHorizontal: 16, paddingBottom: 8 },
|
||||||
error: { color: C.danger, paddingHorizontal: 16, paddingTop: 8 },
|
error: { color: C.danger, paddingHorizontal: 16, paddingTop: 8 },
|
||||||
|
offline: { color: C.muted, fontSize: 13, paddingHorizontal: 16, paddingTop: 8 },
|
||||||
menu: {
|
menu: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
borderTopWidth: StyleSheet.hairlineWidth,
|
borderTopWidth: StyleSheet.hairlineWidth,
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ export interface State {
|
|||||||
hydrated: boolean;
|
hydrated: boolean;
|
||||||
syncing: boolean;
|
syncing: boolean;
|
||||||
pending: number;
|
pending: number;
|
||||||
|
/** True when the last sync failed for network reasons — expected offline. */
|
||||||
|
offline: boolean;
|
||||||
syncError: string | null;
|
syncError: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +36,7 @@ let state: State = {
|
|||||||
hydrated: false,
|
hydrated: false,
|
||||||
syncing: false,
|
syncing: false,
|
||||||
pending: 0,
|
pending: 0,
|
||||||
|
offline: false,
|
||||||
syncError: null,
|
syncError: null,
|
||||||
};
|
};
|
||||||
let queue: Op[] = [];
|
let queue: Op[] = [];
|
||||||
@@ -193,9 +196,13 @@ export async function sync() {
|
|||||||
setState({});
|
setState({});
|
||||||
}
|
}
|
||||||
await pull();
|
await pull();
|
||||||
setState({ syncError: null });
|
setState({ syncError: null, offline: false });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setState({ syncError: e instanceof Error ? e.message : String(e) });
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
|
// fetch() network failures (airplane mode, no wifi, DNS) are the expected
|
||||||
|
// offline case, not an error — the queue simply waits for connectivity.
|
||||||
|
const isNetwork = e instanceof TypeError || /network request failed|unable to resolve|fetch failed|timeout/i.test(msg);
|
||||||
|
setState(isNetwork ? { offline: true, syncError: null } : { syncError: msg });
|
||||||
} finally {
|
} finally {
|
||||||
syncRunning = false;
|
syncRunning = false;
|
||||||
setState({ syncing: false });
|
setState({ syncing: false });
|
||||||
|
|||||||
Reference in New Issue
Block a user