Skip to content

Commit 870b185

Browse files
committed
fix review issues
1 parent c4b10c7 commit 870b185

12 files changed

Lines changed: 213 additions & 39 deletions

File tree

AGENTS.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,24 @@ npm run test:coverage # Jest with coverage
2727
npx jest <file> --no-coverage # Run a single test file (e.g., src/__tests__/xxx.test.ts)
2828
```
2929

30+
## Required Validation
31+
32+
Before considering a task complete, run every validation command that applies to the files
33+
changed in the task:
34+
35+
- After modifying TypeScript or JavaScript files (`.ts`, `.tsx`, `.js`, or `.jsx`), run
36+
`npm run lint:fix` and then `npm run type-check`. Run the type check after lint fixes so it
37+
validates the final code.
38+
- After modifying Expo config plugin implementation files under `plugins/`, or their TypeScript
39+
build configuration, run `npm run plugin:build` after linting.
40+
- After modifying application/runtime behavior or tests, run `npm run test`. This includes changes
41+
to screens, hooks, services, stores, tasks, utilities, native-module JavaScript/TypeScript APIs,
42+
and test files. Documentation-only, comment-only, and formatting-only changes do not require the
43+
test suite.
44+
- After modifying Markdown or JSON files, run `npm run format-docs`.
45+
- If multiple rules apply, run all applicable commands. After any auto-fix, build, or formatting
46+
command, inspect the diff and do not include unrelated generated or formatting changes.
47+
3048
## Architecture Overview
3149

3250
This is a React Native clipboard synchronization app built with **Expo SDK 55**, using React Native 0.83 and React 19. It syncs clipboard content (text, images, files) between devices via SyncClipboard Server, WebDAV, or S3 backends.

App.tsx

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,33 +14,11 @@ import { useTheme } from './src/hooks/useTheme';
1414
import { setDynamicShortcuts } from 'shortcut';
1515
import { moveTaskToBack, setExcludeFromRecents } from 'native-util';
1616
import { networkAutoSwitchService } from './src/services/NetworkAutoSwitchService';
17-
18-
const QUICK_UPLOAD_URL = 'syncclipboard://quick-upload';
19-
const QUICK_DOWNLOAD_URL = 'syncclipboard://quick-download';
20-
21-
function parseQuickTileUrl(url: string | null): {
22-
isQuickTile: boolean;
23-
fromForeground: boolean;
24-
direction: SyncDirection;
25-
} {
26-
if (!url) return { isQuickTile: false, fromForeground: false, direction: SyncDirection.Download };
27-
const fromForeground = url.includes('fg=1');
28-
// Check upload first — its URL is a superset of the download prefix
29-
if (url.startsWith(QUICK_UPLOAD_URL))
30-
return { isQuickTile: true, fromForeground, direction: SyncDirection.Upload };
31-
if (url.startsWith(QUICK_DOWNLOAD_URL))
32-
return { isQuickTile: true, fromForeground, direction: SyncDirection.Download };
33-
return { isQuickTile: false, fromForeground: false, direction: SyncDirection.Download };
34-
}
35-
36-
function isShareIntentUrl(url: string | null): boolean {
37-
if (!url) return false;
38-
try {
39-
return new URL(url).hostname === 'expo-sharing';
40-
} catch {
41-
return false;
42-
}
43-
}
17+
import {
18+
isShareIntentUrl,
19+
parseQuickTileUrl,
20+
shouldEnableAutoUpdateCheck,
21+
} from './src/utils/appLaunch';
4422

4523
async function runOverlayNetworkPreflight(): Promise<void> {
4624
try {
@@ -54,6 +32,7 @@ type AppMode = 'checking' | 'home';
5432

5533
export default function App() {
5634
const [appMode, setAppMode] = useState<AppMode>('checking');
35+
const [autoUpdateCheckEnabled, setAutoUpdateCheckEnabled] = useState(false);
5736
// 快速操作覆盖层:始终以 overlay 形式显示,不卸载 AppNavigator/HomeScreen
5837
const [shareReceiveOverlay, setShareReceiveOverlay] = useState(false);
5938
const [quickActionOverlay, setQuickActionOverlay] = useState<{
@@ -112,6 +91,9 @@ export default function App() {
11291
await runOverlayNetworkPreflight();
11392
// fg=1 完成后留在 app,fg=0/无fg 完成后退出
11493
setQuickActionOverlay({ direction, exitAfterSync: !fromForeground });
94+
} else if (shouldEnableAutoUpdateCheck(url)) {
95+
// URL 解析完成且不是 overlay 冷启动,才允许首页自动检查更新。
96+
setAutoUpdateCheckEnabled(true);
11597
}
11698
});
11799

@@ -141,7 +123,9 @@ export default function App() {
141123
<ThemeProvider>
142124
<I18nProvider>
143125
<ThemedStatusBar />
144-
{appMode === 'checking' ? null : <AppNavigator />}
126+
{appMode === 'checking' ? null : (
127+
<AppNavigator autoUpdateCheckEnabled={autoUpdateCheckEnabled} />
128+
)}
145129
{shareReceiveOverlay && (
146130
<View style={StyleSheet.absoluteFill}>
147131
<ShareReceiveScreen

CLAUDE.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,24 @@ npm run test:coverage # Jest with coverage
2727
npx jest <file> --no-coverage # Run a single test file (e.g., src/__tests__/xxx.test.ts)
2828
```
2929

