feat(app): add version check system and enhance internationalization support

Add comprehensive app update checking functionality with:
- New VersionCheckContext for managing update detection and notifications
- VersionUpdateModal UI component for presenting update information
- Version service API integration with platform-specific update URLs
- Version check menu item in personal settings with manual/automatic checking

Enhance internationalization across workout features:
- Complete workout type translations for English and Chinese
- Localized workout detail modal with proper date/time formatting
- Locale-aware date formatting in fitness rings detail
- Workout notification improvements with deep linking to specific workout details

Improve UI/UX with better chart rendering, sizing fixes, and enhanced navigation flow. Update app version to 1.1.3 and include app version in API headers for better tracking.
This commit is contained in:
2025-11-29 20:47:16 +08:00
parent 83b77615cf
commit a309123b35
19 changed files with 1132 additions and 159 deletions

View File

@@ -0,0 +1,135 @@
import VersionUpdateModal from '@/components/VersionUpdateModal';
import { useToast } from '@/contexts/ToastContext';
import { fetchVersionInfo, getCurrentAppVersion, type VersionInfo } from '@/services/version';
import { log } from '@/utils/logger';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { Linking } from 'react-native';
import { useTranslation } from 'react-i18next';
type VersionCheckContextValue = {
isChecking: boolean;
updateInfo: VersionInfo | null;
checkForUpdate: (options?: { manual?: boolean }) => Promise<VersionInfo | null>;
openStore: () => Promise<void>;
};
const VersionCheckContext = createContext<VersionCheckContextValue | undefined>(undefined);
export function VersionCheckProvider({ children }: { children: React.ReactNode }) {
const { showSuccess, showError } = useToast();
const { t } = useTranslation();
const [isChecking, setIsChecking] = useState(false);
const [updateInfo, setUpdateInfo] = useState<VersionInfo | null>(null);
const [modalVisible, setModalVisible] = useState(false);
const hasAutoCheckedRef = useRef(false);
const currentVersion = useMemo(() => getCurrentAppVersion(), []);
const openStore = useCallback(async () => {
if (!updateInfo?.appStoreUrl) {
showError(t('personal.versionCheck.missingUrl'));
return;
}
try {
const supported = await Linking.canOpenURL(updateInfo.appStoreUrl);
if (!supported) {
throw new Error('URL not supported');
}
await Linking.openURL(updateInfo.appStoreUrl);
log.info('version-update-open-store', { url: updateInfo.appStoreUrl });
} catch (error) {
log.error('version-update-open-store-failed', error);
showError(t('personal.versionCheck.openStoreFailed'));
}
}, [showError, t, updateInfo]);
const checkForUpdate = useCallback(
async ({ manual = false }: { manual?: boolean } = {}) => {
if (isChecking) {
if (manual) {
showSuccess(t('personal.versionCheck.checking'));
}
return updateInfo;
}
setIsChecking(true);
try {
const info = await fetchVersionInfo();
setUpdateInfo(info);
setModalVisible(Boolean(info?.needsUpdate));
if (info?.needsUpdate && manual) {
showSuccess(
t('personal.versionCheck.updateFound', {
version: info.latestVersion,
})
);
} else if (!info?.needsUpdate && manual) {
showSuccess(t('personal.versionCheck.upToDate'));
}
return info;
} catch (error) {
log.error('version-check-failed', error);
if (manual) {
showError(t('personal.versionCheck.failed'));
}
return null;
} finally {
setIsChecking(false);
}
},
[isChecking, showError, showSuccess, t, updateInfo]
);
useEffect(() => {
if (hasAutoCheckedRef.current) return;
hasAutoCheckedRef.current = true;
checkForUpdate({ manual: false }).catch((error) => {
log.error('auto-version-check-failed', error);
});
}, [checkForUpdate]);
const strings = useMemo(
() => ({
title: t('personal.versionCheck.modalTitle'),
tag: t('personal.versionCheck.modalTag'),
currentVersionLabel: t('personal.versionCheck.currentVersion'),
latestVersionLabel: t('personal.versionCheck.latestVersion'),
updatesTitle: t('personal.versionCheck.releaseNotesTitle'),
fallbackNote: t('personal.versionCheck.fallbackNotes'),
remindLater: t('personal.versionCheck.later'),
updateCta: t('personal.versionCheck.updateNow'),
}),
[t]
);
return (
<VersionCheckContext.Provider
value={{
isChecking,
updateInfo,
checkForUpdate,
openStore,
}}
>
{children}
<VersionUpdateModal
visible={modalVisible && Boolean(updateInfo?.needsUpdate)}
info={updateInfo}
currentVersion={currentVersion}
onClose={() => setModalVisible(false)}
onUpdate={openStore}
strings={strings}
/>
</VersionCheckContext.Provider>
);
}
export function useVersionCheck(): VersionCheckContextValue {
const context = useContext(VersionCheckContext);
if (!context) {
throw new Error('useVersionCheck must be used within VersionCheckProvider');
}
return context;
}