feat: add store to save and share state
parent
14f1c50215
commit
2e44837381
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { Text } from '@/components/shared/Themed';
|
||||
import Card from '@/components/shared/Card';
|
||||
|
|
@ -12,6 +12,8 @@ import { TimerPickerModal } from 'react-native-timer-picker';
|
|||
import Button from '@/components/shared/Button';
|
||||
import NumberSelector from '@/components/shared/NumberSelector';
|
||||
import { i18n } from '@/app/i18n/i18n';
|
||||
import { useTimerContext } from '@/app/store/TimerContext';
|
||||
import { ActivityIndicator } from 'react-native';
|
||||
|
||||
type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'Timer'>;
|
||||
|
||||
|
|
@ -19,14 +21,41 @@ const t = i18n.scoped('dashboard');
|
|||
|
||||
export default function Dashboard() {
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const { timerState, isLoading, updateReps, updateWorkTime, updateRestTime } = useTimerContext();
|
||||
|
||||
const [reps, setReps] = useState<number>(1);
|
||||
const [workTime, setWorkTime] = useState<number>(0);
|
||||
const [restTime, setRestTime] = useState<number>(0);
|
||||
const [showWorkTimePicker, setShowWorkTimePicker] = useState<boolean>(false);
|
||||
const [showRestTimePicker, setShowRestTimePicker] = useState<boolean>(false);
|
||||
|
||||
const totalWorkTime: string = formatTime((workTime + restTime) * reps);
|
||||
// Local variables for UI changes
|
||||
const [localReps, setLocalReps] = useState<number>(timerState.reps);
|
||||
const [localWorkTime, setLocalWorkTime] = useState<number>(timerState.workTime);
|
||||
const [localRestTime, setLocalRestTime] = useState<number>(timerState.restTime);
|
||||
|
||||
// Update local states when store data is loaded
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
setLocalReps(timerState.reps);
|
||||
setLocalWorkTime(timerState.workTime);
|
||||
setLocalRestTime(timerState.restTime);
|
||||
}
|
||||
}, [isLoading, timerState]);
|
||||
|
||||
// Handle repetition changes
|
||||
const handleRepsChange = (value: number) => {
|
||||
setLocalReps(value);
|
||||
updateReps(value);
|
||||
};
|
||||
|
||||
const totalWorkTime: string = formatTime((localWorkTime + localRestTime) * localReps);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<LoadingContainer>
|
||||
<ActivityIndicator size="large" />
|
||||
<Text>Chargement...</Text>
|
||||
</LoadingContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
|
|
@ -38,14 +67,14 @@ export default function Dashboard() {
|
|||
<Card backgroundColor="black">
|
||||
<CardTextContainer>
|
||||
<CustomText>{t('repetition')}</CustomText>
|
||||
<NumberSelector reps={reps} setReps={setReps} />
|
||||
<NumberSelector reps={localReps} setReps={handleRepsChange} />
|
||||
</CardTextContainer>
|
||||
</Card>
|
||||
|
||||
<Card backgroundColor="red" onPress={() => setShowWorkTimePicker(true)}>
|
||||
<CardTextContainer>
|
||||
<CustomText>{t('fight')}</CustomText>
|
||||
<CustomText>{formatTime(workTime)}</CustomText>
|
||||
<CustomText>{formatTime(localWorkTime)}</CustomText>
|
||||
</CardTextContainer>
|
||||
</Card>
|
||||
|
||||
|
|
@ -54,7 +83,9 @@ export default function Dashboard() {
|
|||
visible={showWorkTimePicker}
|
||||
setIsVisible={setShowWorkTimePicker}
|
||||
onConfirm={(pickedDuration: any) => {
|
||||
setWorkTime(pickedDuration.minutes * 60 + pickedDuration.seconds);
|
||||
const newWorkTime = pickedDuration.minutes * 60 + pickedDuration.seconds;
|
||||
setLocalWorkTime(newWorkTime);
|
||||
updateWorkTime(newWorkTime);
|
||||
setShowWorkTimePicker(false);
|
||||
}}
|
||||
modalTitle={t('setWorkTime')}
|
||||
|
|
@ -63,6 +94,7 @@ export default function Dashboard() {
|
|||
styles={{
|
||||
theme: "dark",
|
||||
}}
|
||||
initialValue={{ minutes: Math.floor(timerState.workTime / 60), seconds: timerState.workTime % 60 }}
|
||||
modalProps={{
|
||||
overlayOpacity: 0.2,
|
||||
}}
|
||||
|
|
@ -71,7 +103,7 @@ export default function Dashboard() {
|
|||
<Card backgroundColor="green" onPress={() => setShowRestTimePicker(true)}>
|
||||
<CardTextContainer>
|
||||
<CustomText>{t('rest')}</CustomText>
|
||||
<CustomText>{formatTime(restTime)}</CustomText>
|
||||
<CustomText>{formatTime(localRestTime)}</CustomText>
|
||||
</CardTextContainer>
|
||||
</Card>
|
||||
|
||||
|
|
@ -80,12 +112,15 @@ export default function Dashboard() {
|
|||
visible={showRestTimePicker}
|
||||
setIsVisible={setShowRestTimePicker}
|
||||
onConfirm={(pickedDuration: any) => {
|
||||
setRestTime(pickedDuration.minutes * 60 + pickedDuration.seconds);
|
||||
const newRestTime = pickedDuration.minutes * 60 + pickedDuration.seconds;
|
||||
setLocalRestTime(newRestTime);
|
||||
updateRestTime(newRestTime);
|
||||
setShowRestTimePicker(false);
|
||||
}}
|
||||
modalTitle={t('setRestTime')}
|
||||
onCancel={() => setShowRestTimePicker(false)}
|
||||
closeOnOverlayPress
|
||||
initialValue={{ minutes: Math.floor(timerState.restTime / 60), seconds: timerState.restTime % 60 }}
|
||||
styles={{
|
||||
theme: "dark",
|
||||
}}
|
||||
|
|
@ -100,7 +135,7 @@ export default function Dashboard() {
|
|||
|
||||
<VerticalSpacer heightUnits={5} />
|
||||
|
||||
<Button label={t('begin')} onPress={() => navigation.navigate('Timer', { reps, restTime, workTime})} />
|
||||
<Button label={t('begin')} onPress={() => navigation.navigate('Timer')} />
|
||||
|
||||
<VerticalSpacer heightUnits={5} />
|
||||
|
||||
|
|
@ -109,6 +144,13 @@ export default function Dashboard() {
|
|||
);
|
||||
}
|
||||
|
||||
const LoadingContainer = styled.View({
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 20
|
||||
});
|
||||
|
||||
const Title = styled(Text)(({ theme }) => ({
|
||||
textAlign: 'center',
|
||||
fontSize: 40,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
export type RootStackParamList = {
|
||||
Dashboard: undefined;
|
||||
Timer: { reps: number, restTime: number, workTime: number};
|
||||
Timer: undefined;
|
||||
Settings: undefined;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ const Title = styled(Text)(({ theme }) => ({
|
|||
textAlign: 'center',
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
color: theme.colors.black
|
||||
color: theme.colors.fixed.black
|
||||
}));
|
||||
|
||||
const Row = styled.View({
|
||||
|
|
@ -93,4 +93,3 @@ const Row = styled.View({
|
|||
const Container = styled.View({
|
||||
padding: 20
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { loadUserSettings } from "@/components/shared/business/AsyncStorage";
|
|||
import BackgroundTimer from "react-native-background-timer";
|
||||
import { useAudio } from "@/components/useCases/timer/business/useAudio";
|
||||
import { useNotification } from "@/components/useCases/timer/business/useNotifications";
|
||||
import { useTimerContext } from "@/app/store/TimerContext";
|
||||
|
||||
interface TimerProps {
|
||||
reps: number;
|
||||
|
|
@ -21,8 +22,8 @@ interface TimerProps {
|
|||
export default function Timer() {
|
||||
const navigation = useNavigation();
|
||||
const route = useRoute();
|
||||
const { timerState } = useTimerContext();
|
||||
|
||||
const { reps, restTime, workTime } = route.params as TimerProps;
|
||||
const [currentRep, setCurrentRep] = useState<number>(0);
|
||||
const [timeLeft, setTimeLeft] = useState<number>(0);
|
||||
const [isWorkPhase, setIsWorkPhase] = useState<boolean>(true);
|
||||
|
|
@ -51,7 +52,7 @@ export default function Timer() {
|
|||
handleStart();
|
||||
await activateKeepAwakeAsync();
|
||||
} catch (error) {
|
||||
throw new Error("Erreur lors du chargement des paramètres utilisateur");
|
||||
throw new Error("Error loading user settings");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -93,7 +94,7 @@ export default function Timer() {
|
|||
const handleStart = () => {
|
||||
setCurrentRep(1);
|
||||
setIsWorkPhase(true);
|
||||
setTimeLeft(workTime);
|
||||
setTimeLeft(timerState.workTime);
|
||||
setIsRunning(true);
|
||||
setIsFinish(false);
|
||||
updateNotification(
|
||||
|
|
@ -103,24 +104,24 @@ export default function Timer() {
|
|||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setCurrentRep(1);
|
||||
setCurrentRep(0);
|
||||
setIsWorkPhase(true);
|
||||
setTimeLeft(workTime);
|
||||
setTimeLeft(timerState.workTime);
|
||||
setIsRunning(false);
|
||||
setIsFinish(false);
|
||||
cancelNotification();
|
||||
};
|
||||
|
||||
const nextRep = () => {
|
||||
if (currentRep < reps) {
|
||||
if (currentRep < timerState.reps) {
|
||||
if (isWorkPhase) {
|
||||
playSound();
|
||||
setIsWorkPhase(false);
|
||||
setTimeLeft(restTime);
|
||||
setTimeLeft(timerState.restTime);
|
||||
} else {
|
||||
playSound();
|
||||
setIsWorkPhase(true);
|
||||
setTimeLeft(workTime);
|
||||
setTimeLeft(timerState.workTime);
|
||||
setCurrentRep((prevRep) => prevRep + 1);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -135,12 +136,12 @@ export default function Timer() {
|
|||
if (isWorkPhase) {
|
||||
if (currentRep > 1) {
|
||||
setIsWorkPhase(false);
|
||||
setTimeLeft(restTime);
|
||||
setTimeLeft(timerState.restTime);
|
||||
setCurrentRep((prevRep) => prevRep - 1);
|
||||
}
|
||||
} else {
|
||||
setIsWorkPhase(true);
|
||||
setTimeLeft(workTime);
|
||||
setTimeLeft(timerState.workTime);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -167,7 +168,7 @@ export default function Timer() {
|
|||
<TimerContent
|
||||
isWorkPhase={isWorkPhase}
|
||||
timeLeft={timeLeft}
|
||||
reps={reps}
|
||||
reps={timerState.reps}
|
||||
bgColor={renderBgColor()}
|
||||
currentRep={currentRep}
|
||||
isRunning={isRunning}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { useEffect } from 'react';
|
|||
import 'react-native-reanimated';
|
||||
import { i18n } from './i18n/i18n';
|
||||
import { LanguageProvider } from '@/app/shared/providers/LanguageProvider';
|
||||
import { TimerProvider } from '@/app/store/TimerContext';
|
||||
|
||||
export {
|
||||
// Catch any errors thrown by the Layout component.
|
||||
|
|
@ -52,11 +53,13 @@ function RootLayoutNav() {
|
|||
return (
|
||||
<LanguageProvider>
|
||||
<ThemeProvider theme={dynamicTheme}>
|
||||
<TimerProvider>
|
||||
<Stack>
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="Timer" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="Settings" options={{ headerShown: true, title: i18n.t('settings.title') }} />
|
||||
</Stack>
|
||||
</TimerProvider>
|
||||
</ThemeProvider>
|
||||
</LanguageProvider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ export const theme = {
|
|||
};
|
||||
|
||||
export const useThemeColors = () => {
|
||||
const colorScheme = useColorScheme() ?? 'light';
|
||||
return {
|
||||
...theme,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { timerStore, TimerState } from './TimerStore';
|
||||
|
||||
// Context interface
|
||||
interface TimerContextType {
|
||||
timerState: TimerState;
|
||||
isLoading: boolean;
|
||||
updateReps: (reps: number) => Promise<void>;
|
||||
updateWorkTime: (workTime: number) => Promise<void>;
|
||||
updateRestTime: (restTime: number) => Promise<void>;
|
||||
updateState: (state: Partial<TimerState>) => Promise<void>;
|
||||
}
|
||||
|
||||
// Default context values
|
||||
const defaultContextValue: TimerContextType = {
|
||||
timerState: {
|
||||
reps: 1,
|
||||
workTime: 0,
|
||||
restTime: 0
|
||||
},
|
||||
isLoading: true,
|
||||
updateReps: async () => {},
|
||||
updateWorkTime: async () => {},
|
||||
updateRestTime: async () => {},
|
||||
updateState: async () => {}
|
||||
};
|
||||
|
||||
// Context creation
|
||||
const TimerContext = createContext<TimerContextType>(defaultContextValue);
|
||||
|
||||
// Custom hook to use the context
|
||||
export const useTimerContext = () => useContext(TimerContext);
|
||||
|
||||
// Provider props
|
||||
interface TimerProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
// Provider component
|
||||
export const TimerProvider: React.FC<TimerProviderProps> = ({ children }) => {
|
||||
const [timerState, setTimerState] = useState<TimerState>({
|
||||
reps: 1,
|
||||
workTime: 0,
|
||||
restTime: 0
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Load data on startup
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const state = await timerStore.getState();
|
||||
setTimerState(state);
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
// Update repetitions
|
||||
const updateReps = async (reps: number) => {
|
||||
try {
|
||||
await timerStore.saveReps(reps);
|
||||
setTimerState(prev => ({ ...prev, reps }));
|
||||
} catch (error) {
|
||||
console.error('Error updating repetitions:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Update work time
|
||||
const updateWorkTime = async (workTime: number) => {
|
||||
try {
|
||||
await timerStore.saveWorkTime(workTime);
|
||||
setTimerState(prev => ({ ...prev, workTime }));
|
||||
} catch (error) {
|
||||
console.error('Error updating work time:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Update rest time
|
||||
const updateRestTime = async (restTime: number) => {
|
||||
try {
|
||||
await timerStore.saveRestTime(restTime);
|
||||
setTimerState(prev => ({ ...prev, restTime }));
|
||||
} catch (error) {
|
||||
console.error('Error updating rest time:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Update multiple values at once
|
||||
const updateState = async (state: Partial<TimerState>) => {
|
||||
try {
|
||||
const newState = { ...timerState, ...state };
|
||||
await timerStore.saveState(newState);
|
||||
setTimerState(newState);
|
||||
} catch (error) {
|
||||
console.error('Error updating state:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const contextValue: TimerContextType = {
|
||||
timerState,
|
||||
isLoading,
|
||||
updateReps,
|
||||
updateWorkTime,
|
||||
updateRestTime,
|
||||
updateState
|
||||
};
|
||||
|
||||
return (
|
||||
<TimerContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</TimerContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
// Storage keys definition
|
||||
const TIMER_KEYS = {
|
||||
REPS: 'timer_reps',
|
||||
WORK_TIME: 'timer_work_time',
|
||||
REST_TIME: 'timer_rest_time'
|
||||
};
|
||||
|
||||
// Store interface
|
||||
export interface TimerState {
|
||||
reps: number;
|
||||
workTime: number;
|
||||
restTime: number;
|
||||
}
|
||||
|
||||
// Default values
|
||||
const DEFAULT_STATE: TimerState = {
|
||||
reps: 1,
|
||||
workTime: 0,
|
||||
restTime: 0
|
||||
};
|
||||
|
||||
// Store management class
|
||||
class TimerStore {
|
||||
// Method to save repetitions
|
||||
async saveReps(reps: number): Promise<void> {
|
||||
try {
|
||||
await AsyncStorage.setItem(TIMER_KEYS.REPS, reps.toString());
|
||||
} catch (error) {
|
||||
console.error('Error saving repetitions:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Method to save work time
|
||||
async saveWorkTime(workTime: number): Promise<void> {
|
||||
try {
|
||||
await AsyncStorage.setItem(TIMER_KEYS.WORK_TIME, workTime.toString());
|
||||
} catch (error) {
|
||||
console.error('Error saving work time:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Method to save rest time
|
||||
async saveRestTime(restTime: number): Promise<void> {
|
||||
try {
|
||||
await AsyncStorage.setItem(TIMER_KEYS.REST_TIME, restTime.toString());
|
||||
} catch (error) {
|
||||
console.error('Error saving rest time:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Method to save complete state
|
||||
async saveState(state: TimerState): Promise<void> {
|
||||
try {
|
||||
await Promise.all([
|
||||
this.saveReps(state.reps),
|
||||
this.saveWorkTime(state.workTime),
|
||||
this.saveRestTime(state.restTime)
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Error saving state:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Method to retrieve repetitions
|
||||
async getReps(): Promise<number> {
|
||||
try {
|
||||
const value = await AsyncStorage.getItem(TIMER_KEYS.REPS);
|
||||
return value !== null ? parseInt(value, 10) : DEFAULT_STATE.reps;
|
||||
} catch (error) {
|
||||
console.error('Error retrieving repetitions:', error);
|
||||
return DEFAULT_STATE.reps;
|
||||
}
|
||||
}
|
||||
|
||||
// Method to retrieve work time
|
||||
async getWorkTime(): Promise<number> {
|
||||
try {
|
||||
const value = await AsyncStorage.getItem(TIMER_KEYS.WORK_TIME);
|
||||
return value !== null ? parseInt(value, 10) : DEFAULT_STATE.workTime;
|
||||
} catch (error) {
|
||||
console.error('Error retrieving work time:', error);
|
||||
return DEFAULT_STATE.workTime;
|
||||
}
|
||||
}
|
||||
|
||||
// Method to retrieve rest time
|
||||
async getRestTime(): Promise<number> {
|
||||
try {
|
||||
const value = await AsyncStorage.getItem(TIMER_KEYS.REST_TIME);
|
||||
return value !== null ? parseInt(value, 10) : DEFAULT_STATE.restTime;
|
||||
} catch (error) {
|
||||
console.error('Error retrieving rest time:', error);
|
||||
return DEFAULT_STATE.restTime;
|
||||
}
|
||||
}
|
||||
|
||||
// Method to retrieve complete state
|
||||
async getState(): Promise<TimerState> {
|
||||
try {
|
||||
const [reps, workTime, restTime] = await Promise.all([
|
||||
this.getReps(),
|
||||
this.getWorkTime(),
|
||||
this.getRestTime()
|
||||
]);
|
||||
|
||||
return {
|
||||
reps,
|
||||
workTime,
|
||||
restTime
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error retrieving state:', error);
|
||||
return DEFAULT_STATE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export a singleton instance
|
||||
export const timerStore = new TimerStore();
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
import { renderHook, act, waitFor } from '@testing-library/react-native';
|
||||
import { TimerProvider, useTimerContext } from '../TimerContext';
|
||||
import { timerStore } from '../TimerStore';
|
||||
import '@testing-library/jest-native/extend-expect';
|
||||
|
||||
jest.mock('../TimerStore', () => ({
|
||||
timerStore: {
|
||||
getState: jest.fn(),
|
||||
saveReps: jest.fn(),
|
||||
saveWorkTime: jest.fn(),
|
||||
saveRestTime: jest.fn(),
|
||||
saveState: jest.fn(),
|
||||
}
|
||||
}));
|
||||
|
||||
const mockTimerState = {
|
||||
reps: 5,
|
||||
workTime: 30,
|
||||
restTime: 10
|
||||
};
|
||||
|
||||
describe('TimerContext', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Default configuration for getState
|
||||
(timerStore.getState as jest.Mock<Promise<any>>).mockResolvedValue(mockTimerState);
|
||||
});
|
||||
|
||||
it('should load initial data', async () => {
|
||||
const { result } = renderHook(() => useTimerContext(), {
|
||||
wrapper: TimerProvider
|
||||
});
|
||||
|
||||
// Check initial loading state
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
|
||||
// Wait for loading to complete
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
// Verify getState was called
|
||||
expect(timerStore.getState).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Verify data is correctly loaded
|
||||
expect(result.current.timerState).toEqual(mockTimerState);
|
||||
});
|
||||
|
||||
it('should handle errors when loading data', async () => {
|
||||
// Simulate an error
|
||||
(timerStore.getState as jest.Mock<Promise<any>>).mockRejectedValueOnce(new Error('Loading error'));
|
||||
|
||||
// Spy on console.error
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
const { result } = renderHook(() => useTimerContext(), {
|
||||
wrapper: TimerProvider
|
||||
});
|
||||
|
||||
// Wait for loading to complete despite error
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
// Verify error was logged
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
'Error loading data:',
|
||||
expect.any(Error)
|
||||
);
|
||||
|
||||
// Restore console.error
|
||||
(console.error as jest.MockedFunction<typeof console.error>).mockRestore();
|
||||
});
|
||||
|
||||
it('should update repetitions', async () => {
|
||||
const { result } = renderHook(() => useTimerContext(), {
|
||||
wrapper: TimerProvider
|
||||
});
|
||||
|
||||
// Wait for initial loading to complete
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
// Update repetitions
|
||||
await act(async () => {
|
||||
await result.current.updateReps(10);
|
||||
});
|
||||
|
||||
// Verify saveReps was called with the correct value
|
||||
expect(timerStore.saveReps).toHaveBeenCalledWith(10);
|
||||
|
||||
// Verify state was updated
|
||||
expect(result.current.timerState.reps).toBe(10);
|
||||
});
|
||||
|
||||
it('should handle errors when updating repetitions', async () => {
|
||||
// Simulate an error
|
||||
(timerStore.saveReps as jest.Mock<Promise<void>>).mockRejectedValueOnce(new Error('Saving error'));
|
||||
|
||||
// Spy on console.error
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
const { result } = renderHook(() => useTimerContext(), {
|
||||
wrapper: TimerProvider
|
||||
});
|
||||
|
||||
// Wait for initial loading to complete
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
// Attempt to update repetitions
|
||||
await act(async () => {
|
||||
await result.current.updateReps(10);
|
||||
});
|
||||
|
||||
// Verify error was logged
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
'Error updating repetitions:',
|
||||
expect.any(Error)
|
||||
);
|
||||
|
||||
// Restore console.error
|
||||
(console.error as jest.MockedFunction<typeof console.error>).mockRestore();
|
||||
});
|
||||
|
||||
it('should update work time', async () => {
|
||||
const { result } = renderHook(() => useTimerContext(), {
|
||||
wrapper: TimerProvider
|
||||
});
|
||||
|
||||
// Wait for initial loading to complete
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
// Update work time
|
||||
await act(async () => {
|
||||
await result.current.updateWorkTime(45);
|
||||
});
|
||||
|
||||
// Verify saveWorkTime was called with the correct value
|
||||
expect(timerStore.saveWorkTime).toHaveBeenCalledWith(45);
|
||||
|
||||
// Verify state was updated
|
||||
expect(result.current.timerState.workTime).toBe(45);
|
||||
});
|
||||
|
||||
it('should update rest time', async () => {
|
||||
const { result } = renderHook(() => useTimerContext(), {
|
||||
wrapper: TimerProvider
|
||||
});
|
||||
|
||||
// Wait for initial loading to complete
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
// Update rest time
|
||||
await act(async () => {
|
||||
await result.current.updateRestTime(15);
|
||||
});
|
||||
|
||||
// Verify saveRestTime was called with the correct value
|
||||
expect(timerStore.saveRestTime).toHaveBeenCalledWith(15);
|
||||
|
||||
// Verify state was updated
|
||||
expect(result.current.timerState.restTime).toBe(15);
|
||||
});
|
||||
|
||||
it('should update multiple values at once', async () => {
|
||||
const { result } = renderHook(() => useTimerContext(), {
|
||||
wrapper: TimerProvider
|
||||
});
|
||||
|
||||
// Wait for initial loading to complete
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
const newState = {
|
||||
reps: 8,
|
||||
workTime: 60,
|
||||
restTime: 20
|
||||
};
|
||||
|
||||
// Update complete state
|
||||
await act(async () => {
|
||||
await result.current.updateState(newState);
|
||||
});
|
||||
|
||||
// Verify saveState was called with the correct state
|
||||
expect(timerStore.saveState).toHaveBeenCalledWith({
|
||||
...mockTimerState,
|
||||
...newState
|
||||
});
|
||||
|
||||
// Verify state was updated
|
||||
expect(result.current.timerState).toEqual({
|
||||
...mockTimerState,
|
||||
...newState
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,263 @@
|
|||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { timerStore, TimerState } from '../TimerStore';
|
||||
|
||||
// Mock AsyncStorage with correct typing for Jest
|
||||
jest.mock('@react-native-async-storage/async-storage', () => ({
|
||||
setItem: jest.fn(() => Promise.resolve()),
|
||||
getItem: jest.fn(() => Promise.resolve(null)),
|
||||
}));
|
||||
|
||||
// Add type for mocked functions
|
||||
const mockedAsyncStorage = AsyncStorage as jest.Mocked<typeof AsyncStorage>;
|
||||
|
||||
describe('TimerStore', () => {
|
||||
// Clear all mocks after each test
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
// Tests for saveReps method
|
||||
describe('saveReps', () => {
|
||||
test('should save repetitions in AsyncStorage', async () => {
|
||||
await timerStore.saveReps(5);
|
||||
expect(mockedAsyncStorage.setItem).toHaveBeenCalledWith('timer_reps', '5');
|
||||
});
|
||||
|
||||
test('should handle errors when saving repetitions', async () => {
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
mockedAsyncStorage.setItem.mockRejectedValueOnce(new Error('AsyncStorage Error'));
|
||||
|
||||
await timerStore.saveReps(3);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Error saving repetitions:',
|
||||
expect.any(Error)
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
// Tests for saveWorkTime method
|
||||
describe('saveWorkTime', () => {
|
||||
test('should save work time in AsyncStorage', async () => {
|
||||
await timerStore.saveWorkTime(30);
|
||||
expect(mockedAsyncStorage.setItem).toHaveBeenCalledWith('timer_work_time', '30');
|
||||
});
|
||||
|
||||
test('should handle errors when saving work time', async () => {
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
mockedAsyncStorage.setItem.mockRejectedValueOnce(new Error('AsyncStorage Error'));
|
||||
|
||||
await timerStore.saveWorkTime(30);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Error saving work time:',
|
||||
expect.any(Error)
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
// Tests for saveRestTime method
|
||||
describe('saveRestTime', () => {
|
||||
test('should save rest time in AsyncStorage', async () => {
|
||||
await timerStore.saveRestTime(15);
|
||||
expect(mockedAsyncStorage.setItem).toHaveBeenCalledWith('timer_rest_time', '15');
|
||||
});
|
||||
|
||||
test('should handle errors when saving rest time', async () => {
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
mockedAsyncStorage.setItem.mockRejectedValueOnce(new Error('AsyncStorage Error'));
|
||||
|
||||
await timerStore.saveRestTime(15);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Error saving rest time:',
|
||||
expect.any(Error)
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
// Tests for saveState method
|
||||
describe('saveState', () => {
|
||||
test('should save complete state in AsyncStorage', async () => {
|
||||
const state: TimerState = {
|
||||
reps: 3,
|
||||
workTime: 40,
|
||||
restTime: 20
|
||||
};
|
||||
|
||||
await timerStore.saveState(state);
|
||||
|
||||
expect(mockedAsyncStorage.setItem).toHaveBeenCalledWith('timer_reps', '3');
|
||||
expect(mockedAsyncStorage.setItem).toHaveBeenCalledWith('timer_work_time', '40');
|
||||
expect(mockedAsyncStorage.setItem).toHaveBeenCalledWith('timer_rest_time', '20');
|
||||
});
|
||||
|
||||
test('should handle errors when saving state', async () => {
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
mockedAsyncStorage.setItem.mockRejectedValueOnce(new Error('AsyncStorage Error'));
|
||||
|
||||
const state: TimerState = {
|
||||
reps: 3,
|
||||
workTime: 40,
|
||||
restTime: 20
|
||||
};
|
||||
|
||||
await timerStore.saveState(state);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Error saving repetitions:',
|
||||
expect.any(Error)
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
// Tests for getReps method
|
||||
describe('getReps', () => {
|
||||
test('should retrieve repetitions from AsyncStorage', async () => {
|
||||
mockedAsyncStorage.getItem.mockResolvedValueOnce('5');
|
||||
|
||||
const result = await timerStore.getReps();
|
||||
|
||||
expect(result).toBe(5);
|
||||
expect(mockedAsyncStorage.getItem).toHaveBeenCalledWith('timer_reps');
|
||||
});
|
||||
|
||||
test('should return default value if no data is found', async () => {
|
||||
mockedAsyncStorage.getItem.mockResolvedValueOnce(null);
|
||||
|
||||
const result = await timerStore.getReps();
|
||||
|
||||
expect(result).toBe(1); // Default value
|
||||
expect(mockedAsyncStorage.getItem).toHaveBeenCalledWith('timer_reps');
|
||||
});
|
||||
|
||||
test('should handle errors when retrieving repetitions', async () => {
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
mockedAsyncStorage.getItem.mockRejectedValueOnce(new Error('AsyncStorage Error'));
|
||||
|
||||
const result = await timerStore.getReps();
|
||||
|
||||
expect(result).toBe(1); // Default value
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Error retrieving repetitions:',
|
||||
expect.any(Error)
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
// Tests for getWorkTime method
|
||||
describe('getWorkTime', () => {
|
||||
test('should retrieve work time from AsyncStorage', async () => {
|
||||
mockedAsyncStorage.getItem.mockResolvedValueOnce('30');
|
||||
|
||||
const result = await timerStore.getWorkTime();
|
||||
|
||||
expect(result).toBe(30);
|
||||
expect(mockedAsyncStorage.getItem).toHaveBeenCalledWith('timer_work_time');
|
||||
});
|
||||
|
||||
test('should return default value if no data is found', async () => {
|
||||
mockedAsyncStorage.getItem.mockResolvedValueOnce(null);
|
||||
|
||||
const result = await timerStore.getWorkTime();
|
||||
|
||||
expect(result).toBe(0); // Default value
|
||||
expect(mockedAsyncStorage.getItem).toHaveBeenCalledWith('timer_work_time');
|
||||
});
|
||||
|
||||
test('should handle errors when retrieving work time', async () => {
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
mockedAsyncStorage.getItem.mockRejectedValueOnce(new Error('AsyncStorage Error'));
|
||||
|
||||
const result = await timerStore.getWorkTime();
|
||||
|
||||
expect(result).toBe(0); // Default value
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Error retrieving work time:',
|
||||
expect.any(Error)
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
// Tests for getRestTime method
|
||||
describe('getRestTime', () => {
|
||||
test('should retrieve rest time from AsyncStorage', async () => {
|
||||
mockedAsyncStorage.getItem.mockResolvedValueOnce('15');
|
||||
|
||||
const result = await timerStore.getRestTime();
|
||||
|
||||
expect(result).toBe(15);
|
||||
expect(mockedAsyncStorage.getItem).toHaveBeenCalledWith('timer_rest_time');
|
||||
});
|
||||
|
||||
test('should return default value if no data is found', async () => {
|
||||
mockedAsyncStorage.getItem.mockResolvedValueOnce(null);
|
||||
|
||||
const result = await timerStore.getRestTime();
|
||||
|
||||
expect(result).toBe(0); // Default value
|
||||
expect(mockedAsyncStorage.getItem).toHaveBeenCalledWith('timer_rest_time');
|
||||
});
|
||||
|
||||
test('should handle errors when retrieving rest time', async () => {
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
mockedAsyncStorage.getItem.mockRejectedValueOnce(new Error('AsyncStorage Error'));
|
||||
|
||||
const result = await timerStore.getRestTime();
|
||||
|
||||
expect(result).toBe(0); // Default value
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Error retrieving rest time:',
|
||||
expect.any(Error)
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
// Tests for getState method
|
||||
describe('getState', () => {
|
||||
test('should retrieve complete state from AsyncStorage', async () => {
|
||||
mockedAsyncStorage.getItem
|
||||
.mockResolvedValueOnce('3') // reps
|
||||
.mockResolvedValueOnce('40') // workTime
|
||||
.mockResolvedValueOnce('20'); // restTime
|
||||
|
||||
const result = await timerStore.getState();
|
||||
|
||||
expect(result).toEqual({
|
||||
reps: 3,
|
||||
workTime: 40,
|
||||
restTime: 20
|
||||
});
|
||||
|
||||
expect(mockedAsyncStorage.getItem).toHaveBeenCalledWith('timer_reps');
|
||||
expect(mockedAsyncStorage.getItem).toHaveBeenCalledWith('timer_work_time');
|
||||
expect(mockedAsyncStorage.getItem).toHaveBeenCalledWith('timer_rest_time');
|
||||
});
|
||||
|
||||
test('should handle errors when retrieving state', async () => {
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
mockedAsyncStorage.getItem.mockRejectedValueOnce(new Error('AsyncStorage Error'));
|
||||
|
||||
const result = await timerStore.getState();
|
||||
|
||||
expect(result).toEqual({
|
||||
reps: 1,
|
||||
workTime: 0,
|
||||
restTime: 0
|
||||
}); // Default values
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Error retrieving repetitions:',
|
||||
expect.any(Error)
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -34,7 +34,7 @@ describe('[Component] Card', () => {
|
|||
const { getByTestId } = renderComponent({ backgroundColor: 'green'});
|
||||
|
||||
const card = getByTestId('card');
|
||||
expect(card?.props.style[0].backgroundColor).toEqual(theme.colors.green);
|
||||
expect(card?.props.style[0].backgroundColor).toEqual(theme.colors.fixed.green);
|
||||
});
|
||||
|
||||
test('does not render as TouchableOpacity when onPress is not provided', () => {
|
||||
|
|
@ -56,5 +56,3 @@ describe('[Component] Card', () => {
|
|||
expect(onPressMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ exports[`[Component] Button Disabled state renders correctly 1`] = `
|
|||
style={
|
||||
[
|
||||
{
|
||||
"color": "#000",
|
||||
"color": "#000000",
|
||||
},
|
||||
[
|
||||
{
|
||||
|
|
@ -116,7 +116,7 @@ exports[`[Component] Button Loading state renders correctly 1`] = `
|
|||
style={
|
||||
[
|
||||
{
|
||||
"color": "#000",
|
||||
"color": "#000000",
|
||||
},
|
||||
[
|
||||
{
|
||||
|
|
@ -183,7 +183,7 @@ exports[`[Component] Button Ready state renders correctly 1`] = `
|
|||
style={
|
||||
[
|
||||
{
|
||||
"color": "#000",
|
||||
"color": "#000000",
|
||||
},
|
||||
[
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ exports[`[Component] NumberSelect renders correctly 1`] = `
|
|||
style={
|
||||
[
|
||||
{
|
||||
"backgroundColor": "#fff",
|
||||
"backgroundColor": "#FFFFFF",
|
||||
},
|
||||
[
|
||||
{
|
||||
|
|
@ -22,7 +22,7 @@ exports[`[Component] NumberSelect renders correctly 1`] = `
|
|||
style={
|
||||
[
|
||||
{
|
||||
"color": "#000",
|
||||
"color": "#000000",
|
||||
},
|
||||
[
|
||||
{
|
||||
|
|
@ -54,7 +54,7 @@ exports[`[Component] NumberSelect renders correctly 1`] = `
|
|||
style={
|
||||
[
|
||||
{
|
||||
"color": "#000",
|
||||
"color": "#000000",
|
||||
},
|
||||
[
|
||||
{
|
||||
|
|
@ -87,7 +87,7 @@ exports[`[Component] NumberSelect renders correctly 1`] = `
|
|||
style={
|
||||
[
|
||||
{
|
||||
"color": "#000",
|
||||
"color": "#000000",
|
||||
},
|
||||
[
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { Text, View } from '@/components/shared/Themed';
|
||||
import styled from '@emotion/native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
import Button from '@/components/shared/Button';
|
||||
import { HorizontalSpacer, VerticalSpacer } from '@/components/shared/Spacers';
|
||||
|
|
@ -18,10 +18,10 @@ export default function FinishContent({
|
|||
handleStart,
|
||||
handleReset
|
||||
}: FinishContentProps) {
|
||||
const router = useRouter();
|
||||
const navigation = useNavigation();
|
||||
|
||||
const handleBackClick = () => {
|
||||
router.push('/');
|
||||
navigation.goBack();
|
||||
handleReset();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { formatTime } from '@/components/shared/business/timeHelpers';
|
|||
import Button from '@/components/shared/Button';
|
||||
import { ElasticSpacer, VerticalSpacer } from '@/components/shared/Spacers';
|
||||
import { TimerBgColor } from '@/components/useCases/timer/business/type';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { i18n } from '@/app/i18n/i18n';
|
||||
|
||||
interface TimerContentProps {
|
||||
|
|
@ -37,10 +37,10 @@ export default function TimerContent({
|
|||
previousRep,
|
||||
bgColor,
|
||||
}: TimerContentProps ) {
|
||||
const router = useRouter();
|
||||
const navigation = useNavigation();
|
||||
|
||||
const handleBackClick = () => {
|
||||
router.push('/');
|
||||
navigation.goBack();
|
||||
handleReset();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@
|
|||
"@babel/core": "^7.20.0",
|
||||
"@testing-library/jest-native": "^5.4.3",
|
||||
"@testing-library/react-native": "^13.2.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/react": "~18.2.45",
|
||||
"@types/react-native-background-timer": "^2.0.2",
|
||||
"@types/react-native-push-notification": "^8.1.4",
|
||||
|
|
@ -8059,6 +8060,48 @@
|
|||
"@types/istanbul-lib-report": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest": {
|
||||
"version": "29.5.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz",
|
||||
"integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"expect": "^29.0.0",
|
||||
"pretty-format": "^29.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/pretty-format": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
|
||||
"integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jest/schemas": "^29.6.3",
|
||||
"ansi-styles": "^5.0.0",
|
||||
"react-is": "^18.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/jsdom": {
|
||||
"version": "20.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz",
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@
|
|||
"@babel/core": "^7.20.0",
|
||||
"@testing-library/jest-native": "^5.4.3",
|
||||
"@testing-library/react-native": "^13.2.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/react": "~18.2.45",
|
||||
"@types/react-native-background-timer": "^2.0.2",
|
||||
"@types/react-native-push-notification": "^8.1.4",
|
||||
|
|
|
|||
Loading…
Reference in New Issue