30+
## Required Validation
31+
32+
Before considering a task complete, run every validation command that applies to the files
33+
changed in the task:
34+
35+
- After modifying TypeScript or JavaScript files (`.ts`, `.tsx`, `.js`, or `.jsx`), run
36+
`npm run lint:fix` and then `npm run type-check`. Run the type check after lint fixes so it
37+
validates the final code.
38+
- After modifying Expo config plugin implementation files under `plugins/`, or their TypeScript
39+
build configuration, run `npm run plugin:build` after linting.
40+
- After modifying application/runtime behavior or tests, run `npm run test`. This includes changes
41+
to screens, hooks, services, stores, tasks, utilities, native-module JavaScript/TypeScript APIs,
42+
and test files. Documentation-only, comment-only, and formatting-only changes do not require the
43+
test suite.
44+
- After modifying Markdown or JSON files, run `npm run format-docs`.
45+
- If multiple rules apply, run all applicable commands. After any auto-fix, build, or formatting
46+
command, inspect the diff and do not include unrelated generated or formatting changes.
47+
3048
## Architecture Overview
3149

3250
This is a React Native clipboard synchronization app built with **Expo SDK 55**, using React Native 0.83 and React 19. It syncs clipboard content (text, images, files) between devices via SyncClipboard Server, WebDAV, or S3 backends.

src/__tests__/UpdateService.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ function createDependencies(
3333
download: jest.fn(async () => 'file:///update.apk'),
3434
install: jest.fn(async () => {}),
3535
getToday: jest.fn(() => '2026-08-23'),
36+
isAndroid: jest.fn(() => true),
3637
...overrides,
3738
};
3839
}
@@ -66,6 +67,19 @@ describe('UpdateService', () => {
6667
});
6768
});
6869

