Add abstract service

pull/16/head
Anthony RICHARD 4 weeks ago
parent 753a2f5622
commit 4cba2025cf

@ -1,5 +1,8 @@
export const AUTH = {
LOGIN: "/login",
REFRESH_TOKEN: "/refresh-token",
};
export const EXERCICES = {
GETALL: '/exercices',
}
GETALL: "/exercices",
};

@ -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;
}
};

@ -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<T>(callback: () => Promise<T>): Promise<T> {
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<boolean> {
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;
}
}
}

@ -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;
});
}
}

@ -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<User> {
return this.request(async () => {
const response = await apiClient.get(AUTH.LOGIN);
return response.data.data;
});
}
}

@ -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<User>;
}

@ -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<User> {
const user = this.users.find(
(x) => x.email === email && x.password === password
);
if (!user) {
throw new Error("No user.");
}
return user;
}
}

@ -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<Workout[]>([]);
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);

@ -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);

@ -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<Workout[]>([]);
@ -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) {}
};

@ -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 <Loading />;
@ -16,7 +15,6 @@ export default function TabBarLayout() {
if (!session) {
return <Redirect href={"/log-in"} />;
}
console.log(session);
if (!session.isQuizDone()) {
return <Redirect href={"/quiz"} />;
}

@ -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 (
<SafeAreaView>
<Stack.Screen options={{ title: 'Oops!' }} />
<Stack.Screen options={{ title: "Oops!" }} />
<View>
<Text>This screen {pathname} doesn't exist: {pathname}</Text>
<Button title="Retour Home" onPress={() => router.replace("/")}/>
<Text>Go to home screen!</Text>
<Text>
This screen {pathname} doesn't exist: {pathname}
</Text>
<Button title="Retour Home" onPress={() => router.replace("/")} />
<Text>Go to home screen!</Text>
</View>
</SafeAreaView>
);

@ -1,28 +1,29 @@
import {Image, Text, TouchableOpacity, View} from "react-native";
import {Ionicons} from "@expo/vector-icons";
import { useSession } from "@/ctx";
import React from "react";
import {useSession} from "@/ctx";
import { Image, Text, View } from "react-native";
export default function HeaderProfileComponent() {
const {session} = useSession();
const { session } = useSession();
return (
<View className="rounded-2xl overflow-hidden shadow-lg h-auto p-4">
<View className="flex-row items-center justify-between">
<View className="flex-row items-center w-full">
<Image
className="h-16 w-16 rounded-2xl"
source={require("assets/images/sigma-profile.jpeg")}
/>
return (
<View className="rounded-2xl overflow-hidden shadow-lg h-auto p-4">
<View className="flex-row items-center justify-between">
<View className="flex-row items-center w-full">
<Image className="h-16 w-16 rounded-2xl"
source={require("assets/images/sigma-profile.jpeg")}
/>
<View>
<Text className="text-gray-500 font-semibold ml-4">Prêt pour t'entrainer ?</Text>
<Text className="text-black text-4xl ml-4 mt-0.5">{session}</Text>
</View>
</View>
</View>
<View>
<Text className="text-gray-500 font-semibold ml-4">
Prêt pour t'entrainer ?
</Text>
<Text className="text-black text-4xl ml-4 mt-0.5">
{session?.name}
</Text>
</View>
</View>
);
}
</View>
</View>
);
}

