Skip to content

Commit

Permalink
feat(sign-up): add sign up screen & form
Browse files Browse the repository at this point in the history
  • Loading branch information
Stukz committed Jan 14, 2025
1 parent 7c98604 commit f6a963e
Show file tree
Hide file tree
Showing 11 changed files with 8,223 additions and 11,284 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,9 @@
"eslint": "^8.57.0",
"eslint-config-expo": "^7.1.2",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.7.0",
"eslint-plugin-i18n-json": "^4.0.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.8.3",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.37.2",
Expand Down
19,255 changes: 7,985 additions & 11,270 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

3 changes: 0 additions & 3 deletions src/api/auth/use-login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ const login = async (variables: Variables) => {
password: variables.password,
},
},
headers: {
'Content-Type': 'application/json',
},
});
return data;
};
Expand Down
48 changes: 48 additions & 0 deletions src/api/auth/use-sign-up.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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: {
email: variables.email,
password: variables.password,
password_confirmation: variables.passwordConfirmation,
name: variables.name,
},
},
});

return data;
};

export const useSignUp = createMutation<Response, Variables>({
mutationFn: (variables) => signUp(variables),
});
38 changes: 35 additions & 3 deletions src/api/common/interceptors.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,57 @@
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 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 { 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 } = token;

config.headers.Authorization = `Bearer ${access}`;
config.headers[ACCESS_TOKEN] = access;
config.headers[CLIENT_HEADER] = _client;
config.headers[UID_HEADER] = uid;
config.headers[EXPIRY_HEADER] = token[EXPIRY_HEADER];
}

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];

if (token) {
signIn({ access: token, client: _client, uid, expiry });
}

response.data = toCamelCase(data);

return response;
},
(error: AxiosError) => Promise.reject(error),
Expand Down
4 changes: 4 additions & 0 deletions src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export default function RootLayout() {
<Stack.Screen name="onboarding" options={{ headerShown: false }} />
<Stack.Screen name="forgot-password" />
<Stack.Screen name="sign-in" options={{ headerShown: false }} />
<Stack.Screen
name="sign-up"
options={{ headerBackTitleVisible: false }}
/>
<Stack.Screen
name="www"
options={{
Expand Down
10 changes: 4 additions & 6 deletions src/app/sign-in.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ import { showMessage } from 'react-native-flash-message';
import { useLogin } from '@/api/auth/use-login';
import type { LoginFormProps } from '@/components/login-form';
import { LoginForm } from '@/components/login-form';
import { useAuth } from '@/core';
import { FocusAwareStatusBar } from '@/ui';

export default function Login() {
const router = useRouter();
const signIn = useAuth.use.signIn();
const { mutate: login } = useLogin({
onSuccess: (data) => {
signIn({ access: data.accessToken, refresh: data.refreshToken });

const { mutate: login, isPending } = useLogin({
onSuccess: () => {
router.push('/');
},
onError: (error) => showMessage({ message: error.message, type: 'danger' }),
Expand All @@ -25,7 +23,7 @@ export default function Login() {
return (
<>
<FocusAwareStatusBar />
<LoginForm onSubmit={onSubmit} />
<LoginForm onSubmit={onSubmit} isLoading={isPending} />
</>
);
}
30 changes: 30 additions & 0 deletions src/app/sign-up.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<FocusAwareStatusBar />
<SignUpForm onSubmit={onSubmit} isPending={isPending} />
</>
);
}
16 changes: 15 additions & 1 deletion src/components/login-form.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -21,10 +22,14 @@ const schema = z.object({
export type FormType = z.infer<typeof schema>;

export type LoginFormProps = {
isLoading?: boolean;
onSubmit?: SubmitHandler<FormType>;
};

export const LoginForm = ({ onSubmit = () => {} }: LoginFormProps) => {
export const LoginForm = ({
onSubmit = () => {},
isLoading = false,
}: LoginFormProps) => {
const { handleSubmit, control } = useForm<FormType>({
resolver: zodResolver(schema),
});
Expand Down Expand Up @@ -55,11 +60,20 @@ export const LoginForm = ({ onSubmit = () => {} }: LoginFormProps) => {
placeholder="***"
secureTextEntry={true}
/>

<Button
testID="login-button"
label="Login"
onPress={handleSubmit(onSubmit)}
loading={isLoading}
/>

<Text>
Don't have an account?{' '}
<Link href="/sign-up" disabled={isLoading}>
<Text className="font-bold text-black">Sign up</Text>
</Link>
</Text>
</View>
</KeyboardAvoidingView>
);
Expand Down
97 changes: 97 additions & 0 deletions src/components/sign-up-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { type SubmitHandler, useForm } from 'react-hook-form';
import { KeyboardAvoidingView } from 'react-native';
import z from 'zod';

import { Button, ControlledInput,Text, View } from '@/ui';

const MIN_PASSWORD_LENGTH = 6;

const passwordSchema = z
.string({ required_error: 'Password is required' })
.min(MIN_PASSWORD_LENGTH, 'Password must be at least 6 characters');

const schema = z
.object({
email: z
.string({ required_error: 'Email is required' })
.email('Invalid email format'),
name: z.string({ required_error: 'Name is required' }),
password: passwordSchema,
passwordConfirmation: z.string({
required_error: 'Password confirmation is required',
}),
})
.refine((data) => data.password === data.passwordConfirmation, {
message: 'Passwords do not match',
path: ['passwordConfirmation'],
});

export type FormType = z.infer<typeof schema>;

export type SignUpFormProps = {
onSubmit?: SubmitHandler<FormType>;
isPending?: boolean;
};

export const SignUpForm = ({
onSubmit = () => {},
isPending = false,
}: SignUpFormProps) => {
const { handleSubmit, control } = useForm<FormType>({
resolver: zodResolver(schema),
});

return (
<KeyboardAvoidingView
className="flex-1"
behavior="padding"
keyboardVerticalOffset={10}
>
<View className="flex-1 justify-center p-4">
<Text testID="form-title" className="pb-6 text-center text-2xl">
Sign Up
</Text>

<ControlledInput
testID="email-input"
autoCapitalize="none"
autoComplete="email"
control={control}
name="email"
label="Email"
/>
<ControlledInput
testID="name-input"
control={control}
name="name"
label="Name"
/>
<ControlledInput
testID="password-input"
control={control}
name="password"
label="Password"
placeholder="***"
secureTextEntry={true}
/>
<ControlledInput
testID="password-confirmation-input"
control={control}
name="passwordConfirmation"
label="Password Confirmation"
placeholder="***"
secureTextEntry={true}
/>

<Button
testID="sign-up-button"
label="Sign Up"
onPress={handleSubmit(onSubmit)}
loading={isPending}
disabled={isPending}
/>
</View>
</KeyboardAvoidingView>
);
};
4 changes: 3 additions & 1 deletion src/core/auth/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ const TOKEN = 'token';

export type TokenType = {
access: string;
refresh: string;
client: string;
uid: string;
expiry: string;
};

export const getToken = () => getItem<TokenType>(TOKEN);
Expand Down

0 comments on commit f6a963e

Please sign in to comment.