70+
it('iOS 自动检查静默跳过且不会进入 APK 下载流程', async () => {
71+
const dependencies = createDependencies({ isAndroid: jest.fn(() => false) });
72+
const service = new UpdateService(dependencies);
73+
74+
await expect(service.checkAutomatically()).resolves.toBeNull();
75+
expect(dependencies.check).not.toHaveBeenCalled();
76+
await expect(
77+
service.downloadAndInstall('github', updateResult.latestVersion, updateResult.assets)
78+
).rejects.toThrow();
79+
expect(dependencies.download).not.toHaveBeenCalled();
80+
expect(dependencies.install).not.toHaveBeenCalled();
81+
});
82+
6983
it('下载时持续发布进度并在完成后调用安装器', async () => {
7084
const dependencies = createDependencies({
7185
download: jest.fn(async (options) => {
@@ -83,4 +97,57 @@ describe('UpdateService', () => {
8397
expect(dependencies.install).toHaveBeenCalledWith('file:///update.apk');
8498
expect(service.getState()).toMatchObject({ isDownloading: false, downloadProgress: 0 });
8599
});
100+
101+
it('取消尚未退出的下载时保持互斥,阻止立即重试覆盖同一缓存文件', async () => {
102+
const dependencies = createDependencies({
103+
download: jest.fn(
104+
(options) =>
105+
new Promise<string>((_resolve, reject) => {
106+
options.signal?.addEventListener('abort', () => {
107+
reject(new DOMException('Aborted', 'AbortError'));
108+
});
109+
})
110+
),
111+
});
112+
const service = new UpdateService(dependencies);
113+
const firstDownload = service.downloadAndInstall(
114+
'github',
115+
updateResult.latestVersion,
116+
updateResult.assets
117+
);
118+
await Promise.resolve();
119+
await Promise.resolve();
120+
121+
service.cancelDownload();
122+
const retry = service.downloadAndInstall(
123+
'github',
124+
updateResult.latestVersion,
125+
updateResult.assets
126+
);
127+
128+
expect(service.getState().isDownloading).toBe(true);
129+
expect(dependencies.download).toHaveBeenCalledTimes(1);
130+
await expect(retry).resolves.toBeUndefined();
131+
await expect(firstDownload).rejects.toMatchObject({ name: 'AbortError' });
132+
expect(service.getState().isDownloading).toBe(false);
133+
});
134+
135+
it('安装器启动失败时保留可重试的更新状态', async () => {
136+
const dependencies = createDependencies({
137+
install: jest.fn(async () => {
138+
throw new Error('installer unavailable');
139+
}),
140+
});
141+
const service = new UpdateService(dependencies);
142+
await service.checkForUpdates();
143+
144+
await expect(
145+
service.downloadAndInstall('github', updateResult.latestVersion, updateResult.assets)
146+
).rejects.toThrow('installer unavailable');
147+
expect(service.getState()).toMatchObject({
148+
updateAvailable: true,
149+
latestVersion: updateResult.latestVersion,
150+
assets: updateResult.assets,
151+
});
152+
});
86153
});

src/__tests__/appLaunch.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { shouldEnableAutoUpdateCheck } from '@/utils/appLaunch';
2+
3+
describe('shouldEnableAutoUpdateCheck', () => {
4+
it.each([
5+
'syncclipboard://quick-upload',
6+
'syncclipboard://quick-upload?fg=1',
7+
'syncclipboard://quick-download',
8+
'syncclipboard://quick-download?fg=1',
9+
'syncclipboard://expo-sharing',
10+
])('overlay 冷启动 %s 跳过更新检查', (url) => {
11+
expect(shouldEnableAutoUpdateCheck(url)).toBe(false);
12+
});
13+
14+
it.each([null, 'syncclipboard://home', 'https://example.com'])('普通启动 %s 允许检查', (url) => {
15+
expect(shouldEnableAutoUpdateCheck(url)).toBe(true);
16+
});
17+
});

src/hooks/useUpdateDialog.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useCallback } from 'react';
2-
import { Alert } from 'react-native';
2+
import { Alert, Platform } from 'react-native';
33
import { useTranslation } from 'react-i18next';
44
import { APP_VERSION } from '@/constants';
55
import { updateService } from '@/services/update';
@@ -35,6 +35,15 @@ export function useUpdateDialog(showMessage: ShowMessage): {
3535

3636
const showUpdateDialog = useCallback(
3737
(version: string, assets: ReleaseAssetInfo[], releaseNotes?: string) => {
38+
if (Platform.OS !== 'android') {
39+
Alert.alert(
40+
t('settings.updateNotSupportedTitle'),
41+
t('settings.updateNotSupportedMessage'),
42+
[{ text: t('common.confirm') }]
43+
);
44+
return;
45+
}
46+
3847
const channelName = channel === 'github' ? 'GitHub' : 'Gitee';
3948
const body = releaseNotes
4049
? t('settings.newVersionMessageWithChannel', {

src/i18n/locales/en.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,9 @@ const en: DeepString<typeof zh> = {
362362
downloadGitee: 'Download from Gitee',
363363
downloadGitHub: 'Download from GitHub',
364364
updateNow: 'Update Now',
365+
updateNotSupportedTitle: 'In-app updates unavailable',
366+
updateNotSupportedMessage:
367+
'Downloading and installing updates in the app is not supported on this platform.',
365368
noSuitableApk: 'No suitable APK found for this device',
366369
downloadCanceled: 'Download canceled',
367370
autoCheckUpdate: 'Auto-check for updates',

src/i18n/locales/zh.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,8 @@ const zh = {
351351
downloadGitee: 'Gitee 下载',
352352
downloadGitHub: 'GitHub 下载',
353353
updateNow: '立即更新',
354+
updateNotSupportedTitle: '暂不支持应用内更新',
355+
updateNotSupportedMessage: '当前平台暂不支持应用内下载和安装更新。',
354356
noSuitableApk: '找不到适合当前设备的 APK',
355357
downloadCanceled: '已取消下载',
356358
autoCheckUpdate: '自动检查更新',

src/navigation/AppNavigator.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,11 @@ const SettingsStackNavigator = () => {
7878
);
7979
};
8080

81-
export const AppNavigator = () => {
81+
interface AppNavigatorProps {
82+
autoUpdateCheckEnabled: boolean;
83+
}
84+
85+
export const AppNavigator = ({ autoUpdateCheckEnabled }: AppNavigatorProps) => {
8286
const { theme } = useTheme();
8387
const { t } = useTranslation();
8488

@@ -147,7 +151,9 @@ export const AppNavigator = () => {
147151
},
148152
})}
149153
>
150-
<Tab.Screen name="Home" component={HomeScreen} options={{ title: t('nav.home') }} />
154+
<Tab.Screen name="Home" options={{ title: t('nav.home') }}>
155+
{() => <HomeScreen autoUpdateCheckEnabled={autoUpdateCheckEnabled} />}
156+
</Tab.Screen>
151157
<Tab.Screen
152158
name="History"
153159
component={HistoryScreen}

src/screens/HomeScreen.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ import { longRunningTaskManager } from '@/longRunningTask/LongRunningTaskManager
3737
import { updateService } from '@/services/update';
3838
import { useUpdateDialog } from '@/hooks/useUpdateDialog';
3939

40-
export function HomeScreen() {
40+
interface HomeScreenProps {
41+
autoUpdateCheckEnabled: boolean;
42+
}
43+
44+
export function HomeScreen({ autoUpdateCheckEnabled }: HomeScreenProps) {
4145
const { theme } = useTheme();
4246
const { t } = useTranslation();
4347
const navigation = useNavigation();
@@ -68,7 +72,7 @@ export function HomeScreen() {
6872

6973
// 首页首次挂载后自动检查更新(默认每天一次)。
7074
useEffect(() => {
71-
if (!isLoaded) return;
75+
if (!isLoaded || !autoUpdateCheckEnabled) return;
7276
updateService
7377
.checkAutomatically()
7478
.then((result) => {
@@ -81,7 +85,7 @@ export function HomeScreen() {
8185
console.warn('[HomeScreen] Auto update check failed:', error);
8286
}
8387
});
84-
}, [isLoaded, showUpdateDialog]);
88+
}, [autoUpdateCheckEnabled, isLoaded, showUpdateDialog]);
8589

8690
// 启动所有后台任务(先加载字体,再启动后台任务,避免后台繁重任务导致导航栏图标加载缓慢)
8791
useEffect(() => {

0 commit comments

Comments
 (0)