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.
136 lines
4.3 KiB
TypeScript
136 lines
4.3 KiB
TypeScript
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;
|
|
}
|