From 4cba2025cf2161f134ec4875aade0cedcf287a40 Mon Sep 17 00:00:00 2001 From: Anthony RICHARD Date: Mon, 12 May 2025 15:12:59 +0200 Subject: [PATCH] Add abstract service --- api/endpoints.jsx | 9 ++-- api/services/ExercicesServices.jsx | 12 ----- api/services/abstract.service.tsx | 52 +++++++++++++++++++ api/services/exercice/ExercicesServices.jsx | 12 +++++ api/services/user/user.service.api.tsx | 14 +++++ .../services}/user/user.service.interface.tsx | 2 +- .../services}/user/user.service.stub.tsx | 14 +++-- app/(tabs)/(exercice)/ExercicesScreen.tsx | 6 ++- app/(tabs)/(exercice)/WorkoutScreen.tsx | 6 ++- app/(tabs)/(home)/HomeScreen.tsx | 7 ++- app/(tabs)/_layout.tsx | 4 +- app/+not-found.tsx | 17 +++--- components/HeaderProfileComponent.tsx | 45 ++++++++-------- components/WorkoutCardComponent.tsx | 4 +- components/form/LoginForm.tsx | 18 ++++--- components/form/SigninForm.tsx | 1 - package-lock.json | 52 +++++++++++++++++-- package.json | 4 +- 18 files changed, 205 insertions(+), 74 deletions(-) delete mode 100644 api/services/ExercicesServices.jsx create mode 100644 api/services/abstract.service.tsx create mode 100644 api/services/exercice/ExercicesServices.jsx create mode 100644 api/services/user/user.service.api.tsx rename {service => api/services}/user/user.service.interface.tsx (54%) rename {service => api/services}/user/user.service.stub.tsx (89%) diff --git a/api/endpoints.jsx b/api/endpoints.jsx index dacb119..cc7aff0 100644 --- a/api/endpoints.jsx +++ b/api/endpoints.jsx @@ -1,5 +1,8 @@ - +export const AUTH = { + LOGIN: "/login", + REFRESH_TOKEN: "/refresh-token", +}; export const EXERCICES = { - GETALL: '/exercices', -} \ No newline at end of file + GETALL: "/exercices", +}; diff --git a/api/services/ExercicesServices.jsx b/api/services/ExercicesServices.jsx deleted file mode 100644 index ca27e8f..0000000 --- a/api/services/ExercicesServices.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import apiClient from "../client"; -import {EXERCICES} from "../endpoints"; - -export const getExercices = async () => { - try { - const response = await apiClient.get(EXERCICES.GETALL); - return response.data.data; - } catch (error) { - console.error(error); - throw error; - } -}; diff --git a/api/services/abstract.service.tsx b/api/services/abstract.service.tsx new file mode 100644 index 0000000..967be3f --- /dev/null +++ b/api/services/abstract.service.tsx @@ -0,0 +1,52 @@ +import AsyncStorage from "@react-native-async-storage/async-storage"; +import apiClient from "../client"; +import { AUTH } from "../endpoints"; + +const ACCESS_TOKEN_PATH = "access-token"; +const REFRESH_TOKEN_PATH = "refresh-token"; + +export abstract class AbstractService { + protected async request(callback: () => Promise): Promise { + try { + return await callback(); + } catch (error: any) { + if (error.response?.status === 401) { + const success = await this.tryRefreshToken(); + if (success) { + return await callback(); + } else { + throw new Error("Session expired"); + } + } + throw error; + } + } + + private async tryRefreshToken(): Promise { + try { + const refreshToken = await AsyncStorage.getItem(REFRESH_TOKEN_PATH); + + if (!refreshToken) return false; + + const response = await apiClient.post(AUTH.REFRESH_TOKEN, { + refreshToken, + }); + + const { accessToken, refreshToken: newRefreshToken } = response.data; + + // Save new tokens + await AsyncStorage.setItem(ACCESS_TOKEN_PATH, accessToken); + await AsyncStorage.setItem("refreshToken", newRefreshToken); + + // Update apiClient headers + apiClient.defaults.headers.common[ + "Authorization" + ] = `Bearer ${accessToken}`; + + return true; + } catch (e) { + console.error("Refresh token failed", e); + return false; + } + } +} diff --git a/api/services/exercice/ExercicesServices.jsx b/api/services/exercice/ExercicesServices.jsx new file mode 100644 index 0000000..619e4b4 --- /dev/null +++ b/api/services/exercice/ExercicesServices.jsx @@ -0,0 +1,12 @@ +import apiClient from "@/api/client"; +import { EXERCICES } from "@/api/endpoints"; +import { AbstractService } from "../abstract.service"; + +export class ExerciceService extends AbstractService { + async getExercices() { + return this.request(async () => { + const response = await apiClient.get(EXERCICES.GETALL); + return response.data.data; + }); + } +} diff --git a/api/services/user/user.service.api.tsx b/api/services/user/user.service.api.tsx new file mode 100644 index 0000000..d905d64 --- /dev/null +++ b/api/services/user/user.service.api.tsx @@ -0,0 +1,14 @@ +import apiClient from "@/api/client"; +import { AUTH } from "@/api/endpoints"; +import { User } from "@/model/User"; +import { AbstractService as AbstractAPIService } from "../abstract.service"; +import { IUserService } from "./user.service.interface"; + +export class UserAPIService extends AbstractAPIService implements IUserService { + async login(email: string, password: string): Promise { + return this.request(async () => { + const response = await apiClient.get(AUTH.LOGIN); + return response.data.data; + }); + } +} diff --git a/service/user/user.service.interface.tsx b/api/services/user/user.service.interface.tsx similarity index 54% rename from service/user/user.service.interface.tsx rename to api/services/user/user.service.interface.tsx index d8b9a8f..ac33ee0 100644 --- a/service/user/user.service.interface.tsx +++ b/api/services/user/user.service.interface.tsx @@ -1,5 +1,5 @@ import { User } from "@/model/User"; export interface IUserService { - login(email: string, password: string): User | undefined; + login(email: string, password: string): Promise; } diff --git a/service/user/user.service.stub.tsx b/api/services/user/user.service.stub.tsx similarity index 89% rename from service/user/user.service.stub.tsx rename to api/services/user/user.service.stub.tsx index 8162857..d2f7f8b 100644 --- a/service/user/user.service.stub.tsx +++ b/api/services/user/user.service.stub.tsx @@ -1,7 +1,7 @@ import { User } from "@/model/User"; import { IUserService } from "./user.service.interface"; -export class UserServiceStub implements IUserService { +export class UserStubService implements IUserService { private readonly users: User[] = [ new User( "Alice", @@ -165,7 +165,15 @@ export class UserServiceStub implements IUserService { ), ]; - login(email: string, password: string): User | undefined { - return this.users.find((x) => x.email === email && x.password === password); + async login(email: string, password: string): Promise { + const user = this.users.find( + (x) => x.email === email && x.password === password + ); + + if (!user) { + throw new Error("No user."); + } + + return user; } } diff --git a/app/(tabs)/(exercice)/ExercicesScreen.tsx b/app/(tabs)/(exercice)/ExercicesScreen.tsx index 96ca18e..0e86150 100644 --- a/app/(tabs)/(exercice)/ExercicesScreen.tsx +++ b/app/(tabs)/(exercice)/ExercicesScreen.tsx @@ -1,10 +1,12 @@ -import { getExercices } from "@/api/services/ExercicesServices"; +import { ExerciceService } from "@/api/services/exercice/ExercicesServices"; import WorkoutCardComponent from "@/components/WorkoutCardComponent"; import { Workout } from "@/model/Workout"; import { useRouter } from "expo-router"; import React, { useEffect, useState } from "react"; import { FlatList, Text, TouchableOpacity, View } from "react-native"; +const service = new ExerciceService(); + export default function ExercicesScreen() { const [exercices, setExercices] = useState([]); const [loading, setLoading] = useState(true); @@ -14,7 +16,7 @@ export default function ExercicesScreen() { useEffect(() => { const fetchData = async () => { try { - const data = await getExercices(); + const data = await service.getExercices(); setExercices(data); } catch (err: any) { setError(err.message); diff --git a/app/(tabs)/(exercice)/WorkoutScreen.tsx b/app/(tabs)/(exercice)/WorkoutScreen.tsx index b4127c2..01bd46a 100644 --- a/app/(tabs)/(exercice)/WorkoutScreen.tsx +++ b/app/(tabs)/(exercice)/WorkoutScreen.tsx @@ -1,9 +1,11 @@ -import { getExercices } from "@/api/services/ExercicesServices"; +import { ExerciceService } from "@/api/services/exercice/ExercicesServices"; import WorkoutPresentationComponent from "@/components/WorkoutPresentationComponent"; import { useRouter } from "expo-router"; import React, { useEffect, useState } from "react"; import { ActivityIndicator, Text, View } from "react-native"; +const service = new ExerciceService(); + export default function WorkoutScreen() { const router = useRouter(); @@ -14,7 +16,7 @@ export default function WorkoutScreen() { useEffect(() => { const loadExercices = async () => { try { - const data = await getExercices(); + const data = await service.getExercices(); setExercices(data); } catch (err: any) { setError(err.message); diff --git a/app/(tabs)/(home)/HomeScreen.tsx b/app/(tabs)/(home)/HomeScreen.tsx index d62cc43..a979719 100644 --- a/app/(tabs)/(home)/HomeScreen.tsx +++ b/app/(tabs)/(home)/HomeScreen.tsx @@ -1,4 +1,4 @@ -import { getExercices } from "@/api/services/ExercicesServices"; +import { ExerciceService } from "@/api/services/exercice/ExercicesServices"; import ActivitiesComponent from "@/components/ActivitiesComponent"; import CalendarComponent from "@/components/CalendarComponent"; import WelcomeComponent from "@/components/WelcomeComponent"; @@ -7,6 +7,9 @@ import Screen from "@/components/ui/Screen"; import { Workout } from "@/model/Workout"; import React, { useEffect, useState } from "react"; import { ScrollView, Text, View } from "react-native"; + +const service = new ExerciceService(); + export default function HomeScreen() { const [exercices, setExercices] = useState([]); @@ -16,7 +19,7 @@ export default function HomeScreen() { useEffect(() => { const fetchData = async () => { try { - const data = await getExercices(); + const data = await service.getExercices(); setExercices(data); } catch (err: any) {} }; diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index cd8fd2d..425b201 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -1,13 +1,12 @@ import { useSession } from "@/ctx"; import { AntDesign, Ionicons, MaterialIcons } from "@expo/vector-icons"; -import { Redirect, Tabs, useRouter } from "expo-router"; +import { Redirect, Tabs } from "expo-router"; import React from "react"; import Loading from "../loading"; export default function TabBarLayout() { const { session, isLoading } = useSession(); - const router = useRouter(); const sizeIcon = 24; if (isLoading) { return ; @@ -16,7 +15,6 @@ export default function TabBarLayout() { if (!session) { return ; } - console.log(session); if (!session.isQuizDone()) { return ; } diff --git a/app/+not-found.tsx b/app/+not-found.tsx index e9c125e..ea8a2ca 100644 --- a/app/+not-found.tsx +++ b/app/+not-found.tsx @@ -1,7 +1,6 @@ -import {Link, router, Stack, usePathname, useRouter} from 'expo-router'; -import {Button, SafeAreaView, StyleSheet, Text, View} from 'react-native'; -import React from "react"; - +import { Stack, usePathname, useRouter } from "expo-router"; +import { Button, Text, View } from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; export default function NotFoundScreen() { const pathname = usePathname(); @@ -9,11 +8,13 @@ export default function NotFoundScreen() { return ( - + - This screen {pathname} doesn't exist: {pathname} -