Skip to content

Commit

Permalink
Merge pull request #2 from nijuyonkadesu/login-page
Browse files Browse the repository at this point in the history
Login page
  • Loading branch information
nijuyonkadesu authored Jul 9, 2023
2 parents ef0a026 + 1840707 commit 0411d0e
Show file tree
Hide file tree
Showing 20 changed files with 446 additions and 7 deletions.
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".DeskApplication"
android:allowBackup="true"
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/java/one/njk/celestidesk/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ class MainActivity : AppCompatActivity() {
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment_content_main) as NavHostFragment
val navController = navHostFragment.navController
appBarConfiguration = AppBarConfiguration(navController.graph)
// Top level Navigation destination do not have <- icon
appBarConfiguration = AppBarConfiguration(setOf(R.id.loginFragment, R.id.requestFragment))
setupActionBarWithNavController(navController, appBarConfiguration)

binding.fab.setOnClickListener { view ->
Expand Down
15 changes: 15 additions & 0 deletions app/src/main/java/one/njk/celestidesk/data/RolesDataStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.first
import one.njk.celestidesk.data.auth.model.TokenResponse

enum class Role {
EMPLOYEE, TEAM_LEAD, MANAGER
Expand All @@ -15,6 +16,7 @@ class RolesDataStore(private val context: Context) {
companion object {
private val Context.rolesDataStore by preferencesDataStore(name = "roles")
private val ROLE = stringPreferencesKey("ROLE")
private val TOKEN = stringPreferencesKey("TOKEN")
}

suspend fun setRole(role: Role) {
Expand All @@ -27,4 +29,17 @@ class RolesDataStore(private val context: Context) {
val role = context.rolesDataStore.data.first()[ROLE]
return role?.let { Role.valueOf(it) } ?: Role.EMPLOYEE
}

suspend fun getToken(): TokenResponse {
return TokenResponse(
"local copy",
context.rolesDataStore.data.first()[TOKEN] ?: ""
)
}

suspend fun setToken(tokenResponse: TokenResponse) {
context.rolesDataStore.edit {
it[TOKEN] = tokenResponse.token
}
}
}
23 changes: 23 additions & 0 deletions app/src/main/java/one/njk/celestidesk/data/auth/AuthApi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package one.njk.celestidesk.data.auth

import one.njk.celestidesk.data.auth.model.AuthLoginRequest
import one.njk.celestidesk.data.auth.model.AuthSignUpRequest
import one.njk.celestidesk.data.auth.model.TokenResponse
import retrofit2.http.Body
import retrofit2.http.POST


interface AuthApi {

@POST("signup")
suspend fun signUp(
@Body request: AuthSignUpRequest
): TokenResponse

@POST("login")
suspend fun logIn(
@Body request: AuthLoginRequest
): TokenResponse

// TODO: Use interceptor for multiple authentication routes
}
10 changes: 10 additions & 0 deletions app/src/main/java/one/njk/celestidesk/data/auth/AuthRepository.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package one.njk.celestidesk.data.auth

import one.njk.celestidesk.data.auth.model.AuthLoginRequest
import one.njk.celestidesk.data.auth.model.AuthResult
import one.njk.celestidesk.data.auth.model.AuthSignUpRequest

interface AuthRepository {
suspend fun signUp(user: AuthSignUpRequest): AuthResult<Unit>
suspend fun logIn(user: AuthLoginRequest): AuthResult<Unit>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package one.njk.celestidesk.data.auth

import android.util.Log
import one.njk.celestidesk.data.RolesDataStore
import one.njk.celestidesk.data.auth.model.AuthLoginRequest
import one.njk.celestidesk.data.auth.model.AuthResult
import one.njk.celestidesk.data.auth.model.AuthSignUpRequest
import retrofit2.HttpException

class AuthRepositoryImpl(
private val api: AuthApi,
private val pref: RolesDataStore
): AuthRepository {
override suspend fun signUp(user: AuthSignUpRequest): AuthResult<Unit> {
return try {
val response = api.signUp(user)
pref.setToken(response)
AuthResult.Authorized()
} catch (e: HttpException) {
Log.d("network", e.message.toString())

if(e.code() == 401) AuthResult.UnAuthorized()
else AuthResult.UnknownError()
} catch (e: Exception) {
AuthResult.UnknownError()
}
}

override suspend fun logIn(user: AuthLoginRequest): AuthResult<Unit> {
return try {
val response = api.logIn(user)
pref.setToken(response)
Log.d("network", response.message)
AuthResult.Authorized()
} catch (e: HttpException) {
Log.d("network", e.message.toString())

if(e.code() == 401) AuthResult.UnAuthorized()
else AuthResult.UnknownError()
} catch (e: Exception) {
Log.d("network", e.message.toString())
AuthResult.UnknownError()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package one.njk.celestidesk.data.auth.model

data class AuthLoginRequest(
val username: String,
val password: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package one.njk.celestidesk.data.auth.model

sealed class AuthResult<T> (val data: T? = null){
class Authorized<T>(data: T? = null): AuthResult<T>(data)
class UnAuthorized<T> : AuthResult<T>()
class UnknownError<T> : AuthResult<T>()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package one.njk.celestidesk.data.auth.model

import one.njk.celestidesk.data.Role

data class AuthSignUpRequest(
val name: String,
val username: String,
val orgHandle: String,
val type: Role,
val password: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package one.njk.celestidesk.data.auth.model

data class TokenResponse (
val message: String,
val token: String,
)
45 changes: 45 additions & 0 deletions app/src/main/java/one/njk/celestidesk/di/AppModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package one.njk.celestidesk.di

import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import one.njk.celestidesk.data.RolesDataStore
import one.njk.celestidesk.data.auth.AuthApi
import one.njk.celestidesk.data.auth.AuthRepository
import one.njk.celestidesk.data.auth.AuthRepositoryImpl
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.create
import javax.inject.Singleton

const val BASE_URL = "https://celestidesk.onrender.com/api/employee/"

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

@Provides
@Singleton
fun provideAuthApi(): AuthApi {
val moshi = Moshi
.Builder()
.add(KotlinJsonAdapterFactory())
.build()

return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
.create()
}

@Provides
@Singleton
fun provideAuthRepository(api: AuthApi, pref: RolesDataStore): AuthRepository{
return AuthRepositoryImpl(api, pref)
}

}
88 changes: 88 additions & 0 deletions app/src/main/java/one/njk/celestidesk/ui/LoginFragment.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package one.njk.celestidesk.ui

import android.content.Context
import android.os.Bundle
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import one.njk.celestidesk.data.auth.model.AuthResult
import one.njk.celestidesk.databinding.FragmentLoginBinding
import one.njk.celestidesk.viewmodels.AuthViewModel

@AndroidEntryPoint
class LoginFragment: Fragment() {
lateinit var binding: FragmentLoginBinding
val viewModel: AuthViewModel by viewModels()

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentLoginBinding.inflate(inflater, container, false)
binding.apply {
submit.setOnClickListener {
lifecycleScope.launch {
val inputUsername = username.editText?.text.toString()
val inputPassword = password.editText?.text.toString()
if(inputPassword.isNotEmpty() && inputUsername.isNotEmpty()){
viewModel.logIn(inputUsername, inputPassword)
}
}
}
viewModel.state.observe(viewLifecycleOwner){
if(it.isLoading) {
loading.visibility = View.VISIBLE
submit.visibility = View.GONE
} else {
loading.visibility = View.INVISIBLE
submit.visibility = View.VISIBLE
}
if(it.authResult is AuthResult.Authorized) {
findNavController().navigate(
LoginFragmentDirections.actionLoginFragmentToRequestFragment())
} else {
Toast.makeText(context, "verify your username / password", Toast.LENGTH_SHORT).show()
}
}
submit.setOnEditorActionListener { view, event, _ ->
if(event == EditorInfo.IME_ACTION_NEXT){
view.requestFocus()
true
} else {
handleKeyEvent(view, event)
true
}
}
}
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}

override fun onDestroyView() {
super.onDestroyView()
}
private fun handleKeyEvent(view: View, keyCode: Int): Boolean {
if (keyCode == KeyEvent.KEYCODE_ENTER) {
// Hide the keyboard
val inputMethodManager =
requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
return true
}
return false
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package one.njk.celestidesk
package one.njk.celestidesk.ui

import android.os.Bundle
import androidx.fragment.app.Fragment
Expand Down
47 changes: 47 additions & 0 deletions app/src/main/java/one/njk/celestidesk/viewmodels/AuthViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package one.njk.celestidesk.viewmodels

import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.update
import one.njk.celestidesk.data.auth.AuthRepository
import one.njk.celestidesk.data.auth.model.AuthLoginRequest
import one.njk.celestidesk.data.auth.model.AuthResult
import javax.inject.Inject

@HiltViewModel
class AuthViewModel @Inject constructor(
private val api: AuthRepository
) : ViewModel() {

private val uiState = MutableStateFlow(UiState())

@OptIn(ExperimentalCoroutinesApi::class)
val state: LiveData<UiState> = uiState.flatMapLatest { uiState ->
flowOf(uiState).flowOn(Dispatchers.Default)
}.asLiveData()

suspend fun logIn(username: String, password: String){
uiState.update {
it.copy(isLoading = true)
}
val result = api.logIn(
AuthLoginRequest(username, password)
)
uiState.update {
it.copy(isLoading = false, authResult = result)
}
}
}

data class UiState(
val isLoading: Boolean = false,
val authResult: AuthResult<Unit> = AuthResult.UnknownError(),
)
4 changes: 4 additions & 0 deletions app/src/main/res/drawable/profile.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<vector android:height="24dp" android:viewportHeight="960"
android:viewportWidth="960" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M480,479q-66,0 -108,-42t-42,-108q0,-66 42,-108t108,-42q66,0 108,42t42,108q0,66 -42,108t-108,42ZM220,800q-25,0 -42.5,-17.5T160,740v-34q0,-38 19,-65t49,-41q67,-30 128.5,-45T480,540q62,0 123,15.5T731,600q31,14 50,41t19,65v34q0,25 -17.5,42.5T740,800L220,800Z"/>
</vector>
Loading

0 comments on commit 0411d0e

Please sign in to comment.