Skip to content

Commit 905ae15

Browse files
committed
Improve manual update flow and auto-check config
1 parent 999ab18 commit 905ae15

6 files changed

Lines changed: 194 additions & 132 deletions

File tree

app.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
"scheme": "ccnubox",
1212
"userInterfaceStyle": "automatic",
1313
"updates": {
14-
"url": "https://ota.ccnubox.muxixyz.com/65d670f0-9625-4631-9603-f4b11f44e621"
14+
"url": "https://ota.ccnubox.muxixyz.com/65d670f0-9625-4631-9603-f4b11f44e621",
15+
"checkAutomatically": "ON_LOAD",
16+
"fallbackToCacheTimeout": 0
1517
},
1618
"assetBundlePatterns": [
1719
"src/assets/images/",

src/app/(setting)/checkUpdate.tsx

Lines changed: 135 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as Application from 'expo-application';
22
import * as Constants from 'expo-constants';
33
import * as Updates from 'expo-updates';
4-
import React, { useEffect, useState } from 'react';
4+
import React, { useMemo, useState } from 'react';
55
import { Image, StyleSheet, View } from 'react-native';
66
import { ScrollView } from 'react-native-gesture-handler';
77

@@ -12,6 +12,11 @@ import ThemeBasedView from '@/components/view';
1212

1313
import useVisualScheme from '@/store/visualScheme';
1414

15+
import {
16+
checkAndDownloadUpdateAsync,
17+
type EasUpdateProgress,
18+
} from '@/utils/easUpdate';
19+
1520
import { UpdateInfo } from '@/types/updateInfo';
1621

1722
// eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -20,19 +25,124 @@ const mxLogo = require('../../assets/images/mx-logo.png');
2025
function CheckUpdate(): React.ReactNode {
2126
const version = Application.nativeApplicationVersion;
2227
const updateInfo = Constants.default.expoConfig?.extra
23-
?.updateInfo as UpdateInfo;
24-
const [loading, setLoading] = useState(false);
28+
?.updateInfo as UpdateInfo | undefined;
29+
const [manualProgress, setManualProgress] = useState<
30+
EasUpdateProgress | 'restarting' | null
31+
>(null);
32+
const [hasDownloadedUpdate, setHasDownloadedUpdate] = useState(false);
2533
const currentStyle = useVisualScheme(state => state.currentStyle);
26-
const { isUpdateAvailable, isUpdatePending } = Updates.useUpdates();
27-
useEffect(() => {
28-
if (isUpdatePending) {
29-
void Updates.reloadAsync();
34+
const {
35+
downloadProgress,
36+
isChecking,
37+
isDownloading,
38+
isRestarting,
39+
isStartupProcedureRunning,
40+
isUpdateAvailable,
41+
isUpdatePending,
42+
} = Updates.useUpdates();
43+
44+
const canRestart = isUpdatePending || hasDownloadedUpdate;
45+
const isBusy =
46+
manualProgress !== null ||
47+
isChecking ||
48+
isDownloading ||
49+
isRestarting ||
50+
isStartupProcedureRunning;
51+
52+
const buttonLabel = useMemo(() => {
53+
if (manualProgress === 'restarting' || isRestarting) return '正在重启…';
54+
if (manualProgress === 'downloading' || isDownloading) {
55+
return downloadProgress === undefined
56+
? '正在下载…'
57+
: `正在下载 ${Math.round(downloadProgress * 100)}%`;
58+
}
59+
if (
60+
manualProgress === 'checking' ||
61+
isChecking ||
62+
isStartupProcedureRunning
63+
)
64+
return '正在检查…';
65+
if (canRestart) return '立即重启应用';
66+
if (isUpdateAvailable && !isUpdatePending) return '下载更新';
67+
return '检查更新';
68+
}, [
69+
canRestart,
70+
downloadProgress,
71+
isChecking,
72+
isDownloading,
73+
isRestarting,
74+
isStartupProcedureRunning,
75+
isUpdateAvailable,
76+
isUpdatePending,
77+
manualProgress,
78+
]);
79+
80+
const statusText = useMemo(() => {
81+
if (manualProgress === 'restarting' || isRestarting)
82+
return '正在应用更新,请稍候。';
83+
if (manualProgress === 'downloading' || isDownloading)
84+
return '正在下载更新,请保持网络连接。';
85+
if (
86+
manualProgress === 'checking' ||
87+
isChecking ||
88+
isStartupProcedureRunning
89+
)
90+
return '正在检查是否有可用更新。';
91+
if (canRestart) return '更新已下载,重启应用后即可使用。';
92+
if (isUpdateAvailable && !isUpdatePending)
93+
return '发现可用更新,点击按钮开始下载。';
94+
return '更新会在后台自动检查,也可以在这里手动检查。';
95+
}, [
96+
canRestart,
97+
isChecking,
98+
isDownloading,
99+
isRestarting,
100+
isStartupProcedureRunning,
101+
isUpdateAvailable,
102+
isUpdatePending,
103+
manualProgress,
104+
]);
105+
106+
const handleUpdatePress = async () => {
107+
if (isBusy) return;
108+
109+
if (canRestart) {
110+
setManualProgress('restarting');
111+
try {
112+
await Updates.reloadAsync();
113+
} catch {
114+
setManualProgress(null);
115+
Toast.show({ text: '应用更新失败,请稍后重试。' });
116+
}
117+
return;
118+
}
119+
120+
if (__DEV__ || !Updates.isEnabled) {
121+
Toast.show({ text: '当前构建不支持热更新检查。' });
122+
return;
30123
}
31-
}, [isUpdatePending]);
32124

33-
useEffect(() => {
34-
if (isUpdateAvailable) Updates.fetchUpdateAsync().then(_r => {});
35-
}, [isUpdateAvailable]);
125+
setManualProgress('checking');
126+
try {
127+
const result = await checkAndDownloadUpdateAsync({
128+
hasAvailableUpdate: isUpdateAvailable,
129+
onProgress: setManualProgress,
130+
});
131+
132+
if (result.status === 'downloaded') {
133+
setHasDownloadedUpdate(true);
134+
Toast.show({ text: '更新已下载,可以立即重启应用。' });
135+
} else if (result.status === 'up-to-date') {
136+
Toast.show({ text: '已是最新版', icon: 'success' });
137+
} else {
138+
Toast.show({ text: '当前构建未启用热更新。' });
139+
}
140+
} catch {
141+
Toast.show({ text: '检查更新失败,请检查网络后重试。' });
142+
} finally {
143+
setManualProgress(null);
144+
}
145+
};
36146

37147
return (
38148
<ThemeBasedView style={styles.container}>
@@ -44,61 +154,46 @@ function CheckUpdate(): React.ReactNode {
44154
</TypoText>
45155
<View style={styles.versionBlock}>
46156
<TypoText level={2} bold style={styles.versionTitle}>
47-
热更新版本 {updateInfo.otaVersion}
157+
热更新版本 {updateInfo?.otaVersion ?? Updates.runtimeVersion}
48158
</TypoText>
49159
<TypoText level="body">应用版本 {version}</TypoText>
50-
<TypoText level="body">{updateInfo.updateTime || ''}</TypoText>
160+
<TypoText level="body">{updateInfo?.updateTime ?? ''}</TypoText>
51161
</View>
52162
<View style={styles.divider} />
53163
<View style={styles.sectionBlock}>
54164
<TypoText level={3} bold style={styles.sectionTitle}>
55165
新增功能:
56166
</TypoText>
57167
<TypoText level="body" style={styles.sectionContent}>
58-
{(updateInfo.newFeatures || []).join('\n')}
168+
{(updateInfo?.newFeatures ?? []).join('\n')}
59169
</TypoText>
60170
<TypoText level={3} bold style={styles.sectionTitle}>
61171
Bug修复:
62172
</TypoText>
63173
<TypoText level="body" style={styles.sectionContent}>
64-
{(updateInfo.fixedIssues || []).join('\n')}
174+
{(updateInfo?.fixedIssues ?? []).join('\n')}
65175
</TypoText>
66176
<TypoText level={3} bold style={styles.sectionTitle}>
67177
已知问题:
68178
</TypoText>
69179
<TypoText level="body" style={styles.sectionContent}>
70-
{(updateInfo.knownIssues || []).join('\n')}
180+
{(updateInfo?.knownIssues ?? []).join('\n')}
71181
</TypoText>
72182
</View>
73183
<View style={styles.divider} />
74184
<Button
75185
style={[styles.updateButton, currentStyle?.button_style]}
76-
onPress={() => {
77-
setLoading(true);
78-
if (__DEV__) {
79-
Toast.show({ text: '已是最新版', icon: 'success' });
80-
setLoading(false);
81-
} else {
82-
Updates.checkForUpdateAsync()
83-
.then(res => {
84-
if (!res.isAvailable) {
85-
Toast.show({ text: '已是最新版', icon: 'success' });
86-
}
87-
})
88-
.catch(err => {
89-
Toast.show({ text: err.toString() });
90-
})
91-
.finally(() => {
92-
setLoading(false);
93-
});
94-
}
95-
}}
96-
isLoading={loading}
186+
onPress={() => void handleUpdatePress()}
187+
isLoading={isBusy}
97188
>
98-
检 查 更 新
189+
{buttonLabel}
99190
</Button>
100-
<TypoText level="body" style={styles.bottomTip}>
101-
更新后请重启应用以确保新功能生效。
191+
<TypoText
192+
level="body"
193+
style={styles.bottomTip}
194+
accessibilityLiveRegion="polite"
195+
>
196+
{statusText}
102197
</TypoText>
103198
</View>
104199
</ScrollView>

src/app/_layout.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import { usePortalStore } from '../store/portal';
1717
import useScraper from '../store/scraper';
1818
import useVisualScheme from '../store/visualScheme';
1919
import { commonColors } from '../styles/common';
20-
import { fetchUpdate } from '../utils';
2120

2221
export default function RootLayout() {
2322
const rootNavigationState = useRootNavigationState();
@@ -40,35 +39,30 @@ export default function RootLayout() {
4039
useJPush();
4140
useBadgeSync();
4241

43-
const initApp = React.useCallback(async () => {
42+
React.useEffect(() => {
4443
// 引入所有样式以及基于 theme 的组件
4544
initVisualScheme();
4645
// 加载字体
4746
void loadAsync({
47+
// eslint-disable-next-line @typescript-eslint/no-require-imports
4848
antoutline: require('@ant-design/icons-react-native/fonts/antoutline.ttf'),
4949
});
5050
// 配置Toast
5151
Toast.config({ mask: false, stackable: true });
52-
// 获取更新
53-
if (!__DEV__) {
54-
fetchUpdate();
55-
}
5652
// 在 store 中设置爬虫 ref
5753
setRef(scraperRef as React.RefObject<WebView>);
5854
// 在 store 中配置 portal ref
5955
setPortalRef(portalRef);
6056
}, [initVisualScheme, setPortalRef, setRef]);
6157

6258
React.useEffect(() => {
63-
initApp();
6459
const listener = Appearance.addChangeListener(scheme => {
65-
console.log('toggled change scheme', scheme);
6660
if (isAutoTheme) {
6761
changeTheme(scheme.colorScheme === 'dark' ? 'dark' : 'light');
6862
}
6963
});
7064
return () => listener.remove();
71-
}, [isAutoTheme, changeTheme, initApp]);
65+
}, [isAutoTheme, changeTheme]);
7266

7367
React.useEffect(() => {
7468
const activeRootRouteName =

src/utils/easUpdate.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import * as Updates from 'expo-updates';
2+
3+
export type EasUpdateResult =
4+
| { status: 'disabled' }
5+
| { status: 'up-to-date' }
6+
| { status: 'downloaded' };
7+
8+
export type EasUpdateProgress = 'checking' | 'downloading';
9+
10+
type EasUpdateOptions = {
11+
hasAvailableUpdate?: boolean;
12+
onProgress?: (progress: EasUpdateProgress) => void;
13+
};
14+
15+
let activeUpdateOperation: Promise<EasUpdateResult> | null = null;
16+
17+
const runUpdateOperation = async (
18+
options: EasUpdateOptions,
19+
): Promise<EasUpdateResult> => {
20+
if (!Updates.isEnabled) {
21+
return { status: 'disabled' };
22+
}
23+
24+
if (!options.hasAvailableUpdate) {
25+
options.onProgress?.('checking');
26+
const checkResult = await Updates.checkForUpdateAsync();
27+
if (!checkResult.isAvailable && !checkResult.isRollBackToEmbedded) {
28+
return { status: 'up-to-date' };
29+
}
30+
}
31+
32+
options.onProgress?.('downloading');
33+
const fetchResult = await Updates.fetchUpdateAsync();
34+
if (fetchResult.isNew || fetchResult.isRollBackToEmbedded) {
35+
return { status: 'downloaded' };
36+
}
37+
38+
return { status: 'up-to-date' };
39+
};
40+
41+
export const checkAndDownloadUpdateAsync = (
42+
options: EasUpdateOptions = {},
43+
): Promise<EasUpdateResult> => {
44+
if (activeUpdateOperation) {
45+
return activeUpdateOperation;
46+
}
47+
48+
activeUpdateOperation = runUpdateOperation(options).finally(() => {
49+
activeUpdateOperation = null;
50+
});
51+
52+
return activeUpdateOperation;
53+
};

0 commit comments

Comments
 (0)