From 9416bc0f9b7b566aae97e06d5f3a3cfec23f709c Mon Sep 17 00:00:00 2001 From: Nicolas Villamizar Date: Wed, 15 Jan 2025 13:29:06 -0500 Subject: [PATCH] feat(sign-up): add sign up screen & form --- src/api/auth/use-login.ts | 8 +-- src/api/auth/use-sign-up.ts | 43 +++++++++++++++ src/api/common/interceptors.ts | 42 ++++++++++++-- src/app/_layout.tsx | 4 ++ src/app/sign-in.tsx | 10 ++-- src/app/sign-up.tsx | 30 ++++++++++ src/components/login-form.tsx | 16 +++++- src/components/sign-up-form.tsx | 97 +++++++++++++++++++++++++++++++++ src/core/auth/utils.tsx | 5 +- 9 files changed, 236 insertions(+), 19 deletions(-) create mode 100644 src/api/auth/use-sign-up.ts create mode 100644 src/app/sign-up.tsx create mode 100644 src/components/sign-up-form.tsx diff --git a/src/api/auth/use-login.ts b/src/api/auth/use-login.ts index 8c54cb73..61d22d11 100644 --- a/src/api/auth/use-login.ts +++ b/src/api/auth/use-login.ts @@ -21,13 +21,7 @@ const login = async (variables: Variables) => { url: '/v1/users/sign_in', method: 'POST', data: { - user: { - email: variables.email, - password: variables.password, - }, - }, - headers: { - 'Content-Type': 'application/json', + user: variables, }, }); return data; diff --git a/src/api/auth/use-sign-up.ts b/src/api/auth/use-sign-up.ts new file mode 100644 index 00000000..469e4b3d --- /dev/null +++ b/src/api/auth/use-sign-up.ts @@ -0,0 +1,43 @@ +import { createMutation } from 'react-query-kit'; + +import { client } from '../common'; + +type Variables = { + email: string; + name: string; + password: string; + passwordConfirmation: string; +}; + +type Response = { + status: string; + data: { + id: string; + email: string; + name: string; + provider: string; + uid: string; + allowPasswordChange: boolean; + createdAt: string; + updatedAt: string; + nickname?: string; + image?: string; + birthday?: string; + }; +}; + +const signUp = async (variables: Variables) => { + const { data } = await client({ + url: '/v1/users', + method: 'POST', + data: { + user: variables, + }, + }); + + return data; +}; + +export const useSignUp = createMutation({ + mutationFn: (variables) => signUp(variables), +}); diff --git a/src/api/common/interceptors.ts b/src/api/common/interceptors.ts index 82fc601e..cf7b6791 100644 --- a/src/api/common/interceptors.ts +++ b/src/api/common/interceptors.ts @@ -1,25 +1,59 @@ import type { AxiosError, InternalAxiosRequestConfig } from 'axios'; -import { useAuth } from '@/core'; +import { signIn, useAuth } from '@/core'; import { client } from './client'; import { toCamelCase, toSnakeCase } from './utils'; +const ACCESS_TOKEN = 'access-token'; +const CLIENT_HEADER = 'client'; +const UID_HEADER = 'uid'; +const EXPIRY_HEADER = 'expiry'; +const AUTHORIZATION_HEADER = 'Authorization'; + +const CONTENT_TYPE = 'Content-Type'; +const MULTIPART_FORM_DATA = 'multipart/form-data'; + export default function interceptors() { - const token = useAuth.getState().token; client.interceptors.request.use((config: InternalAxiosRequestConfig) => { - if (config.data) { + const token = useAuth.getState().token; + + const { headers, data } = config; + + if (headers && headers[CONTENT_TYPE] !== MULTIPART_FORM_DATA && data) { config.data = toSnakeCase(config.data); } + if (token) { - config.headers.Authorization = `Bearer ${token}`; + const { access, client: _client, uid, bearer, expiry } = token; + + config.headers[AUTHORIZATION_HEADER] = bearer; + config.headers[ACCESS_TOKEN] = access; + config.headers[CLIENT_HEADER] = _client; + config.headers[UID_HEADER] = uid; + config.headers[EXPIRY_HEADER] = expiry; } + return config; }); client.interceptors.response.use( (response) => { + const { data, headers } = response; response.data = toCamelCase(response.data); + + const token = headers[ACCESS_TOKEN]; + const _client = headers[CLIENT_HEADER]; + const uid = headers[UID_HEADER]; + const expiry = headers[EXPIRY_HEADER]; + const bearer = headers[AUTHORIZATION_HEADER]; + + if (token) { + signIn({ access: token, client: _client, uid, expiry, bearer }); + } + + response.data = toCamelCase(data); + return response; }, (error: AxiosError) => Promise.reject(error), diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx index 391f68ef..19915def 100644 --- a/src/app/_layout.tsx +++ b/src/app/_layout.tsx @@ -35,6 +35,10 @@ export default function RootLayout() { + { - signIn({ access: data.accessToken, refresh: data.refreshToken }); + + const { mutate: login, isPending } = useLogin({ + onSuccess: () => { router.push('/'); }, onError: (error) => showMessage({ message: error.message, type: 'danger' }), @@ -25,7 +23,7 @@ export default function Login() { return ( <> - + ); } diff --git a/src/app/sign-up.tsx b/src/app/sign-up.tsx new file mode 100644 index 00000000..b4d9a961 --- /dev/null +++ b/src/app/sign-up.tsx @@ -0,0 +1,30 @@ +import { useRouter } from 'expo-router'; +import React from 'react'; +import { showMessage } from 'react-native-flash-message'; + +import { useSignUp } from '@/api/auth/use-sign-up'; +import type { SignUpFormProps } from '@/components/sign-up-form'; +import { SignUpForm } from '@/components/sign-up-form'; +import { FocusAwareStatusBar } from '@/ui'; + +export default function SignIn() { + const router = useRouter(); + + const { mutate: signUp, isPending } = useSignUp({ + onSuccess: () => { + router.push('/'); + }, + onError: (error) => showMessage({ message: error.message, type: 'danger' }), + }); + + const onSubmit: SignUpFormProps['onSubmit'] = (data) => { + signUp(data); + }; + + return ( + <> + + + + ); +} diff --git a/src/components/login-form.tsx b/src/components/login-form.tsx index 7ad35c85..06e54d6a 100644 --- a/src/components/login-form.tsx +++ b/src/components/login-form.tsx @@ -1,4 +1,5 @@ import { zodResolver } from '@hookform/resolvers/zod'; +import { Link } from 'expo-router'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import { KeyboardAvoidingView } from 'react-native-keyboard-controller'; @@ -21,10 +22,14 @@ const schema = z.object({ export type FormType = z.infer; export type LoginFormProps = { + isLoading?: boolean; onSubmit?: SubmitHandler; }; -export const LoginForm = ({ onSubmit = () => {} }: LoginFormProps) => { +export const LoginForm = ({ + onSubmit = () => {}, + isLoading = false, +}: LoginFormProps) => { const { handleSubmit, control } = useForm({ resolver: zodResolver(schema), }); @@ -55,11 +60,20 @@ export const LoginForm = ({ onSubmit = () => {} }: LoginFormProps) => { placeholder="***" secureTextEntry={true} /> +