diff --git a/App.tsx b/App.tsx
index 14e952c..ac346f3 100644
--- a/App.tsx
+++ b/App.tsx
@@ -5,10 +5,10 @@ import { useFonts, Roboto_400Regular, Roboto_700Bold } from '@expo-google-fonts/
import { THEME } from './src/theme';
import { Loading } from '@components/Loading';
-import { SignIn } from '@screens/Signin';
import { Routes } from '@routes/index';
+import { AuthContextProvider } from '@contexts/AuthContext';
export default function App() {
const [fontsLoaded] = useFonts({
@@ -22,7 +22,9 @@ export default function App() {
backgroundColor='transparent'
translucent
/>
- {fontsLoaded ? : }
+
+ {fontsLoaded ? : }
+
);
}
\ No newline at end of file
diff --git a/package.json b/package.json
index 4fc08d8..f41fb2c 100644
--- a/package.json
+++ b/package.json
@@ -29,7 +29,8 @@
"react-native-screens": "~3.18.0",
"react-native-svg": "13.4.0",
"react-native-web": "~0.18.9",
- "yup": "^1.0.0"
+ "yup": "^1.0.0",
+ "@react-native-async-storage/async-storage": "~1.17.3"
},
"devDependencies": {
"@babel/core": "^7.12.9",
diff --git a/src/components/ExerciseCard.tsx b/src/components/ExerciseCard.tsx
index 6436a8b..db20c1d 100644
--- a/src/components/ExerciseCard.tsx
+++ b/src/components/ExerciseCard.tsx
@@ -2,16 +2,21 @@ import { Heading, HStack, Icon, Image, Text, VStack } from "native-base";
import { Entypo } from '@expo/vector-icons'
import { TouchableOpacity, TouchableOpacityProps } from "react-native";
-interface ExerciseCardProps extends TouchableOpacityProps { }
+import { ExerciseDTO } from "@dtos/ExerciseDTO";
+import { api } from "@services/http.service";
-export function ExerciseCard({ ...rest }: ExerciseCardProps) {
+interface ExerciseCardProps extends TouchableOpacityProps {
+ data: ExerciseDTO
+ }
+
+export function ExerciseCard({data, ...rest }: ExerciseCardProps) {
return (
- Remada Alternada
+ {data.name}
- 2 séries x 12 repetições
+ {data.series} séries x {data.repetitions} repetições
diff --git a/src/components/HistoryCard.tsx b/src/components/HistoryCard.tsx
index 1b74935..fcf07b3 100644
--- a/src/components/HistoryCard.tsx
+++ b/src/components/HistoryCard.tsx
@@ -1,20 +1,25 @@
+import { HistoryDTO } from "@dtos/historyDTO";
import { Heading, HStack, Text, VStack } from "native-base";
-export function HistoryCard() {
+interface HistoryCardProps {
+ data: HistoryDTO
+}
+
+export function HistoryCard({ data }: HistoryCardProps) {
return (
- Costas
+ {data.group}
- Puxada frontal
+ {data.name}
- 08:56
+ {data.hour}
)
diff --git a/src/components/HomeHeader.tsx b/src/components/HomeHeader.tsx
index 04f0e75..46ef64c 100644
--- a/src/components/HomeHeader.tsx
+++ b/src/components/HomeHeader.tsx
@@ -1,13 +1,27 @@
import { Heading, HStack, Icon, Text, VStack } from "native-base";
import { MaterialIcons } from '@expo/vector-icons'
import { TouchableOpacity } from "react-native";
+
+import { api } from "@services/http.service";
+
+import { useAuth } from "@hooks/useAuth";
+
+import defaulUserPhotoImg from '@assets/userPhotoDefault.png';
+
import { UserPhoto } from "./UserPhoto";
export function HomeHeader() {
+
+ const { user, signOut } = useAuth()
+
+ console.info(`${api.defaults.baseURL}/avatar/${user.avatar}`)
+
return (
- Carlos
+ {user.name}
-
+
Promise;
+ updateUserProfile: (userUpdated: UserDTO) => Promise;
+ signOut: () => Promise;
+ isLoadingUserStorageData: boolean;
+};
+
+type AuthContextProviderProps = {
+ children: ReactNode;
+};
+
+export const AuthContext = createContext(
+ {} as AuthContextDataProps
+);
+
+export function AuthContextProvider({ children }: AuthContextProviderProps) {
+ const [user, setUser] = useState({} as UserDTO);
+ const [isLoadingUserStorageData, setIsLoadingUserStorageData] =
+ useState(true);
+
+ async function userAndTokenUpdate(userData: UserDTO, token: string) {
+ api.defaults.headers.common["Authorization"] = `Bearer ${token}`;
+
+ setUser(userData);
+ }
+
+ async function storageUserAndTokenSave(
+ userData: UserDTO,
+ token: string,
+ refresh_token: string
+ ) {
+ try {
+ setIsLoadingUserStorageData(true);
+ await storageUserSave(userData);
+ await storageAuthTokenSave({ token, refresh_token });
+ } catch (error) {
+ throw error;
+ } finally {
+ setIsLoadingUserStorageData(false);
+ }
+ }
+
+ async function singIn(email: string, password: string) {
+ try {
+ const { data } = await api.post("/sessions", { email, password });
+
+ if (data.user && data.token && data.refresh_token) {
+ await storageUserAndTokenSave(
+ data.user,
+ data.token,
+ data.refresh_token
+ );
+ userAndTokenUpdate(data.user, data.token);
+ }
+ } catch (error) {
+ throw error;
+ } finally {
+ setIsLoadingUserStorageData(false);
+ }
+ }
+
+ async function signOut() {
+ try {
+ setIsLoadingUserStorageData(true);
+ setUser({} as UserDTO);
+ await storageUserRemove();
+ await storageAuthTokenRemove();
+ } catch (error) {
+ throw error;
+ } finally {
+ setIsLoadingUserStorageData(false);
+ }
+ }
+
+ async function updateUserProfile(userUpdated: UserDTO) {
+ try {
+ setUser(userUpdated);
+ await storageUserSave(userUpdated);
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ async function loadUserData() {
+ try {
+ setIsLoadingUserStorageData(true);
+
+ const userLogged = await storageUserGet();
+ const { token } = await storageAuthTokenGet();
+
+ if (token && userLogged) {
+ userAndTokenUpdate(userLogged, token);
+ }
+ } catch (error) {
+ throw error;
+ } finally {
+ setIsLoadingUserStorageData(false);
+ }
+ }
+
+ useEffect(() => {
+ loadUserData();
+ }, []);
+
+ useEffect(() => {
+ const subscribe = api.registerInterceptTokenManager(signOut);
+
+ return () => {
+ subscribe();
+ };
+ }, [signOut]);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/dtos/ExerciseDTO.ts b/src/dtos/ExerciseDTO.ts
new file mode 100644
index 0000000..e0989a4
--- /dev/null
+++ b/src/dtos/ExerciseDTO.ts
@@ -0,0 +1,10 @@
+export interface ExerciseDTO {
+ id: string,
+ demo: string,
+ group: string,
+ name: string,
+ repetitions: string,
+ series: number,
+ thumb: string,
+ updated_at: string,
+}
\ No newline at end of file
diff --git a/src/dtos/HistoryByDayDTO.ts b/src/dtos/HistoryByDayDTO.ts
new file mode 100644
index 0000000..7ab5ee3
--- /dev/null
+++ b/src/dtos/HistoryByDayDTO.ts
@@ -0,0 +1,5 @@
+import { HistoryDTO } from '@dtos/historyDTO';
+export interface HistoryByDayDTO{
+ title: string,
+ data: HistoryDTO[]
+}
\ No newline at end of file
diff --git a/src/dtos/HistoryDTO.ts b/src/dtos/HistoryDTO.ts
new file mode 100644
index 0000000..2abdd6b
--- /dev/null
+++ b/src/dtos/HistoryDTO.ts
@@ -0,0 +1,7 @@
+export interface HistoryDTO {
+ id: string,
+ name: string,
+ group: string,
+ hour: string,
+ created_at: string
+}
\ No newline at end of file
diff --git a/src/dtos/UserDTO.ts b/src/dtos/UserDTO.ts
new file mode 100644
index 0000000..3e9ada1
--- /dev/null
+++ b/src/dtos/UserDTO.ts
@@ -0,0 +1,6 @@
+export interface UserDTO {
+ id: string;
+ name: string;
+ email: string;
+ avatar: string;
+}
diff --git a/src/hooks/useAuth.tsx b/src/hooks/useAuth.tsx
new file mode 100644
index 0000000..fb00a1c
--- /dev/null
+++ b/src/hooks/useAuth.tsx
@@ -0,0 +1,7 @@
+import { useContext } from "react";
+import { AuthContext } from "@contexts/AuthContext";
+
+export function useAuth() {
+ const context = useContext(AuthContext)
+ return context
+}
\ No newline at end of file
diff --git a/src/routes/app.routes.tsx b/src/routes/app.routes.tsx
index 784d346..57c4a43 100644
--- a/src/routes/app.routes.tsx
+++ b/src/routes/app.routes.tsx
@@ -13,7 +13,7 @@ import { Profile } from '@screens/Profile';
type AppRoutes = {
home: undefined;
- exercise: undefined;
+ exercise: { exerciseId: string};
profile: undefined;
history: undefined;
}
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index 9c527dc..44090a5 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -1,21 +1,32 @@
+
import { DefaultTheme, NavigationContainer } from "@react-navigation/native";
import { SafeAreaView } from "react-native-safe-area-context";
import { useTheme } from "native-base";
+import { useAuth } from '@hooks/useAuth'
import { AuthRoutes } from "./auth.routes";
import { AppRoutes } from "./app.routes";
+import { Loading } from "@components/Loading";
+
export function Routes() {
const { colors } = useTheme()
+ const { user, isLoadingUserStorageData } = useAuth()
const theme = DefaultTheme
theme.colors.background = colors.gray[700]
+ if (isLoadingUserStorageData) {
+ return (
+
+ )
+ }
+
return (
-
+ {user.id ? : }
)
diff --git a/src/screens/Exercise.tsx b/src/screens/Exercise.tsx
index 41ee7f4..9a89a04 100644
--- a/src/screens/Exercise.tsx
+++ b/src/screens/Exercise.tsx
@@ -1,24 +1,95 @@
-import { useNavigation } from "@react-navigation/native";
-import { Box, Heading, HStack, Icon, Image, ScrollView, Text, VStack } from "native-base";
+import { useEffect, useState } from "react";
import { Platform, TouchableOpacity } from 'react-native'
+import { useNavigation, useRoute } from "@react-navigation/native";
+
+import { Box, Heading, HStack, Icon, Image, ScrollView, Text, VStack, useToast } from "native-base";
import { Feather } from '@expo/vector-icons'
import { AppNavigatorRoutesProps } from "@routes/app.routes";
+import { api } from "@services/http.service";
+
import BodySvg from '@assets/body.svg'
import SeriesSvg from '@assets/series.svg'
import RepetitionSvg from '@assets/repetitions.svg'
+
import { Button } from "@components/Button";
+import { Loading } from "@components/Loading";
+
+import { AppError } from "@utils/AppError";
+
+import { ExerciseDTO } from "@dtos/ExerciseDTO";
+
+interface RouteParamsProps {
+ exerciseId: string
+}
export function Exercise() {
+ const [sendingRegister, setSendingRegister] = useState(false)
+ const [isLoading, setIsLoading] = useState(true)
+ const [exercise, setExercise] = useState({} as ExerciseDTO)
const navigation = useNavigation()
+ const toast = useToast()
+ const route = useRoute()
+
+ const { exerciseId } = route.params as RouteParamsProps
+
+
function handleGoBack() {
navigation.goBack()
}
+ async function fetchExerciseDetails() {
+ setIsLoading(true)
+ try {
+ const response = await api.get(`/exercises/${exerciseId}`)
+ // console.info(`${api.defaults.baseURL}/exercise/demo/${response.data?.demo}`)
+ setExercise(response.data)
+ } catch (error) {
+ const isAppError = error instanceof AppError
+ const title = isAppError ? error.message : 'Não foi possível carregar os detalhes do exercício.'
+ toast.show({
+ title: title,
+ placement: 'top',
+ bgColor: 'red.500'
+ })
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ async function handleSendingRegister() {
+ try {
+ setSendingRegister(true)
+ await api.post('/history', { exercise_id: exerciseId })
+
+ toast.show({
+ title: "Parabens! Exercício registrado no seu histórico.",
+ placement: 'top',
+ bgColor: 'green.700'
+ })
+
+ navigation.navigate('history')
+ } catch (error) {
+ const isAppError = error instanceof AppError
+ const title = isAppError ? error.message : 'Não foi possível registrar o exercício.'
+ toast.show({
+ title: title,
+ placement: 'top',
+ bgColor: 'red.500'
+ })
+ } finally {
+ setSendingRegister(false)
+ }
+ }
+
+ useEffect(() => {
+ fetchExerciseDetails()
+ }, [exerciseId])
+
return (
@@ -28,52 +99,56 @@ export function Exercise() {
- Puxada frontal
+ {exercise.name}
- Costas
+ {exercise.group}
-
-
-
-
-
-
-
-
-
- 3 séries
-
-
+ {isLoading ? :
+
+
+
+
+
-
-
-
- 12 repetições
-
+
+
+
+
+
+ {exercise.series} séries
+
+
+
+
+
+
+ {exercise.repetitions} repetições
+
+
-
-
-
-
-
-
+
+
+
+
+ }
)
diff --git a/src/screens/History.tsx b/src/screens/History.tsx
index 0965369..df30cf9 100644
--- a/src/screens/History.tsx
+++ b/src/screens/History.tsx
@@ -1,48 +1,85 @@
-import { useState } from "react";
-import { Heading, SectionList, Text, VStack } from "native-base";
+import { useFocusEffect } from "@react-navigation/native";
+import { useCallback, useState } from "react";
+import { Heading, SectionList, Text, VStack, useToast } from "native-base";
-import { ScreenHeader } from '@components/ScreenHeader'
+import { api } from "@services/http.service";
+
+import { ScreenHeader } from "@components/ScreenHeader";
import { HistoryCard } from "@components/HistoryCard";
+import { Loading } from "@components/Loading";
+
+import { AppError } from "@utils/AppError";
+import { HistoryByDayDTO } from "@dtos/HistoryByDayDTO";
export function History() {
- const [exercises, setExercises] = useState([
- {
- title: '11.01.2015',
- data: ['Supino Reto', 'Supino Inclinado']
- },
- {
- title: '11.01.2015',
- data: ['Supino Reto', 'Supino Inclinado']
- }
- ])
- return (
-
-
- item}
- renderItem={({ item }) => (
-
- )}
- renderSectionHeader={({ section }) => (
-
- {section.title}
-
- )}
- contentContainerStyle={exercises.length === 0 && { flex: 1, justifyContent: 'center' }}
- ListEmptyComponent={() => (
-
- Não há exercícios registrados ainda. {'\n'}
- Vamos fazer exercícios hoje?
-
- )}
- px="8"
- showsVerticalScrollIndicator={false}
- />
- {/* */}
- {/* */}
-
- )
-}
\ No newline at end of file
+ const toast = useToast();
+ const [isLoading, setIsLoading] = useState(true);
+ const [exercises, setExercises] = useState([]);
+
+ async function fetchHistory() {
+ try {
+ setIsLoading(true);
+ const response = await api.get("/history");
+ setExercises(response.data);
+ } catch (error) {
+ const isAppError = error instanceof AppError;
+ const title = isAppError
+ ? error.message
+ : "Não foi possível carregar o histórico.";
+ toast.show({
+ title: title,
+ placement: "top",
+ bgColor: "red.500",
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ }
+
+ useFocusEffect(
+ useCallback(() => {
+ fetchHistory();
+ }, [])
+ );
+
+ return (
+
+
+ {isLoading ? (
+
+ ) : (
+ item.id}
+ renderItem={({ item }) => }
+ renderSectionHeader={({ section }) => (
+
+ {section.title}
+
+ )}
+ contentContainerStyle={
+ exercises.length === 0 && { flex: 1, justifyContent: "center" }
+ }
+ ListEmptyComponent={() => (
+
+ Não há exercícios registrados ainda. {"\n"}
+ Vamos fazer exercícios hoje?
+
+ )}
+ px="8"
+ showsVerticalScrollIndicator={false}
+ />
+ )}
+ {/* */}
+ {/* */}
+
+ );
+}
diff --git a/src/screens/Home.tsx b/src/screens/Home.tsx
index 706fbf9..2cfcad8 100644
--- a/src/screens/Home.tsx
+++ b/src/screens/Home.tsx
@@ -1,7 +1,7 @@
-import { useState } from "react";
+import { useCallback, useEffect, useState } from "react";
import { Platform } from "react-native";
-import { useNavigation } from "@react-navigation/native";
-import { Box, Text, FlatList, Heading, HStack, TextArea, VStack } from "native-base";
+import { useFocusEffect, useNavigation } from "@react-navigation/native";
+import { Text, FlatList, Heading, HStack, VStack, useToast } from "native-base";
import { AppNavigatorRoutesProps } from "@routes/app.routes";
@@ -9,17 +9,66 @@ import { ExerciseCard } from "@components/ExerciseCard";
import { Group } from "@components/Group";
import { HomeHeader } from "@components/HomeHeader";
+import { api } from '@services/http.service'
+
+import { AppError } from "@utils/AppError";
+import { ExerciseDTO } from "@dtos/ExerciseDTO";
+import { Loading } from "@components/Loading";
+
export function Home() {
- const [group, setGroup] = useState(['costas', 'ombro', 'peito', 'biceps', 'triceps', 'perna'])
- const [exercises, setExercises] = useState(['Remada alta', 'Remada alternada', 'Supino reto', 'Supino Inclinado'])
- const [groupSelected, setGroupSelected] = useState('costas')
+ const [group, setGroup] = useState([])
+ const [exercises, setExercises] = useState([])
+ const [groupSelected, setGroupSelected] = useState('antebraço')
+ const [isLoading, setIsLoading] = useState(true)
+ const toast = useToast()
const navigation = useNavigation()
- function handleOpenExerciseDetails() {
- navigation.navigate('exercise')
+ function handleOpenExerciseDetails(exerciseId: string) {
+ navigation.navigate('exercise', { exerciseId })
}
+ async function fetchGroups() {
+ try {
+ const response = await api.get('/groups')
+ setGroup(response.data)
+ } catch (error) {
+ const isAppError = error instanceof AppError
+ const title = isAppError ? error.message : 'Não foi possível carregar os grupos musculares.'
+ toast.show({
+ title: title,
+ placement: 'top',
+ bgColor: 'red.500'
+ })
+ }
+ }
+
+ async function fetchExerciseByGroup() {
+ setIsLoading(true)
+ try {
+ const response = await api.get(`/exercises/bygroup/${groupSelected}`)
+ setExercises(response.data)
+ } catch (error) {
+ const isAppError = error instanceof AppError
+ const title = isAppError ? error.message : 'Não foi possível carregar os exercícios.'
+ toast.show({
+ title: title,
+ placement: 'top',
+ bgColor: 'red.500'
+ })
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ useEffect(() => {
+ fetchGroups()
+ }, [])
+
+ useFocusEffect(useCallback(() => {
+ fetchExerciseByGroup()
+ }, [groupSelected]))
+
return (
@@ -45,32 +94,33 @@ export function Home() {
minH={10}
/>
+ {isLoading ? :
+
+
+
+ Exercícios
+
+
+ {exercises.length}
+
+
-
-
-
- Exercícios
-
-
- {exercises.length}
-
-
-
-
- item}
- renderItem={({ item }) => (
-
- )}
- showsVerticalScrollIndicator={false}
- _contentContainerStyle={{ paddingBottom: Platform.OS === 'ios' ? 20 : 80 }}
- />
- {/* */}
-
+ item.id}
+ renderItem={({ item }) => (
+ handleOpenExerciseDetails(item.id)}
+ />
+ )}
+ showsVerticalScrollIndicator={false}
+ _contentContainerStyle={{ paddingBottom: Platform.OS === 'ios' ? 20 : 80 }}
+ />
+ {/* */}
+
+ }
)
}
\ No newline at end of file
diff --git a/src/screens/Profile.tsx b/src/screens/Profile.tsx
index 1e0e3c5..09f22eb 100644
--- a/src/screens/Profile.tsx
+++ b/src/screens/Profile.tsx
@@ -1,114 +1,287 @@
import { useState } from "react";
import { TouchableOpacity } from "react-native";
-import { Center, ScrollView, VStack, Skeleton, Text, Heading, Toast } from "native-base";
+import { Controller, useForm } from "react-hook-form";
+import { Center, ScrollView, VStack, Skeleton, Text, Heading, useToast } from "native-base";
+import { yupResolver } from '@hookform/resolvers/yup'
import * as ImagePicker from 'expo-image-picker'
import * as FileSystem from 'expo-file-system';
+import * as yup from 'yup'
-import { UserPhoto } from "@components/UserPhoto";
-import { ScreenHeader } from "@components/ScreenHeader";
-import { Input } from "@components/Input";
-import { Button } from "@components/Button";
+import { api } from "@services/http.service";
-const AVATAR_IMG_SIZE = 33
+import { useAuth } from '@hooks/useAuth';
+
+import { AppError } from "@utils/AppError";
+
+import { ScreenHeader } from '@components/ScreenHeader';
+import { UserPhoto } from '@components/UserPhoto';
+import { Input } from '@components/Input';
+import { Button } from '@components/Button';
+
+import defaulUserPhotoImg from '@assets/userPhotoDefault.png';
+
+const PHOTO_SIZE = 33;
+
+type FormDataProps = {
+ name: string;
+ email: string;
+ password: string;
+ old_password: string;
+ confirm_password: string;
+}
+
+const profileSchema = yup.object({
+ name: yup
+ .string()
+ .required('Informe o nome'),
+ password: yup
+ .string()
+ .min(6, 'A senha deve ter pelo menos 6 dígitos.')
+ .nullable()
+ .transform((value) => !!value ? value : null),
+ confirm_password: yup
+ .string()
+ .nullable()
+ .transform((value) => !!value ? value : null)
+ .oneOf([yup.ref('password'), null], 'A confirmação de senha não confere.')
+ .when('password', {
+ is: (Field: any) => Field,
+ then: (schema) => yup
+ .string()
+ .nullable()
+ .required('Informe a confirmação da senha.')
+ .oneOf([yup.ref('password')], 'A confirmação de senha não confere.')
+ .transform((value) => !!value ? value : null)
+ })
+})
export function Profile() {
- const [photoIsLoading, setPhotoIsLoading] = useState(true)
- const [userPhoto, setUserPhoto] = useState('https://github.com/CarlosAlbertoTI.png')
-
- async function handleUserPhotoSelect() {
- try {
- setPhotoIsLoading(true)
- const photoSelected = await ImagePicker.launchImageLibraryAsync({
- mediaTypes: ImagePicker.MediaTypeOptions.Images,
- quality: 1,
- aspect: [4, 4],
- allowsEditing: true
- })
-
- if (photoSelected.canceled) {
- return;
- }
-
- if (photoSelected.assets[0].uri) {
-
- const photoInfo = await FileSystem.getInfoAsync(photoSelected.assets[0].uri)
-
- if(photoInfo.size && ((photoInfo.size / 1024 / 1024) > 3)){
- return Toast.show({
- title:'Essa imagem é muito grande. Escolha uma de até 3',
- placement:'top',
- bgColor:'red.500'
- })
- }
- setUserPhoto(photoSelected.assets[0].uri)
- }
+ const [isUpdate, setIsUpdate] = useState(false)
+ const [photoIsLoading, setPhotoIsLoading] = useState(false);
+ const [userPhoto, setUserPhoto] = useState('https://github.com/rodrigorgtic.png');
+
+ const toast = useToast();
+ const { user, updateUserProfile } = useAuth();
+ const { control, handleSubmit, formState: { errors } } = useForm({
+ defaultValues: {
+ name: user.name,
+ email: user.email
+ },
+ resolver: yupResolver(profileSchema)
+ });
+
+ async function handleUserPhotoSelected() {
+ setPhotoIsLoading(true);
+
+ try {
+ const photoSelected = await ImagePicker.launchImageLibraryAsync({
+ mediaTypes: ImagePicker.MediaTypeOptions.Images,
+ quality: 1,
+ aspect: [4, 4],
+ allowsEditing: true,
+ });
+
+ if (photoSelected.canceled) {
+ return;
+ }
- } catch (error) {
- console.info(error)
- } finally {
- setPhotoIsLoading(false)
+ if (photoSelected.assets![0].uri) {
+
+ const photoInfo = await FileSystem.getInfoAsync(photoSelected.assets![0].uri);
+
+ if (photoInfo.size && (photoInfo.size / 1024 / 1024) > 5) {
+
+ return toast.show({
+ title: 'Essa imagem é muito grande. Escolha uma de até 5MB.',
+ placement: 'top',
+ bgColor: 'red.500'
+ })
}
+
+ const fileExtension = photoSelected.assets![0].uri.split(".").pop()
+ const photoFile = {
+ name: `${user.name}.${fileExtension}`.toLowerCase(),
+ uri: photoSelected.assets![0].uri,
+ type: `${photoSelected.assets![0].type}`
+ } as any;
+
+ const userPhotoUploadForm = new FormData();
+
+ userPhotoUploadForm.append('avatar', photoFile);
+
+ const avatarUpdatedResponse = await api.patch('/users/avatar', userPhotoUploadForm, {
+ headers: {
+ 'Content-Type': 'multipart/form-data'
+ }
+ });
+
+ const userUpdated = user
+ userUpdated.avatar = avatarUpdatedResponse.data.avatar
+ updateUserProfile(userUpdated)
+
+ toast.show({
+ title: 'Foto atualizada!',
+ placement: 'top',
+ bgColor: 'green.500'
+ })
+
+ }
+
+ } catch (error) {
+ console.log(error)
+ } finally {
+ setPhotoIsLoading(false)
}
+ }
+
+ async function handleProfileUpdate(data: FormDataProps) {
+ try {
+ setIsUpdate(true)
+
+ const userUpdated = user
+ userUpdated.name = data.name
+
+ await api.put('/users', data)
+
+ await updateUserProfile(userUpdated)
+
+
+ toast.show({
+ title: 'Perfil atualizado com sucesso',
+ placement: 'top',
+ bgColor: 'green.500'
+ })
+ } catch (error) {
+ const isAppError = error instanceof AppError
+ const title = isAppError ? error.message : 'Não foi possível atualizar os dados do usuário. Por favor tente novamente mais tarde'
+
+ toast.show({
+ title: title,
+ placement: 'top',
+ bgColor: 'red.500'
+ })
+ }
+ finally {
+ setIsUpdate(false)
+ }
+ }
+
+ return (
+
+
+
+
+
+ {
+ photoIsLoading ?
+
+ :
+
+ }
+
+
+
+ Alterar Foto
+
+
+
+ (
+
+ )}
+ />
+
+ (
+
+ )}
+ />
+
+
+
+
+ Alterar senha
+
+
+ (
+
+ )}
+ />
+
+ (
+
+ )}
+ />
+
+ (
+
+ )}
+ />
- return (
-
-
-
-
- {
- photoIsLoading ?
-
- :
-
- }
-
-
- Alterar Foto
-
-
-
-
-
-
-
-
-
- Alterar senha
-
-
-
-
-
-
-
-
-
- )
+
+
+
+
+ );
}
\ No newline at end of file
diff --git a/src/screens/SignUp.tsx b/src/screens/SignUp.tsx
index 35b244a..da917ce 100644
--- a/src/screens/SignUp.tsx
+++ b/src/screens/SignUp.tsx
@@ -1,16 +1,23 @@
-import { Platform, ScrollView } from "react-native";
-import { Text, Image, VStack, Center, Heading, TextArea } from "native-base";
+import { useState } from "react";
+import { Alert, Platform, ScrollView } from "react-native";
+import { Text, Image, VStack, Center, Heading, TextArea, useToast } from "native-base";
import { useNavigation } from "@react-navigation/native";
import { useForm, Controller } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
import * as yup from 'yup'
+
+import { api } from "../services/http.service";
+
import LogoSvg from '@assets/logo.svg'
import BackgroundImage from '@assets/background.png'
import { Input } from "@components/Input";
import { Button } from "@components/Button";
+import { AppError } from "@utils/AppError";
+
+import { useAuth } from "@hooks/useAuth";
interface FormDataProps {
name: string,
@@ -27,6 +34,9 @@ const signUpSchema = yup.object({
})
export function SignUp() {
+ const [isLoading, setIsLoading] = useState(false)
+ const toast = useToast()
+ const { singIn } = useAuth()
const navigation = useNavigation()
const { control, handleSubmit, formState: { errors } } = useForm({
@@ -38,16 +48,34 @@ export function SignUp() {
}
async function handleSignUp({ name, email, password, confirmPassword }: FormDataProps) {
- const request = await fetch('http://localhost:3333/users', {
- method: 'POST',
- headers: {
- 'Accept': 'Application/json',
- 'Content-type': 'Application/json'
- },
- body: JSON.stringify({ name, email, password })
- })
- const data = await request.json()
- console.info(data)
+
+ try {
+ setIsLoading(true)
+ await api.post('/users', { name, email, password })
+ await singIn(email, password)
+
+ } catch (error) {
+ setIsLoading(false)
+ const isAppError = error instanceof AppError
+ const title = isAppError ? error.message : 'Não foi possível criar a conta. Tente novamente mais tarde.'
+
+ toast.show({
+ title,
+ placement: 'top',
+ bgColor: 'red.500'
+ })
+ }
+
+
+ // const request = await fetch('/users', {
+ // method: 'POST',
+ // headers: {
+ // 'Accept': 'Application/json',
+ // 'Content-type': 'Application/json'
+ // },
+ // body: JSON.stringify({ name, email, password })
+ // })
+ // const data = await request.json()
}
return (
@@ -131,6 +159,7 @@ export function SignUp() {
/>
diff --git a/src/screens/Signin.tsx b/src/screens/Signin.tsx
index 0f974ca..41b2bc2 100644
--- a/src/screens/Signin.tsx
+++ b/src/screens/Signin.tsx
@@ -1,15 +1,32 @@
+import { useState } from "react";
+import { Controller, useForm } from "react-hook-form";
import { useNavigation } from "@react-navigation/native";
-import { VStack, Image, Text, Center, Heading, ScrollView } from "native-base";
+import { VStack, Image, Text, Center, Heading, ScrollView, useToast } from "native-base";
import { AuthNavigatorRoutesProps } from '@routes/auth.routes'
+import { useAuth } from '@hooks/useAuth'
+
import LogoSvg from '@assets/logo.svg';
import BackgroundImg from '@assets/background.png';
import { Input } from "@components/Input";
import { Button } from "@components/Button";
+import { AppError } from "@utils/AppError";
+
+interface FormData {
+ email: string;
+ password: string;
+}
+
export function SignIn() {
+ const [loading, setLoading] = useState(false)
+ const toast = useToast()
+
+ const { singIn } = useAuth()
+
+ const { control, handleSubmit, formState: { errors } } = useForm()
const navigation = useNavigation()
@@ -17,6 +34,24 @@ export function SignIn() {
navigation.navigate('signUp')
}
+ async function handleSignIn({ email, password }: FormData) {
+ try {
+ setLoading(true)
+ await singIn(email, password)
+ } catch (error) {
+ const isAppError = error instanceof AppError
+
+ const title = isAppError ? error.message : 'Não foi possível logar. Tente novamente mais tarde'
+
+ setLoading(false)
+ toast.show({
+ title,
+ placement: 'top',
+ bgColor: 'red.500'
+ })
+ }
+ }
+
return (
@@ -41,18 +76,36 @@ export function SignIn() {
Acesse a conta
- (
+
+ )}
/>
- (
+
+ )}
/>
-
+
diff --git a/src/service/http.service.ts b/src/service/http.service.ts
deleted file mode 100644
index 801ca53..0000000
--- a/src/service/http.service.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import axios from "axios";
-
-export const api = axios.create({
- baseURL:'http://localhost:3333',
-})
\ No newline at end of file
diff --git a/src/services/http.service.ts b/src/services/http.service.ts
new file mode 100644
index 0000000..4afd1dc
--- /dev/null
+++ b/src/services/http.service.ts
@@ -0,0 +1,129 @@
+import {
+ storageAuthTokenGet,
+ storageAuthTokenSave,
+} from "@storage/storageAuthToken";
+import { AppError } from "../utils/AppError";
+import axios, { AxiosError, AxiosInstance } from "axios";
+
+type SignOut = () => void;
+
+type PromiseType = {
+ onSuccess: (token: string) => void;
+ onFailure: (error: AxiosError) => void;
+};
+
+type APIInstanceProps = AxiosInstance & {
+ registerInterceptTokenManager: (signOut: SignOut) => () => void;
+};
+
+const api = axios.create({
+ baseURL: "http://localhost:3333",
+}) as APIInstanceProps;
+
+// api.interceptors.request.use(
+// (config) => {
+// console.info("INTERCEPTIOR => ", config);
+// return config;
+// },
+// (error) => {
+// return Promise.reject(error);
+// }
+// );
+
+let failedQueue: Array = [];
+let isRefreshing = false;
+
+api.registerInterceptTokenManager = (signOut) => {
+ const interceptTokenManager = api.interceptors.response.use(
+ (response) => response,
+ async (requestError) => {
+ if (requestError?.response?.status === 401) {
+ if (
+ requestError.response.data.message === "token.expired" ||
+ requestError.response.data.message === "token.invalid"
+ ) {
+ const { refresh_token } = await storageAuthTokenGet();
+
+ if (!refresh_token) {
+ signOut();
+ return Promise.reject(requestError);
+ }
+
+ const originalRequestConfig = requestError.config;
+
+ if (isRefreshing) {
+ return new Promise((resolve, reject) => {
+ failedQueue.push({
+ onSuccess: (token) => {
+ originalRequestConfig.headers = {
+ Authorization: `Bearer ${token}`,
+ };
+ resolve(api(originalRequestConfig));
+ },
+ onFailure: (error: AxiosError) => {
+ reject(error);
+ },
+ });
+ });
+ }
+
+ isRefreshing = true;
+ return new Promise(async (resolve, reject) => {
+ try {
+ const { data } = await api.post("/sessions/refresh-token", {
+ refresh_token,
+ });
+ await storageAuthTokenSave({
+ token: data.token,
+ refresh_token: refresh_token,
+ });
+
+ if (originalRequestConfig.data) {
+ originalRequestConfig.headers = JSON.parse(
+ originalRequestConfig.data
+ );
+ }
+
+ originalRequestConfig.headers = {
+ Authorization: `Bearer ${data.token}`,
+ };
+ api.defaults.headers.common[
+ "Authorization"
+ ] = `Bearer ${data.token}`;
+
+ failedQueue.forEach((request) => {
+ request.onSuccess(data.token);
+ });
+ console.log("=====> TOKEN ATUALIZADO");
+
+ resolve(api(originalRequestConfig));
+ } catch (error: any) {
+ failedQueue.forEach((request) => {
+ request.onFailure(error);
+ });
+
+ signOut();
+ reject(error);
+ } finally {
+ isRefreshing = false;
+ failedQueue = [];
+ }
+ });
+ }
+ signOut();
+ }
+
+ if (requestError.response && requestError.response.data) {
+ return Promise.reject(new AppError(requestError.response.data.message));
+ } else {
+ return Promise.reject(requestError);
+ }
+ }
+ );
+
+ return () => {
+ api.interceptors.response.eject(interceptTokenManager);
+ };
+};
+
+export { api };
diff --git a/src/storage/storageAuthToken.ts b/src/storage/storageAuthToken.ts
new file mode 100644
index 0000000..fedbf57
--- /dev/null
+++ b/src/storage/storageAuthToken.ts
@@ -0,0 +1,31 @@
+import AsyncStorage from "@react-native-async-storage/async-storage";
+
+import { AUTH_STORAGE } from "@storage/storageConfig";
+
+type StorageAuthTokenProps = {
+ token: string;
+ refresh_token: string;
+};
+
+export async function storageAuthTokenSave({
+ token,
+ refresh_token,
+}: StorageAuthTokenProps) {
+ await AsyncStorage.setItem(
+ AUTH_STORAGE,
+ JSON.stringify({ token, refresh_token })
+ );
+}
+
+export async function storageAuthTokenGet() {
+ const response = await AsyncStorage.getItem(AUTH_STORAGE);
+ const { token, refresh_token }: StorageAuthTokenProps = response
+ ? JSON.parse(response)
+ : {};
+
+ return { token, refresh_token };
+}
+
+export async function storageAuthTokenRemove() {
+ await AsyncStorage.removeItem(AUTH_STORAGE);
+}
diff --git a/src/storage/storageConfig.ts b/src/storage/storageConfig.ts
new file mode 100644
index 0000000..2a81ec1
--- /dev/null
+++ b/src/storage/storageConfig.ts
@@ -0,0 +1,4 @@
+const USER_STORAGE = '@ignitegym:user';
+ const AUTH_STORAGE = '@ignitegym:token';
+
+ export { USER_STORAGE, AUTH_STORAGE };
\ No newline at end of file
diff --git a/src/storage/storageUser.ts b/src/storage/storageUser.ts
new file mode 100644
index 0000000..d0f3af0
--- /dev/null
+++ b/src/storage/storageUser.ts
@@ -0,0 +1,20 @@
+import AsyncStorage from "@react-native-async-storage/async-storage";
+import { UserDTO } from "@dtos/UserDTO";
+
+import { USER_STORAGE } from "@storage/storageConfig";
+
+export async function storageUserSave(user: UserDTO) {
+ await AsyncStorage.setItem(USER_STORAGE, JSON.stringify(user));
+}
+
+export async function storageUserGet() {
+ const storage = await AsyncStorage.getItem(USER_STORAGE);
+
+ const user: UserDTO = storage ? JSON.parse(storage) : {};
+
+ return user;
+}
+
+export async function storageUserRemove() {
+ await AsyncStorage.removeItem(USER_STORAGE);
+}
diff --git a/src/utils/AppError.ts b/src/utils/AppError.ts
new file mode 100644
index 0000000..56da958
--- /dev/null
+++ b/src/utils/AppError.ts
@@ -0,0 +1,7 @@
+export class AppError {
+ message: string;
+
+ constructor(message: string) {
+ this.message = message;
+ }
+}