@ -1,9 +1,9 @@
import Text from "@/components/ui/Text";
import { Workout } from "@/model/Workout";
import { AntDesign, MaterialCommunityIcons } from "@expo/vector-icons";
import { useRouter } from "expo-router";
import React from "react";
import Text from "@/components/ui/Text"
import {ImageBackground, TouchableOpacity, View} from "react-native";
import { ImageBackground, TouchableOpacity, View } from "react-native";
interface WorkoutCardComponentProps {
exercise?: Workout;

@ -1,5 +1,5 @@
import { UserStubService } from "@/api/services/user/user.service.stub";
import { useSession } from "@/ctx";
import { UserServiceStub } from "@/service/user/user.service.stub";
import { router } from "expo-router";
import React from "react";
import { View, ViewProps } from "react-native";
@ -10,7 +10,7 @@ import FormError from "./FormError";
import TextInput from "./FormInput";
import PasswordTextInput from "./SecretTextInput";
const userService = new UserServiceStub();
const userService = new UserStubService();
export default React.forwardRef<any, ViewProps>(
({ ...props }, ref): React.ReactElement => {
@ -36,12 +36,14 @@ export default React.forwardRef<any, ViewProps>(
if (password != "") {
validateForm();
const user = userService.login(email, password);
if (user) {
signIn(user);
router.replace("/HomeScreen");
} else {
invalidateForm("Email ou mot de passe incorrect");
}
user.then((u) => {
if (u) {
signIn(u);
router.replace("/HomeScreen");
} else {
invalidateForm("Email ou mot de passe incorrect");
}
});
} else {
invalidateForm(EMPTY_FIELD);
}

@ -32,7 +32,6 @@ export default React.forwardRef<any, ViewProps>(
if (confirmPassword != "") {
if (confirmPassword == password) {
validateForm();
console.log("tmp");
} else {
invalidateForm(NOT_MATCHING_PASSWORD);
}

52
package-lock.json generated

@ -10,6 +10,7 @@
"dependencies": {
"@expo/html-elements": "^0.4.2",
"@expo/vector-icons": "^14.0.2",
"@react-native-async-storage/async-storage": "^2.1.2",
"@react-native-community/slider": "4.5.6",
"@react-native-segmented-control/segmented-control": "2.5.7",
"@react-navigation/bottom-tabs": "^7.2.0",
@ -26,7 +27,7 @@
"expo-haptics": "^14.0.1",
"expo-linear-gradient": "^14.0.2",
"expo-linking": "^7.0.5",
"expo-router": "~5.0.6",
"expo-router": "~5.0.7",
"expo-secure-store": "^14.0.1",
"expo-splash-screen": "~0.30.8",
"expo-status-bar": "^2.0.1",
@ -38,6 +39,7 @@
"react-dom": "19.0.0",
"react-hook-form": "^7.54.2",
"react-native": "0.79.2",
"react-native-async-storage": "^0.0.1",
"react-native-element-dropdown": "^2.12.4",
"react-native-gesture-handler": "~2.24.0",
"react-native-gifted-charts": "^1.4.54",
@ -2723,6 +2725,18 @@
}
}
},
"node_modules/@react-native-async-storage/async-storage": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.1.2.tgz",
"integrity": "sha512-dvlNq4AlGWC+ehtH12p65+17V0Dx7IecOWl6WanF2ja38O1Dcjjvn7jVzkUHJ5oWkQBlyASurTPlTHgKXyYiow==",
"license": "MIT",
"dependencies": {
"merge-options": "^3.0.4"
},
"peerDependencies": {
"react-native": "^0.0.0-0 || >=0.65 <1.0"
}
},
"node_modules/@react-native-community/slider": {
"version": "4.5.6",
"resolved": "https://registry.npmjs.org/@react-native-community/slider/-/slider-4.5.6.tgz",
@ -6028,9 +6042,9 @@
}
},
"node_modules/expo-router": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/expo-router/-/expo-router-5.0.6.tgz",
"integrity": "sha512-/44G3liB7LMMDoUO+lN5TS8XvZrAhLtq7cVGoilO2QkoSBjFQfxFA9VYOVWVlu2R80tN6dM3cgsEuoA275FGQg==",
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/expo-router/-/expo-router-5.0.7.tgz",
"integrity": "sha512-NlEgRXCKtseDuIHBp87UfkvqsuVrc0MYG+zg33dopaN6wik4RkrWWxUYdNPHub0s/7qMye6zZBY4ZCrXwd/xpA==",
"license": "MIT",
"dependencies": {
"@expo/metro-runtime": "5.0.4",
@ -7045,6 +7059,15 @@
"node": ">=0.12.0"
}
},
"node_modules/is-plain-obj": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
"integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
@ -8680,6 +8703,18 @@
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
"license": "MIT"
},
"node_modules/merge-options": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
"integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==",
"license": "MIT",
"dependencies": {
"is-plain-obj": "^2.1.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@ -10480,6 +10515,15 @@
}
}
},
"node_modules/react-native-async-storage": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/react-native-async-storage/-/react-native-async-storage-0.0.1.tgz",
"integrity": "sha512-EwOIpNfwMiELMnS7HDzgyX2xO4TALK95O473Z6Kai5Il5j/03yTTErwMtvoYnA0OKUHd1R54ptmbbMukUH03Xg==",
"license": "MIT",
"peerDependencies": {
"react-native": ">=0.5.0"
}
},
"node_modules/react-native-css-interop": {
"version": "0.1.22",
"resolved": "https://registry.npmjs.org/react-native-css-interop/-/react-native-css-interop-0.1.22.tgz",

@ -17,6 +17,7 @@
"dependencies": {
"@expo/html-elements": "^0.4.2",
"@expo/vector-icons": "^14.0.2",
"@react-native-async-storage/async-storage": "^2.1.2",
"@react-native-community/slider": "4.5.6",
"@react-native-segmented-control/segmented-control": "2.5.7",
"@react-navigation/bottom-tabs": "^7.2.0",
@ -33,7 +34,7 @@
"expo-haptics": "^14.0.1",
"expo-linear-gradient": "^14.0.2",
"expo-linking": "^7.0.5",
"expo-router": "~5.0.6",
"expo-router": "~5.0.7",
"expo-secure-store": "^14.0.1",
"expo-splash-screen": "~0.30.8",
"expo-status-bar": "^2.0.1",
@ -45,6 +46,7 @@
"react-dom": "19.0.0",
"react-hook-form": "^7.54.2",
"react-native": "0.79.2",
"react-native-async-storage": "^0.0.1",
"react-native-element-dropdown": "^2.12.4",
"react-native-gesture-handler": "~2.24.0",
"react-native-gifted-charts": "^1.4.54",

Loading…
Cancel
Save