diff --git a/apps/mobile/app.json b/apps/mobile/app.json index 9c4063c2..9f34e979 100644 --- a/apps/mobile/app.json +++ b/apps/mobile/app.json @@ -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" diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx index c07384e4..4b76dbc0 100644 --- a/apps/mobile/app/_layout.tsx +++ b/apps/mobile/app/_layout.tsx @@ -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() { - { - const section = query.queryKey[1]; - const isOfflineReadableData = section === "notebooks" || section === "memos" || section === "memo"; + + { + 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, - }} - > - - - - + maxAge: MOBILE_CACHE_MAX_AGE, + persister, + }} + > + + + + + diff --git a/apps/mobile/eas.json b/apps/mobile/eas.json index de9d744c..2782fa3b 100644 --- a/apps/mobile/eas.json +++ b/apps/mobile/eas.json @@ -9,13 +9,15 @@ "distribution": "internal" }, "preview": { + "channel": "preview", "android": { "buildType": "apk" }, "distribution": "internal" }, "production": { - "autoIncrement": true + "autoIncrement": true, + "channel": "production" } }, "submit": { diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 507470b7..024d42a6 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -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", diff --git a/apps/mobile/src/components/MobileUpdateCard.tsx b/apps/mobile/src/components/MobileUpdateCard.tsx new file mode 100644 index 00000000..cca30e81 --- /dev/null +++ b/apps/mobile/src/components/MobileUpdateCard.tsx @@ -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 ( + + + {english ? "App updates" : "应用更新"} + + {english + ? "EdgeEver checks for compatible updates when it starts and returns to the foreground." + : "EdgeEver 会在启动和回到前台时自动检查兼容更新。"} + + + {english ? "Current version" : "当前版本"}: v{Constants.expoConfig?.version ?? "unknown"} + + + void checkForUpdate()} + style={[styles.button, busy && styles.buttonDisabled]} + > + {busy ? : } + {statusLabel} + + {!isSupported ? ( + + {english ? "Available in installed release builds." : "此功能会在已安装的正式版中启用。"} + + ) : null} + + ); +}; + +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, + }, +}); diff --git a/apps/mobile/src/lib/mobile-update.tsx b/apps/mobile/src/lib/mobile-update.tsx new file mode 100644 index 00000000..2ac8b4a3 --- /dev/null +++ b/apps/mobile/src/lib/mobile-update.tsx @@ -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; + isSupported: boolean; + status: MobileUpdateStatus; +}; + +const MobileUpdateContext = createContext({ + checkForUpdate: async () => undefined, + isSupported: false, + status: "idle", +}); + +export const MobileUpdateProvider = ({ children }: { children: ReactNode }) => { + const { resolvedLocale } = useMobileLocale(); + const [status, setStatus] = useState("idle"); + const activeCheckRef = useRef | 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( + () => ({ + checkForUpdate: () => { + if (status === "ready") { + showRestartPrompt(); + return Promise.resolve(); + } + return runCheck(true); + }, + isSupported, + status, + }), + [isSupported, runCheck, showRestartPrompt, status] + ); + + return {children}; +}; + +export const useMobileUpdate = () => useContext(MobileUpdateContext); diff --git a/apps/mobile/src/screens/WorkspaceScreen.tsx b/apps/mobile/src/screens/WorkspaceScreen.tsx index 304982f0..b0f3e98b 100644 --- a/apps/mobile/src/screens/WorkspaceScreen.tsx +++ b/apps/mobile/src/screens/WorkspaceScreen.tsx @@ -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 = ({ + diff --git a/bun.lock b/bun.lock index 0318fbcf..87de0cb9 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/docs/mobile-build.md b/docs/mobile-build.md index a442b395..d6594da7 100644 --- a/docs/mobile-build.md +++ b/docs/mobile-build.md @@ -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.