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.
344 lines
8.4 KiB
TypeScript
344 lines
8.4 KiB
TypeScript
import type { VersionInfo } from '@/services/version';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { LinearGradient } from 'expo-linear-gradient';
|
|
import React, { useMemo } from 'react';
|
|
import {
|
|
Modal,
|
|
Pressable,
|
|
StyleSheet,
|
|
Text,
|
|
TouchableOpacity,
|
|
View,
|
|
} from 'react-native';
|
|
|
|
type VersionUpdateModalProps = {
|
|
visible: boolean;
|
|
info: VersionInfo | null;
|
|
currentVersion: string;
|
|
onClose: () => void;
|
|
onUpdate: () => void;
|
|
strings: {
|
|
title: string;
|
|
tag: string;
|
|
currentVersionLabel: string;
|
|
latestVersionLabel: string;
|
|
updatesTitle: string;
|
|
fallbackNote: string;
|
|
remindLater: string;
|
|
updateCta: string;
|
|
};
|
|
};
|
|
|
|
export function VersionUpdateModal({
|
|
visible,
|
|
info,
|
|
currentVersion,
|
|
onClose,
|
|
onUpdate,
|
|
strings,
|
|
}: VersionUpdateModalProps) {
|
|
const notes = useMemo(() => {
|
|
if (!info) return [];
|
|
|
|
if (info.releaseNotes && info.releaseNotes.trim().length > 0) {
|
|
return info.releaseNotes
|
|
.split(/\r?\n+/)
|
|
.map((line) => line.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
if (info.updateMessage && info.updateMessage.trim().length > 0) {
|
|
return [info.updateMessage.trim()];
|
|
}
|
|
|
|
return [];
|
|
}, [info]);
|
|
|
|
if (!info) return null;
|
|
|
|
return (
|
|
<Modal
|
|
animationType="fade"
|
|
transparent
|
|
visible={visible}
|
|
onRequestClose={onClose}
|
|
>
|
|
<View style={styles.overlay}>
|
|
<Pressable style={StyleSheet.absoluteFill} onPress={onClose} />
|
|
<View style={styles.cardShadow}>
|
|
<LinearGradient
|
|
colors={['#0F1B61', '#0F274A', '#0A1A3A']}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 1 }}
|
|
style={styles.card}
|
|
>
|
|
<LinearGradient
|
|
colors={['rgba(255,255,255,0.18)', 'rgba(255,255,255,0.03)']}
|
|
style={styles.glowOrb}
|
|
/>
|
|
<LinearGradient
|
|
colors={['rgba(255,255,255,0.08)', 'transparent']}
|
|
style={styles.ribbon}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 1 }}
|
|
/>
|
|
<View style={styles.headerRow}>
|
|
<View style={styles.tag}>
|
|
<Ionicons name="sparkles" size={14} color="#0F1B61" />
|
|
<Text style={styles.tagText}>{strings.tag}</Text>
|
|
</View>
|
|
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
|
|
<Ionicons name="close" size={18} color="#E5E7EB" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
<View style={styles.titleBlock}>
|
|
<Text style={styles.title}>{strings.title}</Text>
|
|
<Text style={styles.subtitle}>
|
|
{info.latestVersion ? `v${info.latestVersion}` : ''}
|
|
</Text>
|
|
</View>
|
|
|
|
<View style={styles.metaRow}>
|
|
<View style={styles.metaChip}>
|
|
<Ionicons name="time-outline" size={14} color="#C7D2FE" />
|
|
<Text style={styles.metaText}>
|
|
{strings.currentVersionLabel} v{currentVersion}
|
|
</Text>
|
|
</View>
|
|
<View style={styles.metaChip}>
|
|
<Ionicons name="arrow-up-circle-outline" size={14} color="#C7D2FE" />
|
|
<Text style={styles.metaText}>
|
|
{strings.latestVersionLabel} v{info.latestVersion}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.noteCard}>
|
|
<Text style={styles.noteTitle}>{strings.updatesTitle}</Text>
|
|
{notes.length > 0 ? (
|
|
notes.map((line, idx) => (
|
|
<View key={`${idx}-${line}`} style={styles.noteItem}>
|
|
<View style={styles.bullet}>
|
|
<Ionicons name="ellipse" size={6} color="#6EE7B7" />
|
|
</View>
|
|
<Text style={styles.noteText}>{line}</Text>
|
|
</View>
|
|
))
|
|
) : (
|
|
<Text style={styles.noteText}>{strings.fallbackNote}</Text>
|
|
)}
|
|
</View>
|
|
|
|
<View style={styles.actions}>
|
|
<TouchableOpacity
|
|
activeOpacity={0.85}
|
|
onPress={onClose}
|
|
style={styles.secondaryButton}
|
|
>
|
|
<Text style={styles.secondaryText}>{strings.remindLater}</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
activeOpacity={0.9}
|
|
onPress={onUpdate}
|
|
style={styles.primaryButtonShadow}
|
|
>
|
|
<LinearGradient
|
|
colors={['#6EE7B7', '#3B82F6']}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 1 }}
|
|
style={styles.primaryButton}
|
|
>
|
|
<Ionicons name="cloud-download-outline" size={18} color="#0B1236" />
|
|
<Text style={styles.primaryText}>{strings.updateCta}</Text>
|
|
</LinearGradient>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</LinearGradient>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
overlay: {
|
|
flex: 1,
|
|
backgroundColor: 'rgba(7, 11, 34, 0.65)',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
padding: 20,
|
|
},
|
|
cardShadow: {
|
|
width: '100%',
|
|
maxWidth: 420,
|
|
shadowColor: '#0B1236',
|
|
shadowOpacity: 0.35,
|
|
shadowOffset: { width: 0, height: 16 },
|
|
shadowRadius: 30,
|
|
elevation: 8,
|
|
},
|
|
card: {
|
|
borderRadius: 24,
|
|
padding: 20,
|
|
overflow: 'hidden',
|
|
borderWidth: 1,
|
|
borderColor: 'rgba(255,255,255,0.08)',
|
|
},
|
|
glowOrb: {
|
|
position: 'absolute',
|
|
width: 220,
|
|
height: 220,
|
|
borderRadius: 110,
|
|
right: -60,
|
|
top: -80,
|
|
opacity: 0.8,
|
|
},
|
|
ribbon: {
|
|
position: 'absolute',
|
|
left: -120,
|
|
bottom: -120,
|
|
width: 260,
|
|
height: 260,
|
|
transform: [{ rotate: '-8deg' }],
|
|
opacity: 0.6,
|
|
},
|
|
headerRow: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
},
|
|
tag: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingHorizontal: 10,
|
|
paddingVertical: 6,
|
|
borderRadius: 14,
|
|
backgroundColor: '#A5B4FC',
|
|
},
|
|
tagText: {
|
|
color: '#0F1B61',
|
|
fontWeight: '700',
|
|
marginLeft: 6,
|
|
fontSize: 12,
|
|
letterSpacing: 0.3,
|
|
},
|
|
closeButton: {
|
|
width: 32,
|
|
height: 32,
|
|
borderRadius: 16,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: 'rgba(255,255,255,0.06)',
|
|
},
|
|
titleBlock: {
|
|
marginTop: 14,
|
|
marginBottom: 8,
|
|
},
|
|
title: {
|
|
fontSize: 24,
|
|
fontWeight: '800',
|
|
color: '#F9FAFB',
|
|
letterSpacing: 0.2,
|
|
},
|
|
subtitle: {
|
|
color: '#C7D2FE',
|
|
marginTop: 6,
|
|
fontSize: 15,
|
|
},
|
|
metaRow: {
|
|
flexDirection: 'row',
|
|
marginTop: 10,
|
|
gap: 8,
|
|
flexWrap: 'wrap',
|
|
},
|
|
metaChip: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: 'rgba(255,255,255,0.08)',
|
|
borderRadius: 12,
|
|
paddingHorizontal: 10,
|
|
paddingVertical: 8,
|
|
},
|
|
metaText: {
|
|
color: '#E5E7EB',
|
|
marginLeft: 6,
|
|
fontSize: 12,
|
|
},
|
|
noteCard: {
|
|
marginTop: 16,
|
|
borderRadius: 16,
|
|
padding: 14,
|
|
backgroundColor: 'rgba(255,255,255,0.06)',
|
|
borderWidth: 1,
|
|
borderColor: 'rgba(255,255,255,0.04)',
|
|
},
|
|
noteTitle: {
|
|
color: '#F9FAFB',
|
|
fontWeight: '700',
|
|
fontSize: 15,
|
|
marginBottom: 8,
|
|
},
|
|
noteItem: {
|
|
flexDirection: 'row',
|
|
alignItems: 'flex-start',
|
|
marginTop: 8,
|
|
},
|
|
bullet: {
|
|
width: 18,
|
|
alignItems: 'center',
|
|
marginTop: 6,
|
|
},
|
|
noteText: {
|
|
flex: 1,
|
|
color: '#E5E7EB',
|
|
fontSize: 14,
|
|
lineHeight: 20,
|
|
},
|
|
actions: {
|
|
marginTop: 18,
|
|
flexDirection: 'row',
|
|
gap: 10,
|
|
},
|
|
secondaryButton: {
|
|
flex: 1,
|
|
height: 48,
|
|
borderRadius: 14,
|
|
borderWidth: 1,
|
|
borderColor: 'rgba(255,255,255,0.16)',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: 'rgba(255,255,255,0.08)',
|
|
},
|
|
secondaryText: {
|
|
color: '#E5E7EB',
|
|
fontWeight: '600',
|
|
fontSize: 14,
|
|
},
|
|
primaryButtonShadow: {
|
|
flex: 1,
|
|
height: 48,
|
|
borderRadius: 14,
|
|
overflow: 'hidden',
|
|
shadowColor: '#1E40AF',
|
|
shadowOpacity: 0.4,
|
|
shadowOffset: { width: 0, height: 12 },
|
|
shadowRadius: 14,
|
|
elevation: 6,
|
|
},
|
|
primaryButton: {
|
|
flex: 1,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: 8,
|
|
},
|
|
primaryText: {
|
|
color: '#0B1236',
|
|
fontWeight: '800',
|
|
fontSize: 15,
|
|
},
|
|
});
|
|
|
|
export default VersionUpdateModal;
|