Fakultas Ilmu Komputer UI
Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
ppl-fasilkom-ui
2021
Kelas D
PT Gizi Sehat - Dietela
Dietela Mobile
Commits
94558afb
Commit
94558afb
authored
Apr 26, 2021
by
Doan Andreas Nathanael
Committed by
Muzaki Azami
Apr 26, 2021
Browse files
Login Form UI & API Integration
parent
6f0e02cb
Changes
13
Hide whitespace changes
Inline
Side-by-side
package.json
View file @
94558afb
...
...
@@ -7,7 +7,7 @@
"ios"
:
"react-native run-ios"
,
"start"
:
"react-native start"
,
"test"
:
"jest --coverage --watchAll=false --verbose --collectCoverageFrom=
\"
src/**/*.tsx
\"
"
,
"test-only"
:
"jest -t"
,
"test-only"
:
"jest
--verbose
-t"
,
"lint"
:
"eslint . --ext .ts,.tsx --fix"
,
"prettify"
:
"prettier --write src"
,
"prep"
:
"npx mrm lint-staged"
,
...
...
src/__mocks__/auth.ts
View file @
94558afb
...
...
@@ -14,6 +14,16 @@ export const invalidRegistrationValues: { [_: string]: any } = {
password2
:
'
12345678
'
,
};
export
const
validLoginValues
:
{
[
_
:
string
]:
any
}
=
{
email
:
'
doan@dietela.com
'
,
password
:
'
g8ake1afig
'
,
};
export
const
invalidLoginValues
:
{
[
_
:
string
]:
any
}
=
{
email
:
'
doan
'
,
password
:
'
12345678
'
,
};
export
const
authResponse
:
LoginResponse
=
{
access_token
:
'
ax41faf
'
,
refresh_token
:
'
9tka0kfa
'
,
...
...
src/app/index.tsx
View file @
94558afb
...
...
@@ -19,7 +19,7 @@ const App: FC = () => {
<
ContextProvider
>
<
NavigationContainer
>
<
Stack
.
Navigator
initialRouteName
=
{
ROUTES
.
registration
}
initialRouteName
=
{
ROUTES
.
initial
}
screenOptions
=
{
screenOptions
}
>
{
navigation
.
map
((
nav
,
i
)
=>
(
<
Stack
.
Screen
...
...
src/provider/UserContext/index.ts
View file @
94558afb
...
...
@@ -4,8 +4,13 @@ import { GoogleSignin } from '@react-native-google-signin/google-signin';
import
{
Toast
}
from
'
components/core
'
;
import
CACHE_KEYS
from
'
constants/cacheKeys
'
;
import
{
removeCache
,
getCache
,
setCache
}
from
'
utils/cache
'
;
import
{
googleLoginApi
,
signupApi
}
from
'
services/auth
'
;
import
{
User
,
RegistrationRequest
}
from
'
services/auth/models
'
;
import
{
googleLoginApi
,
loginApi
,
signupApi
}
from
'
services/auth
'
;
import
{
User
,
RegistrationRequest
,
LoginRequest
,
LoginResponse
,
}
from
'
services/auth/models
'
;
import
{
set401Callback
,
setAuthHeader
,
resetAuthHeader
}
from
'
services/api
'
;
import
{
iUserContext
}
from
'
./types
'
;
...
...
@@ -16,6 +21,19 @@ const initialUser = {
name
:
''
,
};
const
setUserFromResponse
=
async
(
success
:
boolean
,
setUser
:
React
.
Dispatch
<
React
.
SetStateAction
<
User
>>
,
data
?:
LoginResponse
,
)
=>
{
if
(
success
&&
data
)
{
await
setCache
(
CACHE_KEYS
.
authToken
,
data
.
access_token
);
await
setCache
(
CACHE_KEYS
.
refreshToken
,
data
.
refresh_token
);
setUser
(
data
.
user
);
}
};
export
const
UserContext
=
createContext
<
iUserContext
>
({
user
:
initialUser
,
isAuthenticated
:
false
,
...
...
@@ -40,19 +58,15 @@ export const useUserContext = (): iUserContext => {
const
signup
=
async
(
registerData
:
RegistrationRequest
)
=>
{
const
response
=
await
signupApi
(
registerData
);
if
(
response
.
success
&&
response
.
data
)
{
await
setCache
(
CACHE_KEYS
.
authToken
,
response
.
data
?.
access_token
);
await
setCache
(
CACHE_KEYS
.
refreshToken
,
response
.
data
?.
refresh_token
);
setUser
(
response
.
data
.
user
);
}
await
setUserFromResponse
(
response
.
success
,
setUser
,
response
.
data
);
return
response
;
};
// TODO
const
login
=
async
()
=>
{};
const
login
=
async
(
loginData
:
LoginRequest
)
=>
{
const
response
=
await
loginApi
(
loginData
);
await
setUserFromResponse
(
response
.
success
,
setUser
,
response
.
data
);
return
response
;
};
const
logout
=
useCallback
(
async
()
=>
{
await
GoogleSignin
.
signOut
();
...
...
src/provider/UserContext/types.ts
View file @
94558afb
import
{
ApiResponse
}
from
'
services/api
'
;
import
{
LoginResponse
,
RegistrationRequest
,
User
}
from
'
services/auth/models
'
;
import
{
LoginRequest
,
LoginResponse
,
RegistrationRequest
,
User
,
}
from
'
services/auth/models
'
;
export
interface
iUserContext
{
user
:
User
;
isAuthenticated
:
boolean
;
isLoading
:
boolean
;
signup
:
(
data
:
RegistrationRequest
)
=>
ApiResponse
<
LoginResponse
>
;
login
:
(
)
=>
Promise
<
void
>
;
login
:
(
data
:
LoginRequest
)
=>
ApiResponse
<
LoginResponse
>
;
loginWithGoogle
:
()
=>
Promise
<
void
>
;
logout
:
()
=>
Promise
<
void
>
;
}
src/scenes/auth/Login/index.test.tsx
View file @
94558afb
import
React
from
'
react
'
;
import
{
render
}
from
'
utils/testing
'
;
import
{
render
,
fireEvent
,
waitFor
}
from
'
utils/testing
'
;
import
*
as
ROUTES
from
'
constants/routes
'
;
import
axios
from
'
axios
'
;
import
Login
from
'
.
'
;
import
{
authResponse
,
invalidLoginValues
,
validLoginValues
,
}
from
'
__mocks__/auth
'
;
import
{
textField
}
from
'
./schema
'
;
jest
.
mock
(
'
react-native-toast-message
'
);
jest
.
mock
(
'
axios
'
);
const
mockAxios
=
axios
as
jest
.
Mocked
<
typeof
axios
>
;
describe
(
'
Login page
'
,
()
=>
{
it
(
'
renders correctly
'
,
()
=>
{
render
(<
Login
/>,
ROUTES
.
login
);
});
it
(
'
success when field is valid and submit success
'
,
async
()
=>
{
const
loginApi
=
()
=>
Promise
.
resolve
({
status
:
201
,
data
:
authResponse
,
});
mockAxios
.
request
.
mockImplementationOnce
(
loginApi
);
const
{
getByPlaceholderText
,
queryByText
,
getByTestId
}
=
render
(
<
Login
/>,
ROUTES
.
login
,
);
textField
.
map
(({
name
,
placeholder
})
=>
{
const
formField
=
getByPlaceholderText
(
placeholder
as
string
);
fireEvent
.
changeText
(
formField
,
validLoginValues
[
name
]);
});
const
loginButton
=
getByTestId
(
'
loginButton
'
);
await
waitFor
(()
=>
fireEvent
.
press
(
loginButton
));
const
toastWarning
=
queryByText
(
/Profile/i
);
expect
(
toastWarning
).
toBeTruthy
();
});
it
(
'
fails when field is invalid and submit success
'
,
async
()
=>
{
const
loginApi
=
()
=>
Promise
.
reject
({
status
:
400
,
response
:
{
data
:
'
error
'
,
},
});
mockAxios
.
request
.
mockImplementationOnce
(
loginApi
);
const
{
getByPlaceholderText
,
queryByText
,
getByTestId
}
=
render
(
<
Login
/>,
ROUTES
.
login
,
);
textField
.
map
(({
name
,
placeholder
})
=>
{
const
formField
=
getByPlaceholderText
(
placeholder
as
string
);
fireEvent
.
changeText
(
formField
,
invalidLoginValues
[
name
]);
});
const
loginButton
=
getByTestId
(
'
loginButton
'
);
await
waitFor
(()
=>
fireEvent
.
press
(
loginButton
));
const
toastWarning
=
queryByText
(
/Profile/i
);
expect
(
toastWarning
).
toBeFalsy
();
});
afterAll
(()
=>
{
jest
.
clearAllMocks
();
});
});
src/scenes/auth/Login/index.tsx
View file @
94558afb
import
React
,
{
FC
,
useContext
}
from
'
react
'
;
import
{
View
}
from
'
react-native
'
;
import
React
,
{
FC
,
useContext
,
useState
}
from
'
react
'
;
import
{
StyleSheet
,
Text
,
View
}
from
'
react-native
'
;
import
{
UserContext
}
from
'
provider
'
;
import
{
useAuthEffect
,
useForm
}
from
'
hooks
'
;
import
{
GoogleLoginButton
}
from
'
../components
'
;
import
{
BigButton
,
Toast
}
from
'
components/core
'
;
import
{
fieldValidation
,
initialValues
,
textField
}
from
'
./schema
'
;
import
{
generateValidationSchema
}
from
'
utils/form
'
;
import
{
layoutStyles
}
from
'
styles
'
;
import
{
useAuthEffect
}
from
'
hooks
'
;
import
{
TextField
}
from
'
components/form
'
;
import
{
Section
}
from
'
components/layout
'
;
const
isPasswordField
=
(
name
:
string
)
=>
name
===
'
password
'
;
const
Login
:
FC
=
()
=>
{
const
{
isLoading
,
loginWithGoogle
}
=
useContext
(
UserContext
);
const
{
login
,
isLoading
,
loginWithGoogle
}
=
useContext
(
UserContext
);
const
[
nonFieldError
,
setNonFieldError
]
=
useState
<
string
|
null
>
();
const
{
getTextInputProps
,
handleSubmit
,
isSubmitting
,
setFieldError
,
}
=
useForm
({
initialValues
,
validationSchema
:
generateValidationSchema
(
fieldValidation
),
onSubmit
:
async
(
values
)
=>
{
const
response
=
await
login
(
values
);
if
(
!
response
.
success
)
{
const
error
=
response
.
error
;
setFieldError
(
'
email
'
,
error
.
email
);
setFieldError
(
'
password
'
,
error
.
password
);
setNonFieldError
(
error
.
non_field_errors
);
Toast
.
show
({
type
:
'
error
'
,
text1
:
'
Gagal login akun
'
,
text2
:
'
Terjadi kesalahan login. Silakan coba lagi
'
,
});
}
},
});
useAuthEffect
();
return
(
<
View
style
=
{
layoutStyles
}
>
<
GoogleLoginButton
onPress
=
{
loginWithGoogle
}
isLoading
=
{
isLoading
}
/>
{
textField
.
map
(({
name
,
label
,
required
,
placeholder
},
i
)
=>
(
<
TextField
key
=
{
`field
${
i
}
`
}
label
=
{
label
}
required
=
{
required
}
placeholder
=
{
placeholder
}
{
...
getTextInputProps
(
name
)
}
secureTextEntry
=
{
isPasswordField
(
name
)
}
/>
))
}
{
nonFieldError
&&
(
<
Text
style
=
{
styles
.
nonfieldError
}
>
{
nonFieldError
}
</
Text
>
)
}
<
Section
>
<
BigButton
title
=
"login"
onPress
=
{
handleSubmit
}
loading
=
{
isSubmitting
}
testID
=
"loginButton"
/>
</
Section
>
<
Section
>
<
GoogleLoginButton
onPress
=
{
loginWithGoogle
}
isLoading
=
{
isLoading
}
/>
</
Section
>
</
View
>
);
};
const
styles
=
StyleSheet
.
create
({
nonfieldError
:
{
color
:
'
red
'
},
});
export
default
Login
;
src/scenes/auth/Login/schema.ts
0 → 100644
View file @
94558afb
import
{
LoginRequest
,
Role
}
from
'
services/auth/models
'
;
import
{
TextFieldSchema
}
from
'
types/form
'
;
import
{
FieldType
,
FieldValidation
}
from
'
utils/form
'
;
export
const
textField
:
TextFieldSchema
[]
=
[
{
label
:
'
Email address
'
,
placeholder
:
'
Masukkan email Anda
'
,
required
:
true
,
name
:
'
email
'
,
},
{
label
:
'
Password
'
,
placeholder
:
'
Masukkan password Anda
'
,
required
:
true
,
name
:
'
password
'
,
},
];
export
const
initialValues
:
LoginRequest
=
{
email
:
''
,
password
:
''
,
role
:
'
client
'
,
};
export
const
fieldValidation
:
FieldValidation
[]
=
[
{
name
:
'
email
'
,
required
:
true
,
label
:
'
Email address
'
,
type
:
FieldType
.
EMAIL
,
},
{
name
:
'
password
'
,
required
:
true
,
label
:
'
Password
'
,
type
:
FieldType
.
PASSWORD
,
},
];
export
const
setRole
=
(
role
:
Role
)
=>
(
initialValues
.
role
=
role
);
src/scenes/auth/ManualRegistrationPage/index.tsx
View file @
94558afb
...
...
@@ -3,15 +3,15 @@ import { useAuthEffect, useForm } from 'hooks';
import
{
ScrollView
}
from
'
react-native-gesture-handler
'
;
import
{
BigButton
,
Toast
}
from
'
components/core
'
;
import
{
Section
}
from
'
components/layout
'
;
import
{
TextField
}
from
'
components/form
'
;
import
{
GoogleLoginButton
}
from
'
../components
'
;
import
{
fieldValidation
,
initialValues
,
textField
}
from
'
./schema
'
;
import
{
generateValidationSchema
}
from
'
utils/form
'
;
import
{
UserContext
}
from
'
provider
'
;
import
{
layoutStyles
}
from
'
styles
'
;
import
{
GoogleLoginButton
}
from
'
../components
'
;
import
{
Section
}
from
'
components/layout
'
;
const
isPasswordField
=
(
name
:
string
)
=>
name
===
'
password1
'
||
name
===
'
password2
'
;
...
...
src/services/auth/index.ts
View file @
94558afb
...
...
@@ -3,6 +3,7 @@ import { api, RequestMethod, ApiResponse } from '../api';
import
*
as
apiUrls
from
'
./urls
'
;
import
{
GoogleLoginRequest
,
LoginRequest
,
LoginResponse
,
RegistrationRequest
,
}
from
'
./models
'
;
...
...
@@ -18,3 +19,7 @@ export const signupApi = (
):
ApiResponse
<
LoginResponse
>
=>
{
return
api
(
RequestMethod
.
POST
,
apiUrls
.
signup
,
body
);
};
export
const
loginApi
=
(
body
:
LoginRequest
):
ApiResponse
<
LoginResponse
>
=>
{
return
api
(
RequestMethod
.
POST
,
apiUrls
.
login
,
body
);
};
src/services/auth/models.ts
View file @
94558afb
...
...
@@ -9,6 +9,14 @@ export interface RegistrationRequest {
password2
:
string
;
}
export
type
Role
=
'
client
'
|
'
nutritionist
'
|
'
admin
'
;
export
interface
LoginRequest
{
email
:
string
;
password
:
string
;
role
:
Role
;
}
export
interface
User
{
id
:
number
|
null
;
email
:
string
;
...
...
src/services/auth/urls.ts
View file @
94558afb
...
...
@@ -2,3 +2,4 @@ const auth = 'auth/';
export
const
google
=
`
${
auth
}
google/`
;
export
const
signup
=
`
${
auth
}
registration/`
;
export
const
login
=
`
${
auth
}
user-login/`
;
src/types/form.ts
View file @
94558afb
...
...
@@ -2,7 +2,9 @@ import { Props as TextFieldProps } from 'components/form/TextField/types';
import
{
Props
as
FormLabelProps
}
from
'
components/form/FormLabel/types
'
;
import
{
Choice
}
from
'
components/form/MultipleChoice/types
'
;
export
type
TextFieldSchema
=
TextFieldProps
&
{
name
:
string
};
export
type
TextFieldSchema
=
TextFieldProps
&
{
name
:
string
;
};
export
interface
RadioButtonGroupSchema
extends
FormLabelProps
{
choices
:
Choice
[];
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment