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:
135
contexts/VersionCheckContext.tsx
Normal file
135
contexts/VersionCheckContext.tsx
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user