feat(fasting): add auto-renewal and reset functionality for fasting plans

- Implement auto-renewal logic for completed fasting cycles using dayjs
- Add reset button with information modal in FastingOverviewCard
- Configure iOS push notifications for production environment
- Add expo-media-library and react-native-view-shot dependencies
- Update FastingScheduleOrigin type to include 'auto' origin
This commit is contained in:
richarjiang
2025-10-15 19:06:18 +08:00
parent 039138f7e4
commit d39a32c0d8
9 changed files with 548 additions and 155 deletions

View File

@@ -2,9 +2,13 @@ import { CircularRing } from '@/components/CircularRing';
import { Colors } from '@/constants/Colors';
import type { FastingPlan } from '@/constants/Fasting';
import { useColorScheme } from '@/hooks/useColorScheme';
import { Ionicons } from '@expo/vector-icons';
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
import { LinearGradient } from 'expo-linear-gradient';
import React from 'react';
import React, { useState } from 'react';
import {
Modal,
Pressable,
StyleSheet,
Text,
TouchableOpacity,
@@ -22,6 +26,7 @@ type FastingOverviewCardProps = {
endTimeLabel: string;
onAdjustStartPress: () => void;
onViewMealsPress: () => void;
onResetPress: () => void;
progress: number;
};
@@ -36,93 +41,149 @@ export function FastingOverviewCard({
endTimeLabel,
onAdjustStartPress,
onViewMealsPress,
onResetPress,
progress,
}: FastingOverviewCardProps) {
const theme = useColorScheme() ?? 'light';
const colors = Colors[theme];
const themeColors = plan?.theme;
const [showResetInfo, setShowResetInfo] = useState(false);
const isGlassAvailable = isLiquidGlassAvailable();
return (
<LinearGradient
colors={[
themeColors?.accentSecondary ?? colors.heroSurfaceTint,
themeColors?.backdrop ?? colors.pageBackgroundEmphasis,
]}
style={styles.container}
>
<View style={styles.headerRow}>
<View>
<Text style={styles.planLabel}></Text>
{plan?.id && (
<View style={styles.planTag}>
<Text style={[styles.planTagText, { color: themeColors?.accent ?? colors.primary }]}>
{plan.id}
<>
<LinearGradient
colors={[
themeColors?.accentSecondary ?? colors.heroSurfaceTint,
themeColors?.backdrop ?? colors.pageBackgroundEmphasis,
]}
style={styles.container}
>
<View style={styles.headerRow}>
<View>
<Text style={styles.planLabel}></Text>
{plan?.id && (
<View style={styles.planTag}>
<Text style={[styles.planTagText, { color: themeColors?.accent ?? colors.primary }]}>
{plan.id}
</Text>
</View>
)}
</View>
{plan?.badge && (
<View style={[styles.badge, { backgroundColor: `${themeColors?.accent ?? colors.primary}20` }]}>
<Text style={[styles.badgeText, { color: themeColors?.accent ?? colors.primary }]}>
{plan.badge}
</Text>
</View>
)}
</View>
{plan?.badge && (
<View style={[styles.badge, { backgroundColor: `${themeColors?.accent ?? colors.primary}20` }]}>
<Text style={[styles.badgeText, { color: themeColors?.accent ?? colors.primary }]}>
{plan.badge}
<View style={styles.scheduleRow}>
<View style={styles.scheduleCell}>
<Text style={styles.scheduleLabel}></Text>
<Text style={styles.scheduleDay}>{startDayLabel}</Text>
<Text style={styles.scheduleTime}>{startTimeLabel}</Text>
</View>
<View style={styles.separator} />
<View style={styles.scheduleCell}>
<Text style={styles.scheduleLabel}></Text>
<Text style={styles.scheduleDay}>{endDayLabel}</Text>
<Text style={styles.scheduleTime}>{endTimeLabel}</Text>
</View>
</View>
<View style={styles.statusRow}>
<View style={styles.ringContainer}>
<CircularRing
size={168}
strokeWidth={14}
progress={progress}
progressColor={themeColors?.ringProgress ?? colors.primary}
trackColor={themeColors?.ringTrack ?? 'rgba(0,0,0,0.05)'}
showCenterText={false}
startAngleDeg={-90}
resetToken={phaseLabel}
/>
<View style={styles.ringContent}>
<Text style={styles.phaseText}>{phaseLabel}</Text>
<Text style={styles.countdownLabel}>{countdownLabel}</Text>
<Text style={styles.countdownValue}>{countdownValue}</Text>
</View>
</View>
</View>
<View style={styles.actionsRow}>
<TouchableOpacity
style={[styles.secondaryButton, { borderColor: themeColors?.accent ?? colors.primary }]}
onPress={onAdjustStartPress}
activeOpacity={0.85}
>
<Text style={[styles.secondaryButtonText, { color: themeColors?.accent ?? colors.primary }]}>
</Text>
</View>
)}
</View>
</TouchableOpacity>
<View style={styles.scheduleRow}>
<View style={styles.scheduleCell}>
<Text style={styles.scheduleLabel}></Text>
<Text style={styles.scheduleDay}>{startDayLabel}</Text>
<Text style={styles.scheduleTime}>{startTimeLabel}</Text>
</View>
<View style={styles.separator} />
<View style={styles.scheduleCell}>
<Text style={styles.scheduleLabel}></Text>
<Text style={styles.scheduleDay}>{endDayLabel}</Text>
<Text style={styles.scheduleTime}>{endTimeLabel}</Text>
</View>
</View>
<TouchableOpacity
style={[styles.secondaryButton, { borderColor: themeColors?.accent ?? colors.primary }]}
onPress={onResetPress}
activeOpacity={0.85}
>
<Text style={[styles.secondaryButtonText, { color: themeColors?.accent ?? colors.primary }]}>
</Text>
</TouchableOpacity>
<View style={styles.statusRow}>
<View style={styles.ringContainer}>
<CircularRing
size={168}
strokeWidth={14}
progress={progress}
progressColor={themeColors?.ringProgress ?? colors.primary}
trackColor={themeColors?.ringTrack ?? 'rgba(0,0,0,0.05)'}
showCenterText={false}
startAngleDeg={-90}
resetToken={phaseLabel}
/>
<View style={styles.ringContent}>
<Text style={styles.phaseText}>{phaseLabel}</Text>
<Text style={styles.countdownLabel}>{countdownLabel}</Text>
<Text style={styles.countdownValue}>{countdownValue}</Text>
</View>
{isGlassAvailable ? (
<GlassView
style={styles.infoButton}
glassEffectStyle="regular"
isInteractive={true}
>
<TouchableOpacity onPress={() => setShowResetInfo(true)} style={styles.infoButtonInner}>
<Ionicons name="information-circle-outline" size={20} color={themeColors?.accent ?? colors.primary} />
</TouchableOpacity>
</GlassView>
) : (
<TouchableOpacity
onPress={() => setShowResetInfo(true)}
style={[styles.infoButton, styles.fallbackInfoButton]}
>
<Ionicons name="information-circle-outline" size={20} color={themeColors?.accent ?? colors.primary} />
</TouchableOpacity>
)}
</View>
</View>
</LinearGradient>
<View style={styles.actionsRow}>
<TouchableOpacity
style={[styles.secondaryButton, { borderColor: themeColors?.accent ?? colors.primary }]}
onPress={onAdjustStartPress}
activeOpacity={0.85}
<Modal
transparent
visible={showResetInfo}
animationType="fade"
onRequestClose={() => setShowResetInfo(false)}
>
<Pressable
style={styles.infoModalOverlay}
onPress={() => setShowResetInfo(false)}
>
<Text style={[styles.secondaryButtonText, { color: themeColors?.accent ?? colors.primary }]}>
</Text>
</TouchableOpacity>
{/* <TouchableOpacity
style={[styles.primaryButton, { backgroundColor: themeColors?.accent ?? colors.primary }]}
onPress={onViewMealsPress}
activeOpacity={0.9}
>
<Text style={styles.primaryButtonText}>查看食谱</Text>
</TouchableOpacity> */}
</View>
</LinearGradient>
<Pressable style={styles.infoModalContent} onPress={() => { }}>
<View style={styles.infoModalHandle} />
<Text style={styles.infoModalTitle}></Text>
<Text style={styles.infoModalText}>
</Text>
<Text style={styles.infoModalText}>
</Text>
<TouchableOpacity
style={[styles.infoModalButton, { backgroundColor: themeColors?.accent ?? colors.primary }]}
onPress={() => setShowResetInfo(false)}
>
<Text style={styles.infoModalButtonText}></Text>
</TouchableOpacity>
</Pressable>
</Pressable>
</Modal>
</>
);
}
@@ -258,6 +319,70 @@ const styles = StyleSheet.create({
fontSize: 15,
fontWeight: '600',
},
infoButton: {
width: 36,
height: 36,
borderRadius: 18,
alignItems: 'center',
justifyContent: 'center',
marginLeft: 8,
},
infoButtonInner: {
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center',
},
fallbackInfoButton: {
backgroundColor: 'rgba(255,255,255,0.3)',
},
infoModalOverlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'flex-end',
},
infoModalContent: {
backgroundColor: 'white',
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
paddingVertical: 24,
paddingHorizontal: 20,
paddingBottom: 40,
},
infoModalHandle: {
width: 36,
height: 4,
backgroundColor: '#E5E7EB',
borderRadius: 2,
alignSelf: 'center',
marginBottom: 16,
},
infoModalTitle: {
fontSize: 18,
fontWeight: '700',
color: '#2E3142',
marginBottom: 16,
textAlign: 'center',
},
infoModalText: {
fontSize: 15,
color: '#4A5460',
lineHeight: 22,
marginBottom: 12,
},
infoModalButton: {
paddingVertical: 14,
paddingHorizontal: 24,
borderRadius: 24,
alignItems: 'center',
justifyContent: 'center',
marginTop: 8,
},
infoModalButtonText: {
fontSize: 15,
fontWeight: '600',
color: '#FFFFFF',
},
primaryButton: {
flex: 1,
borderRadius: 24,