feat(mobile): install Android updates in app
This commit is contained in:
1 parent
62718b0b7b
commit
bfc70189e3
9 files changed
+238
-59
No files matched your search
@@ -34,6 +34,9 @@
|
||||
"android": {
|
||||
"package": "org.edgeever.mobile",
|
||||
"versionCode": 122,
|
||||
"permissions": [
|
||||
"android.permission.REQUEST_INSTALL_PACKAGES"
|
||||
],
|
||||
"adaptiveIcon": {
|
||||
"backgroundColor": "#16A06E",
|
||||
"backgroundImage": "./assets/icon.png",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"expo-file-system": "~57.0.1",
|
||||
"expo-image-manipulator": "~57.0.5",
|
||||
"expo-image-picker": "~57.0.5",
|
||||
"expo-intent-launcher": "~57.0.1",
|
||||
"expo-router": "latest",
|
||||
"expo-secure-store": "latest",
|
||||
"expo-sharing": "~57.0.8",
|
||||
|
||||
@@ -10,20 +10,22 @@ import { useMobileTheme, resolveMobileThemeStyles } from "../lib/mobile-theme";
|
||||
export const MobileUpdateCard = () => {
|
||||
const { resolvedLocale } = useMobileLocale();
|
||||
const { resolvedTheme } = useMobileTheme();
|
||||
const { checkForUpdate, hasUpdate, isSupported, openUpdate, status, updateKind } = useMobileUpdate();
|
||||
const { checkForUpdate, downloadProgress, hasUpdate, isSupported, openUpdate, status, updateKind } = useMobileUpdate();
|
||||
const english = resolvedLocale === "en-US";
|
||||
const busy = status === "checking" || status === "downloading";
|
||||
const busy = status === "checking" || status === "downloading" || status === "installing";
|
||||
const styles = resolveMobileThemeStyles(baseStyles, resolvedTheme);
|
||||
const checkLabel = status === "checking"
|
||||
? (english ? "Checking…" : "正在检查…")
|
||||
: (english ? "Check for updates" : "检查更新");
|
||||
const openLabel = status === "downloading"
|
||||
? (english ? "Downloading…" : "正在下载…")
|
||||
? (english ? `Downloading ${Math.round((downloadProgress ?? 0) * 100)}%` : `正在下载 ${Math.round((downloadProgress ?? 0) * 100)}%`)
|
||||
: status === "installing"
|
||||
? (english ? "Opening installer…" : "正在打开安装器…")
|
||||
: status === "ready"
|
||||
? (english ? "Restart to apply" : "重启以应用")
|
||||
: updateKind === "ota"
|
||||
? (english ? "Download update" : "下载更新")
|
||||
: (english ? "Get update" : "前往更新");
|
||||
: (english ? "Download & install" : "下载并安装");
|
||||
|
||||
return (
|
||||
<View style={styles.card}>
|
||||
@@ -31,8 +33,8 @@ export const MobileUpdateCard = () => {
|
||||
<Text style={styles.title}>{english ? "App updates" : "应用更新"}</Text>
|
||||
<Text style={styles.description}>
|
||||
{english
|
||||
? "EdgeEver automatically checks for compatible in-app updates. Manual checks also look for newer installable versions."
|
||||
: "EdgeEver 会自动检查兼容的应用内热更新;手动检查还会查找更新的安装包版本。"}
|
||||
? "EdgeEver automatically checks for compatible in-app updates and newer installable versions."
|
||||
: "EdgeEver 会自动检查兼容的应用内热更新和新版安装包。"}
|
||||
</Text>
|
||||
<Text style={styles.version}>
|
||||
{english ? "Current version" : "当前版本"}: v{Updates.runtimeVersion ?? Constants.expoConfig?.version ?? "unknown"}
|
||||
@@ -41,12 +43,12 @@ export const MobileUpdateCard = () => {
|
||||
{hasUpdate ? (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ busy: status === "downloading", disabled: status === "downloading" }}
|
||||
disabled={status === "downloading"}
|
||||
accessibilityState={{ busy, disabled: busy }}
|
||||
disabled={busy}
|
||||
onPress={() => void openUpdate()}
|
||||
style={[styles.button, status === "downloading" && styles.buttonDisabled]}
|
||||
style={[styles.button, busy && styles.buttonDisabled]}
|
||||
>
|
||||
{status === "downloading" ? <ActivityIndicator color="#047857" size="small" /> : <RefreshCw color="#047857" size={16} />}
|
||||
{status === "downloading" || status === "installing" ? <ActivityIndicator color="#047857" size="small" /> : <RefreshCw color="#047857" size={16} />}
|
||||
<Text style={styles.buttonText}>{openLabel}</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
|
||||
@@ -24,12 +24,13 @@ export const MobileUpdateToast = () => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const { resolvedLocale } = useMobileLocale();
|
||||
const { resolvedTheme } = useMobileTheme();
|
||||
const { hasUpdate, installedVersion, openUpdate, status, updateKind } = useMobileUpdate();
|
||||
const { downloadProgress, hasUpdate, installedVersion, openUpdate, status, updateKind } = useMobileUpdate();
|
||||
const english = resolvedLocale === "en-US";
|
||||
const styles = resolveMobileThemeStyles(baseStyles, resolvedTheme);
|
||||
const [storageReady, setStorageReady] = useState(false);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const dismissedForVersionRef = useRef<string | null>(null);
|
||||
const showingDownloadRef = useRef(false);
|
||||
const showingRef = useRef(false);
|
||||
const opacity = useRef(new Animated.Value(0)).current;
|
||||
|
||||
@@ -46,7 +47,32 @@ export const MobileUpdateToast = () => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!storageReady || !hasUpdate || status === "downloading") {
|
||||
if (!storageReady || !hasUpdate) {
|
||||
return;
|
||||
}
|
||||
if (status === "downloading" || status === "installing") {
|
||||
showingDownloadRef.current = true;
|
||||
showingRef.current = true;
|
||||
setVisible(true);
|
||||
Animated.timing(opacity, {
|
||||
duration: FADE_MS,
|
||||
toValue: 1,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
return;
|
||||
}
|
||||
if (showingDownloadRef.current) {
|
||||
showingDownloadRef.current = false;
|
||||
Animated.timing(opacity, {
|
||||
duration: FADE_MS,
|
||||
toValue: 0,
|
||||
useNativeDriver: true,
|
||||
}).start(({ finished }) => {
|
||||
if (finished) {
|
||||
setVisible(false);
|
||||
showingRef.current = false;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!shouldShowMobileUpdateToastForVersion(installedVersion, dismissedForVersionRef.current)) {
|
||||
@@ -87,8 +113,16 @@ export const MobileUpdateToast = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const title = english ? "Update available" : "发现新版本";
|
||||
const detail = status === "ready"
|
||||
const title = status === "downloading"
|
||||
? (english ? "Downloading update" : "正在下载更新")
|
||||
: status === "installing"
|
||||
? (english ? "Installer ready" : "安装包已就绪")
|
||||
: (english ? "Update available" : "发现新版本");
|
||||
const detail = status === "downloading"
|
||||
? `${Math.round((downloadProgress ?? 0) * 100)}%`
|
||||
: status === "installing"
|
||||
? (english ? "Opening the system installer" : "正在打开系统安装器")
|
||||
: status === "ready"
|
||||
? (english ? "Restart in System info" : "可在系统信息中重启")
|
||||
: updateKind === "ota"
|
||||
? (english ? "Optional in-app update" : "可选的应用内更新")
|
||||
@@ -125,8 +159,10 @@ export const MobileUpdateToast = () => {
|
||||
accessibilityLabel={`${title}. ${detail}`}
|
||||
accessibilityRole="button"
|
||||
onPress={() => {
|
||||
dismiss();
|
||||
void openUpdate();
|
||||
if (status !== "downloading" && status !== "installing") {
|
||||
dismiss();
|
||||
void openUpdate();
|
||||
}
|
||||
}}
|
||||
style={styles.toastBody}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { MobileRelease } from "./mobile-release";
|
||||
|
||||
const APK_MIME_TYPE = "application/vnd.android.package-archive";
|
||||
const ACTION_VIEW = "android.intent.action.VIEW";
|
||||
const FLAG_GRANT_READ_URI_PERMISSION = 1;
|
||||
|
||||
export type AndroidApkDownloadProgress = {
|
||||
downloadedBytes: number;
|
||||
progress: number;
|
||||
totalBytes: number;
|
||||
};
|
||||
|
||||
export const downloadAndInstallAndroidApk = async (
|
||||
release: MobileRelease,
|
||||
onProgress: (progress: AndroidApkDownloadProgress) => void,
|
||||
onDownloaded: () => void
|
||||
) => {
|
||||
const [{ File, Paths }, LegacyFileSystem, IntentLauncher] = await Promise.all([
|
||||
import("expo-file-system"),
|
||||
import("expo-file-system/legacy"),
|
||||
import("expo-intent-launcher"),
|
||||
]);
|
||||
const destination = new File(Paths.cache, release.fileName);
|
||||
|
||||
try {
|
||||
const downloadedFile = await File.downloadFileAsync(release.downloadUrl, destination, {
|
||||
idempotent: true,
|
||||
onProgress: ({ bytesWritten, totalBytes }) => {
|
||||
const expectedBytes = totalBytes > 0 ? totalBytes : release.size;
|
||||
onProgress({
|
||||
downloadedBytes: bytesWritten,
|
||||
progress: expectedBytes > 0 ? Math.min(bytesWritten / expectedBytes, 1) : 0,
|
||||
totalBytes: expectedBytes,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (downloadedFile.size !== release.size) {
|
||||
throw new Error(`Downloaded APK size mismatch: expected ${release.size}, received ${downloadedFile.size}`);
|
||||
}
|
||||
|
||||
onProgress({ downloadedBytes: release.size, progress: 1, totalBytes: release.size });
|
||||
onDownloaded();
|
||||
const contentUri = await LegacyFileSystem.getContentUriAsync(downloadedFile.uri);
|
||||
await IntentLauncher.startActivityAsync(ACTION_VIEW, {
|
||||
data: contentUri,
|
||||
flags: FLAG_GRANT_READ_URI_PERMISSION,
|
||||
type: APK_MIME_TYPE,
|
||||
});
|
||||
} catch (error) {
|
||||
if (destination.exists) {
|
||||
destination.delete();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -7,7 +7,12 @@ import {
|
||||
} from "./mobile-release";
|
||||
|
||||
const responseWithTag = (tagName: string, androidVersion = tagName.replace(/^v/, "")) => new Response(JSON.stringify({
|
||||
assets: [{ name: `edgeever-android-v${androidVersion}-arm64-v8a.apk` }],
|
||||
assets: [{
|
||||
browser_download_url: `https://github.com/tianma-if/edgeever/releases/download/${tagName}/edgeever-android-v${androidVersion}-arm64-v8a.apk`,
|
||||
digest: `sha256:${"a".repeat(64)}`,
|
||||
name: `edgeever-android-v${androidVersion}-arm64-v8a.apk`,
|
||||
size: 48_000_000,
|
||||
}],
|
||||
tag_name: tagName,
|
||||
}), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -17,7 +22,13 @@ const responseWithTag = (tagName: string, androidVersion = tagName.replace(/^v/,
|
||||
test("finds a newer formal GitHub release", async () => {
|
||||
const release = await findNewerMobileRelease("0.4.14", async () => responseWithTag("v0.4.15"));
|
||||
|
||||
expect(release).toEqual({ version: "0.4.15" });
|
||||
expect(release).toEqual({
|
||||
digest: `sha256:${"a".repeat(64)}`,
|
||||
downloadUrl: "https://github.com/tianma-if/edgeever/releases/download/v0.4.15/edgeever-android-v0.4.15-arm64-v8a.apk",
|
||||
fileName: "edgeever-android-v0.4.15-arm64-v8a.apk",
|
||||
size: 48_000_000,
|
||||
version: "0.4.15",
|
||||
});
|
||||
});
|
||||
|
||||
test("does not treat the current or an older release as an update", async () => {
|
||||
@@ -28,6 +39,10 @@ test("does not treat the current or an older release as an update", async () =>
|
||||
test("uses the actual reused Android asset version instead of the overall release tag", async () => {
|
||||
expect(await findNewerMobileRelease("1.6.50", async () => responseWithTag("v1.7.0", "1.6.50"))).toBeNull();
|
||||
expect(await findNewerMobileRelease("1.6.49", async () => responseWithTag("v1.7.0", "1.6.50"))).toEqual({
|
||||
digest: `sha256:${"a".repeat(64)}`,
|
||||
downloadUrl: "https://github.com/tianma-if/edgeever/releases/download/v1.7.0/edgeever-android-v1.6.50-arm64-v8a.apk",
|
||||
fileName: "edgeever-android-v1.6.50-arm64-v8a.apk",
|
||||
size: 48_000_000,
|
||||
version: "1.6.50",
|
||||
});
|
||||
});
|
||||
@@ -43,6 +58,15 @@ test("rejects invalid release responses instead of claiming the app is current",
|
||||
assets: [],
|
||||
tag_name: "v0.4.15",
|
||||
})))).rejects.toThrow("exactly one Android APK");
|
||||
await expect(findNewerMobileRelease("0.4.14", async () => new Response(JSON.stringify({
|
||||
assets: [{
|
||||
browser_download_url: "https://attacker.invalid/edgeever.apk",
|
||||
digest: `sha256:${"a".repeat(64)}`,
|
||||
name: "edgeever-android-v0.4.15-arm64-v8a.apk",
|
||||
size: 48_000_000,
|
||||
}],
|
||||
tag_name: "v0.4.15",
|
||||
})))).rejects.toThrow("exactly one Android APK");
|
||||
});
|
||||
|
||||
test("uses GitHub Releases as the only install update destination", () => {
|
||||
|
||||
@@ -31,6 +31,10 @@ type LatestReleaseResponse = {
|
||||
};
|
||||
|
||||
export type MobileRelease = {
|
||||
digest: string;
|
||||
downloadUrl: string;
|
||||
fileName: string;
|
||||
size: number;
|
||||
version: string;
|
||||
};
|
||||
|
||||
@@ -41,19 +45,48 @@ const normalizeVersion = (value: string) => {
|
||||
|
||||
const ANDROID_ASSET_PATTERN = /^edgeever-android-v(\d+\.\d+\.\d+)-arm64-v8a\.apk$/;
|
||||
|
||||
export const findAndroidReleaseVersion = (assets: unknown) => {
|
||||
const findAndroidReleaseAsset = (assets: unknown): MobileRelease | null => {
|
||||
if (!Array.isArray(assets)) return null;
|
||||
const versions = assets
|
||||
const releases = assets
|
||||
.map((asset) => {
|
||||
if (!asset || typeof asset !== "object" || !("name" in asset) || typeof asset.name !== "string") {
|
||||
if (
|
||||
!asset ||
|
||||
typeof asset !== "object" ||
|
||||
!("name" in asset) ||
|
||||
typeof asset.name !== "string"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return ANDROID_ASSET_PATTERN.exec(asset.name)?.[1] ?? null;
|
||||
const version = ANDROID_ASSET_PATTERN.exec(asset.name)?.[1];
|
||||
if (
|
||||
!version ||
|
||||
!("browser_download_url" in asset) ||
|
||||
typeof asset.browser_download_url !== "string" ||
|
||||
!asset.browser_download_url.startsWith("https://github.com/tianma-if/edgeever/releases/download/") ||
|
||||
!("digest" in asset) ||
|
||||
typeof asset.digest !== "string" ||
|
||||
!/^sha256:[a-f0-9]{64}$/.test(asset.digest) ||
|
||||
!("size" in asset) ||
|
||||
typeof asset.size !== "number" ||
|
||||
!Number.isSafeInteger(asset.size) ||
|
||||
asset.size <= 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
digest: asset.digest,
|
||||
downloadUrl: asset.browser_download_url,
|
||||
fileName: asset.name,
|
||||
size: asset.size,
|
||||
version,
|
||||
};
|
||||
})
|
||||
.filter((version): version is string => Boolean(version));
|
||||
return versions.length === 1 ? versions[0] : null;
|
||||
.filter((release): release is MobileRelease => Boolean(release));
|
||||
return releases.length === 1 ? releases[0] : null;
|
||||
};
|
||||
|
||||
export const findAndroidReleaseVersion = (assets: unknown) => findAndroidReleaseAsset(assets)?.version ?? null;
|
||||
|
||||
export const findNewerMobileRelease = async (
|
||||
currentVersion: string,
|
||||
fetchRelease: typeof fetch = fetch
|
||||
@@ -81,10 +114,10 @@ export const findNewerMobileRelease = async (
|
||||
if (!releaseVersion) {
|
||||
throw new Error(`Invalid GitHub release version: ${release.tag_name}`);
|
||||
}
|
||||
const latestVersion = findAndroidReleaseVersion(release.assets);
|
||||
if (!latestVersion) {
|
||||
const latestRelease = findAndroidReleaseAsset(release.assets);
|
||||
if (!latestRelease) {
|
||||
throw new Error("GitHub release response does not contain exactly one Android APK");
|
||||
}
|
||||
|
||||
return gt(latestVersion, normalizedCurrentVersion) ? { version: latestVersion } : null;
|
||||
return gt(latestRelease.version, normalizedCurrentVersion) ? latestRelease : null;
|
||||
};
|
||||
@@ -1,22 +1,20 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||
import Constants from "expo-constants";
|
||||
import { AppState, Linking, Platform, type AppStateStatus } from "react-native";
|
||||
import { AppState, Platform, type AppStateStatus } from "react-native";
|
||||
import * as Updates from "expo-updates";
|
||||
import { Alert } from "../components/LocalizedText";
|
||||
import { useMobileLocale } from "./mobile-locale";
|
||||
import {
|
||||
ANDROID_INSTALL_UPDATE_SOURCES,
|
||||
findNewerMobileRelease,
|
||||
getDefaultMobileInstallUpdateUrl,
|
||||
} from "./mobile-release";
|
||||
import { downloadAndInstallAndroidApk } from "./android-apk-update";
|
||||
import { findNewerMobileRelease, type MobileRelease } from "./mobile-release";
|
||||
|
||||
const FOREGROUND_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
||||
|
||||
type MobileUpdateStatus = "idle" | "checking" | "available" | "downloading" | "ready";
|
||||
type MobileUpdateStatus = "idle" | "checking" | "available" | "downloading" | "ready" | "installing";
|
||||
export type MobileUpdateKind = "install" | "ota";
|
||||
|
||||
type MobileUpdateContextValue = {
|
||||
checkForUpdate: () => Promise<void>;
|
||||
downloadProgress: number | null;
|
||||
hasUpdate: boolean;
|
||||
installedVersion: string | null;
|
||||
isSupported: boolean;
|
||||
@@ -27,6 +25,7 @@ type MobileUpdateContextValue = {
|
||||
|
||||
const MobileUpdateContext = createContext<MobileUpdateContextValue>({
|
||||
checkForUpdate: async () => undefined,
|
||||
downloadProgress: null,
|
||||
hasUpdate: false,
|
||||
installedVersion: null,
|
||||
isSupported: false,
|
||||
@@ -37,6 +36,8 @@ const MobileUpdateContext = createContext<MobileUpdateContextValue>({
|
||||
|
||||
export const MobileUpdateProvider = ({ children }: { children: ReactNode }) => {
|
||||
const { resolvedLocale } = useMobileLocale();
|
||||
const [downloadProgress, setDownloadProgress] = useState<number | null>(null);
|
||||
const [installRelease, setInstallRelease] = useState<MobileRelease | null>(null);
|
||||
const [status, setStatus] = useState<MobileUpdateStatus>("idle");
|
||||
const [updateKind, setUpdateKind] = useState<MobileUpdateKind | null>(null);
|
||||
const activeCheckRef = useRef<Promise<void> | null>(null);
|
||||
@@ -73,6 +74,7 @@ export const MobileUpdateProvider = ({ children }: { children: ReactNode }) => {
|
||||
}
|
||||
const release = await findNewerMobileRelease(installedVersion);
|
||||
if (release) {
|
||||
setInstallRelease(release);
|
||||
setUpdateKind("install");
|
||||
setStatus("available");
|
||||
return;
|
||||
@@ -85,6 +87,7 @@ export const MobileUpdateProvider = ({ children }: { children: ReactNode }) => {
|
||||
const result = await Updates.checkForUpdateAsync();
|
||||
|
||||
if (!result.isAvailable) {
|
||||
setInstallRelease(null);
|
||||
setUpdateKind(null);
|
||||
setStatus("idle");
|
||||
return;
|
||||
@@ -93,6 +96,7 @@ export const MobileUpdateProvider = ({ children }: { children: ReactNode }) => {
|
||||
setUpdateKind("ota");
|
||||
setStatus("available");
|
||||
} catch {
|
||||
setInstallRelease(null);
|
||||
setUpdateKind(null);
|
||||
setStatus("idle");
|
||||
}
|
||||
@@ -128,30 +132,46 @@ export const MobileUpdateProvider = ({ children }: { children: ReactNode }) => {
|
||||
|
||||
const openUpdate = useCallback(async () => {
|
||||
if (updateKind === "install") {
|
||||
if (Platform.OS === "android") {
|
||||
Alert.alert(
|
||||
english ? "Update available" : "发现新版本",
|
||||
english
|
||||
? "Get the latest version from GitHub Releases."
|
||||
: "可从 GitHub Releases 获取最新版本。",
|
||||
[
|
||||
...ANDROID_INSTALL_UPDATE_SOURCES.map((source) => ({
|
||||
text: english ? source.labelEn : source.labelZh,
|
||||
onPress: () => {
|
||||
void Linking.openURL(source.url);
|
||||
},
|
||||
})),
|
||||
{
|
||||
text: english ? "Cancel" : "取消",
|
||||
style: "cancel" as const,
|
||||
},
|
||||
]
|
||||
);
|
||||
if (Platform.OS !== "android" || !installRelease || status === "downloading" || status === "installing") {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = getDefaultMobileInstallUpdateUrl(Platform.OS);
|
||||
await Linking.openURL(url);
|
||||
const sizeMb = Math.max(1, Math.ceil(installRelease.size / 1024 / 1024));
|
||||
Alert.alert(
|
||||
english ? "Update available" : "发现新版本",
|
||||
english
|
||||
? `EdgeEver ${installRelease.version} is available. Download ${sizeMb} MB and install it now?`
|
||||
: `EdgeEver ${installRelease.version} 已发布。是否立即下载 ${sizeMb} MB 安装包并更新?`,
|
||||
[
|
||||
{
|
||||
text: english ? "Cancel" : "取消",
|
||||
style: "cancel",
|
||||
},
|
||||
{
|
||||
text: english ? "Download & install" : "下载并安装",
|
||||
onPress: () => {
|
||||
setDownloadProgress(0);
|
||||
setStatus("downloading");
|
||||
void downloadAndInstallAndroidApk(
|
||||
installRelease,
|
||||
({ progress }) => setDownloadProgress(progress),
|
||||
() => setStatus("installing")
|
||||
).then(() => {
|
||||
setDownloadProgress(null);
|
||||
setStatus("available");
|
||||
}).catch(() => {
|
||||
setDownloadProgress(null);
|
||||
setStatus("available");
|
||||
Alert.alert(
|
||||
english ? "Update failed" : "更新失败",
|
||||
english
|
||||
? "Could not download or open the installer. Try again later."
|
||||
: "无法下载安装包或打开系统安装器,请稍后重试。"
|
||||
);
|
||||
});
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -204,21 +224,22 @@ export const MobileUpdateProvider = ({ children }: { children: ReactNode }) => {
|
||||
: "无法下载应用内更新,请稍后再试。"
|
||||
);
|
||||
}
|
||||
}, [english, isSupported, status, updateKind]);
|
||||
}, [english, installRelease, isSupported, status, updateKind]);
|
||||
|
||||
const value = useMemo<MobileUpdateContextValue>(
|
||||
() => ({
|
||||
checkForUpdate: () => {
|
||||
return runCheck(true);
|
||||
},
|
||||
hasUpdate: status === "available" || status === "ready" || status === "downloading",
|
||||
downloadProgress,
|
||||
hasUpdate: status === "available" || status === "ready" || status === "downloading" || status === "installing",
|
||||
installedVersion,
|
||||
isSupported,
|
||||
openUpdate,
|
||||
status,
|
||||
updateKind,
|
||||
}),
|
||||
[installedVersion, isSupported, openUpdate, runCheck, status, updateKind]
|
||||
[downloadProgress, installedVersion, isSupported, openUpdate, runCheck, status, updateKind]
|
||||
);
|
||||
|
||||
return <MobileUpdateContext.Provider value={value}>{children}</MobileUpdateContext.Provider>;
|
||||
|
||||
@@ -135,6 +135,7 @@
|
||||
"expo-file-system": "~57.0.1",
|
||||
"expo-image-manipulator": "~57.0.5",
|
||||
"expo-image-picker": "~57.0.5",
|
||||
"expo-intent-launcher": "~57.0.1",
|
||||
"expo-router": "latest",
|
||||
"expo-secure-store": "latest",
|
||||
"expo-sharing": "~57.0.8",
|
||||
@@ -2157,6 +2158,8 @@
|
||||
|
||||
"expo-image-picker": ["expo-image-picker@57.0.8", "", { "dependencies": { "expo-image-loader": "~57.0.1" }, "peerDependencies": { "expo": "*" } }, "sha512-LXJCGxunnQ4beqk+TW3+llRyss/XITOKWdIjqgF1H9mL3CgKN+hvABWAqLVWapGz7RoJBf+r53FO9i48yicbbw=="],
|
||||
|
||||
"expo-intent-launcher": ["expo-intent-launcher@57.0.1", "", { "peerDependencies": { "expo": "*" } }, "sha512-BYiPEOUeW3AkW/uxEESbS2UBw9LasJ8Heb6A8kF9Ueh6pqrXQHMNJFHzfrOP8qZfv9G2e8CTzrtzpN5p0vH5fA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
Reference in new issue
Block a user