feat(mobile): add in-app updates

This commit is contained in:
tianma-if committed 2026-07-19 17:15:03 +08:00
1 parent 5deecc6753
commit 03bab30278
9 files changed
+334 -34

No files matched your search

+13 -2
View File
@@ -3,7 +3,18 @@
"name": "EdgeEver",
"slug": "edgeever",
"scheme": "edgeever",
"version": "0.4.8",
"version": "0.4.9",
"runtimeVersion": {
"policy": "appVersion"
},
"updates": {
"url": "https://u.expo.dev/59d2a8c8-fe8f-4e59-874f-a5887f584a20",
"checkAutomatically": "NEVER",
"fallbackToCacheTimeout": 0,
"requestHeaders": {
"expo-channel-name": "production"
}
},
"icon": "./assets/icon.png",
"orientation": "portrait",
"userInterfaceStyle": "light",
@@ -14,7 +25,7 @@
},
"android": {
"package": "org.edgeever.mobile",
"versionCode": 14,
"versionCode": 15,
"adaptiveIcon": {
"backgroundColor": "#047857",
"foregroundImage": "./assets/adaptive-icon-foreground.png"
+21 -18
View File
@@ -14,6 +14,7 @@ import { SessionProvider } from "../src/lib/session";
import { MobileThemeProvider, useMobileTheme } from "../src/lib/mobile-theme";
import { MobileLocaleProvider } from "../src/lib/mobile-locale";
import { AppDialogProvider } from "../src/components/AppDialogProvider";
import { MobileUpdateProvider } from "../src/lib/mobile-update";
void SplashScreen.preventAutoHideAsync();
@@ -72,26 +73,28 @@ export default function RootLayout() {
<ThemedApp>
<SafeAreaProvider>
<AppDialogProvider>
<PersistQueryClientProvider
client={queryClient}
persistOptions={{
buster: "native-cache-v1",
dehydrateOptions: {
shouldDehydrateQuery: (query) => {
const section = query.queryKey[1];
const isOfflineReadableData = section === "notebooks" || section === "memos" || section === "memo";
<MobileUpdateProvider>
<PersistQueryClientProvider
client={queryClient}
persistOptions={{
buster: "native-cache-v1",
dehydrateOptions: {
shouldDehydrateQuery: (query) => {
const section = query.queryKey[1];
const isOfflineReadableData = section === "notebooks" || section === "memos" || section === "memo";
return query.state.status === "success" && isOfflineReadableData;
return query.state.status === "success" && isOfflineReadableData;
},
},
},
maxAge: MOBILE_CACHE_MAX_AGE,
persister,
}}
>
<SessionProvider>
<Stack screenOptions={{ headerShown: false }} />
</SessionProvider>
</PersistQueryClientProvider>
maxAge: MOBILE_CACHE_MAX_AGE,
persister,
}}
>
<SessionProvider>
<Stack screenOptions={{ headerShown: false }} />
</SessionProvider>
</PersistQueryClientProvider>
</MobileUpdateProvider>
</AppDialogProvider>
</SafeAreaProvider>
</ThemedApp>
+3 -1
View File
@@ -9,13 +9,15 @@
"distribution": "internal"
},
"preview": {
"channel": "preview",
"android": {
"buildType": "apk"
},
"distribution": "internal"
},
"production": {
"autoIncrement": true
"autoIncrement": true,
"channel": "production"
}
},
"submit": {
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@edgeever/mobile",
"version": "0.4.8",
"version": "0.4.9",
"private": true,
"type": "module",
"main": "expo-router/entry",
@@ -35,6 +35,7 @@
"expo-splash-screen": "^57.0.4",
"expo-sqlite": "~57.0.1",
"expo-status-bar": "latest",
"expo-updates": "~57.0.8",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-native": "latest",
@@ -0,0 +1,106 @@
import Constants from "expo-constants";
import { ActivityIndicator, StyleSheet, View } from "react-native";
import { RefreshCw } from "./icons";
import { Pressable, Text } from "./LocalizedText";
import { useMobileLocale } from "../lib/mobile-locale";
import { useMobileUpdate } from "../lib/mobile-update";
import { useMobileTheme, resolveMobileThemeStyles } from "../lib/mobile-theme";
export const MobileUpdateCard = () => {
const { resolvedLocale } = useMobileLocale();
const { resolvedTheme } = useMobileTheme();
const { checkForUpdate, isSupported, status } = useMobileUpdate();
const english = resolvedLocale === "en-US";
const busy = status === "checking" || status === "downloading";
const styles = resolveMobileThemeStyles(baseStyles, resolvedTheme);
const statusLabel = status === "checking"
? (english ? "Checking…" : "正在检查…")
: status === "downloading"
? (english ? "Downloading…" : "正在下载…")
: status === "ready"
? (english ? "Downloaded; restart to apply" : "已下载,重启后应用")
: (english ? "Check for updates" : "检查更新");
return (
<View style={styles.card}>
<View style={styles.copy}>
<Text style={styles.title}>{english ? "App updates" : "应用更新"}</Text>
<Text style={styles.description}>
{english
? "EdgeEver checks for compatible updates when it starts and returns to the foreground."
: "EdgeEver 会在启动和回到前台时自动检查兼容更新。"}
</Text>
<Text style={styles.version}>
{english ? "Current version" : "当前版本"}: v{Constants.expoConfig?.version ?? "unknown"}
</Text>
</View>
<Pressable
accessibilityRole="button"
accessibilityState={{ busy, disabled: busy }}
disabled={busy}
onPress={() => void checkForUpdate()}
style={[styles.button, busy && styles.buttonDisabled]}
>
{busy ? <ActivityIndicator color="#047857" size="small" /> : <RefreshCw color="#047857" size={16} />}
<Text style={styles.buttonText}>{statusLabel}</Text>
</Pressable>
{!isSupported ? (
<Text style={styles.hint}>
{english ? "Available in installed release builds." : "此功能会在已安装的正式版中启用。"}
</Text>
) : null}
</View>
);
};
const baseStyles = StyleSheet.create({
card: {
borderTopColor: "#f1f5f9",
borderTopWidth: 1,
gap: 12,
padding: 16,
},
copy: {
gap: 4,
},
title: {
color: "#0f172a",
fontSize: 14,
fontWeight: "700",
},
description: {
color: "#64748b",
fontSize: 12,
lineHeight: 17,
},
version: {
color: "#64748b",
fontSize: 12,
lineHeight: 17,
},
button: {
alignItems: "center",
alignSelf: "flex-start",
backgroundColor: "#ecfdf5",
borderColor: "#a7f3d0",
borderRadius: 8,
borderWidth: 1,
flexDirection: "row",
gap: 8,
minHeight: 40,
paddingHorizontal: 14,
},
buttonDisabled: {
opacity: 0.65,
},
buttonText: {
color: "#047857",
fontSize: 13,
fontWeight: "700",
},
hint: {
color: "#94a3b8",
fontSize: 11,
lineHeight: 16,
},
});
+151
View File
@@ -0,0 +1,151 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import { AppState, type AppStateStatus } from "react-native";
import * as Updates from "expo-updates";
import { Alert } from "../components/LocalizedText";
import { useMobileLocale } from "./mobile-locale";
const FOREGROUND_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
type MobileUpdateStatus = "idle" | "checking" | "downloading" | "ready";
type MobileUpdateContextValue = {
checkForUpdate: () => Promise<void>;
isSupported: boolean;
status: MobileUpdateStatus;
};
const MobileUpdateContext = createContext<MobileUpdateContextValue>({
checkForUpdate: async () => undefined,
isSupported: false,
status: "idle",
});
export const MobileUpdateProvider = ({ children }: { children: ReactNode }) => {
const { resolvedLocale } = useMobileLocale();
const [status, setStatus] = useState<MobileUpdateStatus>("idle");
const activeCheckRef = useRef<Promise<void> | null>(null);
const lastAutomaticCheckRef = useRef(0);
const isSupported = !__DEV__ && Updates.isEnabled;
const english = resolvedLocale === "en-US";
const restart = useCallback(async () => {
try {
await Updates.reloadAsync();
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
Alert.alert(
english ? "Unable to restart" : "重启失败",
english ? `Close and reopen EdgeEver to apply the update.\n\n${detail}` : `请关闭并重新打开 EdgeEver 以应用更新。\n\n${detail}`
);
}
}, [english]);
const showRestartPrompt = useCallback(() => {
Alert.alert(
english ? "Update ready" : "更新已就绪",
english
? "The update has been downloaded. Restart EdgeEver now to apply it?"
: "新版本已下载完成。现在重启 EdgeEver 以应用更新吗?",
[
{ text: english ? "Later" : "稍后", style: "cancel" },
{ text: english ? "Restart now" : "立即重启", onPress: () => void restart() },
]
);
}, [english, restart]);
const runCheck = useCallback((userInitiated: boolean) => {
if (activeCheckRef.current) {
return activeCheckRef.current;
}
if (!isSupported) {
if (userInitiated) {
Alert.alert(
english ? "Updates unavailable" : "暂无法检查更新",
english
? "Update checks are available in installed release builds, not Expo Go or development builds."
: "检查更新仅适用于已安装的正式版,Expo Go 和开发版暂不支持。"
);
}
return Promise.resolve();
}
const check = (async () => {
try {
setStatus("checking");
const result = await Updates.checkForUpdateAsync();
if (!result.isAvailable) {
setStatus("idle");
if (userInitiated) {
Alert.alert(
english ? "You're up to date" : "已是最新版本",
english ? "No new update is available for this version." : "当前版本暂无可用更新。"
);
}
return;
}
setStatus("downloading");
await Updates.fetchUpdateAsync();
setStatus("ready");
showRestartPrompt();
} catch (error) {
setStatus("idle");
if (userInitiated) {
const detail = error instanceof Error ? error.message : String(error);
Alert.alert(
english ? "Unable to check for updates" : "检查更新失败",
english ? `Check your connection and try again.\n\n${detail}` : `请检查网络连接后重试。\n\n${detail}`
);
}
}
})();
activeCheckRef.current = check;
void check.finally(() => {
activeCheckRef.current = null;
});
return check;
}, [english, isSupported, showRestartPrompt]);
useEffect(() => {
const attemptAutomaticCheck = () => {
if (Date.now() - lastAutomaticCheckRef.current < FOREGROUND_CHECK_INTERVAL_MS) {
return;
}
lastAutomaticCheckRef.current = Date.now();
void runCheck(false);
};
const timer = setTimeout(attemptAutomaticCheck, 1_500);
const subscription = AppState.addEventListener("change", (nextState: AppStateStatus) => {
if (nextState === "active") {
attemptAutomaticCheck();
}
});
return () => {
clearTimeout(timer);
subscription.remove();
};
}, [runCheck]);
const value = useMemo<MobileUpdateContextValue>(
() => ({
checkForUpdate: () => {
if (status === "ready") {
showRestartPrompt();
return Promise.resolve();
}
return runCheck(true);
},
isSupported,
status,
}),
[isSupported, runCheck, showRestartPrompt, status]
);
return <MobileUpdateContext.Provider value={value}>{children}</MobileUpdateContext.Provider>;
};
export const useMobileUpdate = () => useContext(MobileUpdateContext);
@@ -119,6 +119,7 @@ import EditorRuntimePrewarm from "../components/EditorRuntimePrewarm";
import { showEdgeEverKeyboard } from "../../modules/edgeever-keyboard";
import LocalTiptapEditor, { type LocalTiptapEditorRef } from "../components/LocalTiptapEditor";
import { resolveMobileThemeStyles, useMobileTheme, type MobileResolvedTheme } from "../lib/mobile-theme";
import { MobileUpdateCard } from "../components/MobileUpdateCard";
const ALL_NOTES_ID = "all";
const DEFAULT_MEMO_TITLE = "无标题笔记";
@@ -1840,6 +1841,7 @@ const SettingsView = ({
</View>
</View>
</View>
<MobileUpdateCard />
<SystemInfoCard embedded />
</SettingsGroup>
</View>
+23 -12
View File
@@ -79,7 +79,7 @@
},
"apps/mobile": {
"name": "@edgeever/mobile",
"version": "0.4.8",
"version": "0.4.9",
"dependencies": {
"@edgeever/client": "workspace:*",
"@edgeever/shared": "workspace:*",
@@ -104,6 +104,7 @@
"expo-splash-screen": "^57.0.4",
"expo-sqlite": "~57.0.1",
"expo-status-bar": "latest",
"expo-updates": "~57.0.8",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-native": "latest",
@@ -1609,6 +1610,8 @@
"expo-document-picker": ["expo-document-picker@57.0.1", "", { "peerDependencies": { "expo": "*" } }, "sha512-qBwM5oxDZ3I9kwFD3pUE1oK/WNv9artoEKO6UpqhQgNRr0XA1ALRVWYjkF4+ge9lUNDRehjTm/jenINkzqg84g=="],
"expo-eas-client": ["expo-eas-client@57.0.1", "", {}, "sha512-4w51+zsl/ziUHQMJgLgUdgsNhRPAwHBfySpPB1hpWU21X74QS9T4SqDftaRnrDagn/DfcrXUMJvHbDQUxLPJNA=="],
"expo-file-system": ["expo-file-system@57.0.1", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-w7/ERvQFrGP2apTO9lDtZ+O6JQIhfakL7+Xqzh+rfMO9B4LB4qwrz+YvLgir8KFRVX64JHBnRuYBVLY1oQZcqw=="],
"expo-font": ["expo-font@57.0.1", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-QyS9L1Kh9sKJg4gfU6rdbpxpmH+DyzBX8z6jVvXMUDoqLr1GqmkO/Wu379KCXjL///kWbhpNlbi7AgBuj4VdIQ=="],
@@ -1619,10 +1622,14 @@
"expo-image-manipulator": ["expo-image-manipulator@57.0.5", "", { "dependencies": { "expo-image-loader": "~57.0.1" }, "peerDependencies": { "expo": "*" } }, "sha512-gEZ6tEsLwtz+dRXPC2QDObcQhAW7c9J6WW4W+5qQ4T6DlgkjtunLpHijM79W8fc99ER2X2IQS1ogJ8u9VAF7qA=="],
"expo-json-utils": ["expo-json-utils@57.0.1", "", {}, "sha512-cgTe1NqzQdYs/WN+3nIY5IZg8s0pb0xaTUbhYvxQDn137GbwRfHoGM2se3m3Vsl4Qu+B9G4RPEK5WJDEU2Do7g=="],
"expo-keep-awake": ["expo-keep-awake@57.0.1", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-28lkFImeXTS+bhAjuCFV7w7tW5bXg27BJVrxv+nC/nyYa86qEa0oFeHwqol6ha5k4pdVDQgBF09GM4A1k76Ssg=="],
"expo-linking": ["expo-linking@57.0.2", "", { "dependencies": { "expo-constants": "~57.0.3", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-A7912NxC4ibDiv7fN2BuEWCrTCmmWdaD+bQa4iNddPDOa3Svs1YDoWdbhJt3mniSaAh9AnUKS2MdFbO4zk8z+Q=="],
"expo-manifests": ["expo-manifests@57.0.1", "", { "dependencies": { "expo-json-utils": "~57.0.1" }, "peerDependencies": { "expo": "*" } }, "sha512-qB/mDG2dYdl+EvUeQuqP8KFYCFgFCQjJYdWIHo8SFBgDzMYmdF286DFY2M1M9Okr99wkb5M4tgA3aCcwv3aEQA=="],
"expo-modules-autolinking": ["expo-modules-autolinking@57.0.8", "", { "dependencies": { "@expo/require-utils": "^57.0.3", "@expo/spawn-async": "^1.8.0", "chalk": "^4.1.0", "commander": "^7.2.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-YBDgbJHlhhhr3JaKErW6znsIL8zQsh206LaDpWrLHV/WluRwhYfZuF/fhkVN7b/DsRxIu0UeCPExaEPMxlC2Hw=="],
"expo-modules-core": ["expo-modules-core@57.0.6", "", { "dependencies": { "@expo/expo-modules-macros-plugin": "0.6.1", "expo-modules-jsi": "~57.0.3", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-worklets": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0" }, "optionalPeers": ["react-native-worklets"] }, "sha512-hePwOh2+i+EpWrVnv95sQeQ0OD5PYuEuIhmglVLzQVaVBV3zHcRvLAc1rV8ROqUL6YtAQUXjPkoSx1ly2ZdTuQ=="],
@@ -1641,8 +1648,14 @@
"expo-status-bar": ["expo-status-bar@57.0.1", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-Xwaq1gAoVRWx5dPG5VhT5RSbnI9OilhZnO5qoPBnUaBAa5VzRzfdS8q0/bsPt0jR2DKLtGuP0bQ6efMJ4RIMDg=="],
"expo-structured-headers": ["expo-structured-headers@57.0.0", "", {}, "sha512-//t9UNPbJSEysc2x4VKJG/u7Osvv5DYJWsET5bqt/B+qcD1by/JXvSQzX3Q/YAgA96xFPontrz6OAPLbO4JKEA=="],
"expo-symbols": ["expo-symbols@57.0.1", "", { "dependencies": { "@expo-google-fonts/material-symbols": "^0.4.1", "sf-symbols-typescript": "^2.0.0" }, "peerDependencies": { "expo": "*", "expo-font": "*", "react": "*", "react-native": "*" } }, "sha512-8Zf+a83OywV0vf1NUtSKpNqKcULmO0GTI+zfFnGYl7SLDH9FjL5RcEZoy6CHvCgq2KDrQF21pl3r7Tb4ItPscw=="],
"expo-updates": ["expo-updates@57.0.8", "", { "dependencies": { "@expo/code-signing-certificates": "^0.0.6", "@expo/plist": "^0.8.1", "@expo/spawn-async": "^1.8.0", "arg": "^4.1.0", "chalk": "^4.1.2", "debug": "^4.3.4", "expo-eas-client": "~57.0.1", "expo-manifests": "~57.0.1", "expo-structured-headers": "~57.0.0", "expo-updates-interface": "~57.0.1", "getenv": "^2.0.0", "glob": "^13.0.0", "ignore": "^5.3.1", "nullthrows": "^1.1.1", "resolve-from": "^5.0.0" }, "peerDependencies": { "expo": "*", "expo-dev-client": "*", "react": "*", "react-native": "*" }, "optionalPeers": ["expo-dev-client"], "bin": { "expo-updates": "bin/cli.js" } }, "sha512-vHQYneTGoPZbA/3dtQlfrWQMyRjqO4t0g2K97Z3VX0K8GSTcANzTPe1MDYncGf7cU/nBIShgWXx7qSbLICEVdQ=="],
"expo-updates-interface": ["expo-updates-interface@57.0.1", "", { "peerDependencies": { "expo": "*" } }, "sha512-+LUWwJ0gf/TEKMVdQAw/Gjih4dvrk+URgy24X9qEGKuuMDZqjBRm9T4yQyBVALGL5TTdPUaB6ILxx3lshm3pwQ=="],
"exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
"exsolve": ["exsolve@1.1.0", "", {}, "sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw=="],
@@ -1759,7 +1772,7 @@
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
"glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
"glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
@@ -3151,18 +3164,12 @@
"@expo/cli/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="],
"@expo/cli/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
"@expo/cli/ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
"@expo/cli/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@expo/config/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
"@expo/config-plugins/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"@expo/config-plugins/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
"@expo/devcert/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
"@expo/devtools/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
@@ -3171,16 +3178,12 @@
"@expo/fingerprint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"@expo/fingerprint/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
"@expo/image-utils/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"@expo/local-build-cache-provider/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"@expo/metro-config/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"@expo/metro-config/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
"@expo/package-manager/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"@expo/plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
@@ -3305,6 +3308,10 @@
"expo-router/react-native-screens": ["react-native-screens@4.26.2", "", { "dependencies": { "react-freeze": "^1.0.0", "warn-once": "^0.1.0" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-2XnWsZToKj76trGtEZzx5ELD/qOICFEprEeUntImmitQFVUkea27fiWdUSITArI356Y1qynpXZINW+Unbhky/A=="],
"expo-updates/arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="],
"expo-updates/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"fbjs/promise": ["promise@7.3.1", "", { "dependencies": { "asap": "~2.0.3" } }, "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg=="],
@@ -3461,6 +3468,8 @@
"workbox-build/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
"workbox-build/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
"workbox-build/pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="],
"workbox-build/source-map": ["source-map@0.8.0-beta.0", "", { "dependencies": { "whatwg-url": "^7.0.0" } }, "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA=="],
@@ -3691,6 +3700,8 @@
"expo-modules-autolinking/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"expo-updates/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"filelist/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="],
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
+13
View File
@@ -2,6 +2,19 @@
EdgeEver Mobile is built with Expo and React Native. Daily Android test packages are built directly on GitHub Actions, without using EAS Build quota.
## App Updates
Installed release builds use EAS Update for compatible JavaScript and asset updates. The app checks once shortly after launch and, at most once every six hours, when it returns to the foreground. Users can also open **My** -> **General settings** -> **App updates** to check manually. A downloaded update is only applied after the user confirms a restart, so an active editing session is not interrupted automatically.
The `production` build and locally generated Play build read from the `production` update channel. Preview EAS builds read from `preview`. Publish and verify an update on `preview` before promoting the same change to production:
```sh
bunx eas-cli update --channel preview --environment preview --message "Describe the update"
bunx eas-cli update --channel production --environment production --message "Describe the update"
```
The update runtime uses the app-version policy. Any native dependency, Expo SDK, native module, or native configuration change requires incrementing `expo.version` and shipping a new store build before publishing code that depends on that native change. EAS Update does not replace Google Play or App Store updates for native binaries.
## Android Debug APK
The `Build EdgeEver Mobile` workflow runs on GitHub Actions and produces a debug APK artifact.