I’m constructing a exercise app in React Native with Expo, and I have to implement a relaxation timer that works even when the app is within the background or the cellphone is locked. When the timer completes, a notification ought to alert the person.
The timer ought to proceed working within the background.
It should work on each iOS and Android.
When the timer ends, a notification ought to seem, even when the app isn’t lively.
I’m utilizing:
expo-notifications for dealing with notifications.
expo-task-manager and expo-background-fetch for background execution.
AsyncStorage to persist timer state.
const TIMER_BACKGROUND_TASK = 'WORKOUT_TIMER_BACKGROUND_TASK';
const STORAGE_KEY = '@workout_timer:information';
const TIMER_NOTIFICATION_ID = 'workout-timer-complete';
// Configurazione delle notifiche
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: false,
precedence: Notifications.AndroidNotificationPriority.MAX,
}),
});
// Setup canale notifiche per Android
if (Platform.OS === 'android') {
Notifications.setNotificationChannelAsync('workout-timer-complete', {
title: 'Timer Completato',
significance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 400, 200, 400],
lockscreenVisibility: Notifications.AndroidNotificationVisibility.PUBLIC,
sound: 'default',
enableVibrate: true,
});
}
// Formatta i secondi in MM:SS
operate formatTime(seconds: quantity): string {
const minutes = Math.ground(seconds / 60);
const secs = seconds % 60;
return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
// Definizione del process in background per il controllo del timer
TaskManager.defineTask(TIMER_BACKGROUND_TASK, async () => {
attempt {
const timerDataString = await AsyncStorage.getItem(STORAGE_KEY);
if (!timerDataString) {
return BackgroundFetch.BackgroundFetchResult.NoData;
}
const timerData = JSON.parse(timerDataString);
const { endTime } = timerData;
const now = Date.now();
const remainingMs = Math.max(0, endTime - now);
// Invia notifica solo se il timer è completato
if (remainingMs <= 0) {
await sendCompletionNotification();
await AsyncStorage.removeItem(STORAGE_KEY);
return BackgroundFetch.BackgroundFetchResult.NewData;
}
return BackgroundFetch.BackgroundFetchResult.NoData;
} catch (error) {
console.error('Errore nel process in background:', error);
return BackgroundFetch.BackgroundFetchResult.Failed;
}
});
// Funzione per inviare la notifica di completamento
async operate sendCompletionNotification() {
attempt {
// Recupera la traduzione dal contesto corrente
let title = "Timer completato!"; // Default
attempt {
// Prova a recuperare la traduzione salvata
const timerDataString = await AsyncStorage.getItem(STORAGE_KEY);
if (timerDataString) {
const { translatedTitle } = JSON.parse(timerDataString);
if (translatedTitle) {
title = translatedTitle;
}
}
} catch (e) {
// Usa il titolo di default se c'è un errore
}
// Configurazione della notifica
const notificationContent = {
title,
information: { sort: 'timer-complete' },
sound: true,
};
// Impostazioni specifiche per piattaforma
if (Platform.OS === 'android') {
Object.assign(notificationContent, {
channelId: 'workout-timer-complete',
precedence: 'max',
class: 'alarm', // Importante per la schermata di blocco
vibrationPattern: [0, 400, 200, 400, 200, 400],
});
} else if (Platform.OS === 'ios') {
Object.assign(notificationContent, {
interruptionLevel: 'timeSensitive', // Livello critico per iOS
sound: true,
});
}
// Rimuovi qualsiasi notifica esistente prima
await Notifications.dismissAllNotificationsAsync();
// Invia la notifica immediatamente
await Notifications.scheduleNotificationAsync({
content material: notificationContent,
set off: null, // Consegna immediata
identifier: TIMER_NOTIFICATION_ID,
});
// Vibra al completamento
const vibrationPattern = Platform.OS === 'android'
? [0, 400, 200, 400, 200, 400]
: [0, 500, 200, 500];
Vibration.vibrate(vibrationPattern);
} catch (error) {
console.error('Errore invio notifica di completamento:', error);
}
}
// Props del componente
interface WorkoutTimerProps null;
isVisible: boolean;
onComplete: () => void;
onDismiss: () => void;
exerciseName?: string;
currentSetNumber?: quantity;
totalSets?: quantity;
// Componente principale
const WorkoutTimer: React.FC = ({
length,
isVisible,
onComplete,
onDismiss,
exerciseName,
currentSetNumber,
totalSets,
}) => {
const [timeRemaining, setTimeRemaining] = useState(null);
const [appState, setAppState] = useState(AppState.currentState);
const timerRef = useRef(null);
const backgroundTimeRef = useRef(null);
const endTimeRef = useRef(null);
const scale = useSharedValue(1);
const { t } = useLanguage();
// Richiedi permessi per le notifiche
useEffect(() => {
const requestPermissions = async () => {
const { standing: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { standing } = await Notifications.requestPermissionsAsync({
ios: {
allowAlert: true,
allowBadge: true,
allowSound: true,
allowCriticalAlerts: true, // Importante per schermata di blocco
provideAppNotificationSettings: true,
},
});
finalStatus = standing;
}
if (finalStatus !== 'granted') {
console.log('Permessi per le notifiche non concessi');
}
};
requestPermissions();
// Setup del listener per le notifiche ricevute
const notificationListener = Notifications.addNotificationReceivedListener(() => {
// Quando la notifica è ricevuta, completa il timer
completeTimer();
});
// Setup del listener per le notifiche toccate
const responseListener = Notifications.addNotificationResponseReceivedListener(() => {
// Quando la notifica è toccata, completa il timer
completeTimer();
});
return () => {
notificationListener.take away();
responseListener.take away();
cleanupResources();
};
}, []);
// Inizializza il timer quando diventa visibile
useEffect(() => {
if (isVisible && length !== null) {
// Rimuovi notifiche esistenti
Notifications.dismissAllNotificationsAsync();
// Calcola il tempo di fantastic
const now = Date.now();
const endTime = now + length * 1000;
endTimeRef.present = endTime;
setTimeRemaining(length);
// Avvia il timer e registra il process in background
startTimer();
registerBackgroundTask();
}
return () => {
cleanupResources();
};
}, [isVisible, duration]);
// Gestione dei cambiamenti di stato dell'app (foreground/background)
useEffect(() => {
const subscription = AppState.addEventListener('change', nextAppState => {
if (appState === 'lively' && nextAppState.match(/inactive|background/)) {
// App va in background - salva il momento
backgroundTimeRef.present = Date.now();
} else if (appState.match(/inactive|background/) && nextAppState === 'lively') {
// App torna in foreground - sincronizza il timer
syncTimerWithStoredEndTime();
}
setAppState(nextAppState);
});
return () => {
subscription.take away();
};
}, [appState]);
// Registra il process in background
const registerBackgroundTask = async () => {
attempt {
if (length !== null && endTimeRef.present) {
// Salva i dati del timer in AsyncStorage
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify({
endTime: endTimeRef.present,
translatedTitle: t.screens.workoutDetails.timer.timerComplete
}));
// Registra il process se non è già registrato
const isRegistered = await TaskManager.isTaskRegisteredAsync(TIMER_BACKGROUND_TASK);
if (!isRegistered) {
await BackgroundFetch.registerTaskAsync(TIMER_BACKGROUND_TASK, {
minimumInterval: 1, // Controlla frequentemente
stopOnTerminate: false,
startOnBoot: true
});
}
}
} catch (error) {
console.error('Errore registrazione process in background:', error);
}
};
// Sincronizza il timer con il tempo di fantastic salvato
const syncTimerWithStoredEndTime = async () => {
attempt {
if (backgroundTimeRef.present && endTimeRef.present) {
const now = Date.now();
const newTimeRemaining = Math.max(0, Math.ground((endTimeRef.present - now) / 1000));
setTimeRemaining(newTimeRemaining);
if (newTimeRemaining <= 0) {
completeTimer();
} else {
startTimer();
}
}
} catch (error) {
console.error('Errore sincronizzazione timer:', error);
}
};
// Pulisci le risorse
const cleanupResources = async () => {
stopTimer();
attempt {
// Cancella le notifiche programmate
await Notifications.cancelScheduledNotificationAsync(TIMER_NOTIFICATION_ID);
// Non annullare la registrazione del process per permettergli di completarsi
// anche se il componente si smonta
// Rimuovi i dati salvati solo se il timer è stato esplicitamente interrotto
if (!isVisible) {
await AsyncStorage.removeItem(STORAGE_KEY);
}
} catch (error) {
console.error('Errore pulizia risorse:', error);
}
};
// Avvia il timer
const startTimer = () => {
stopTimer();
timerRef.present = setInterval(() => {
setTimeRemaining(prev => {
if (!prev || prev <= 0) {
completeTimer();
return 0;
}
const newTime = prev - 1;
// Anima il timer quando si avvicina al completamento
if (newTime <= 5 && newTime > 0) {
// Anima il testo
scale.worth = withSequence(
withTiming(1.2, { length: 300 }),
withTiming(1, { length: 300 })
);
// Vibrazione leggera per gli ultimi 5 secondi, solo in foreground
if (appState === 'lively') {
Vibration.vibrate(40);
}
}
return newTime;
});
}, 1000);
};
// Ferma il timer
const stopTimer = () => {
if (timerRef.present) {
clearInterval(timerRef.present);
timerRef.present = null;
}
};
// Completa il timer
const completeTimer = async () => {
stopTimer();
setTimeRemaining(0);
// Invia la notifica di completamento solo se NON siamo in foreground
if (appState !== 'lively') {
await sendCompletionNotification();
} else {
// Se l'app è in foreground, fornisci suggestions aptico
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
// Vibra il dispositivo al completamento per enfasi
const vibrationPattern = Platform.OS === 'android'
? [0, 400, 200, 400, 200, 400]
: [0, 500, 200, 500];
Vibration.vibrate(vibrationPattern);
}
// Pulisci le risorse
await cleanupResources();
// Notifica il componente mum or dad del completamento
onComplete();
};
// Stile animato per il testo del timer
const animatedStyle = useAnimatedStyle(() => ({
rework: [{ scale: scale.value }],
}));
// Gestione dello skip o chiusura timer
const handleSkipOrDismiss = () => {
stopTimer();
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
cleanupResources();
onDismiss();
};
if (!isVisible) return null;
return (
);
};
export default WorkoutTimer;