Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ android {
}
kotlinOptions {
jvmTarget = '1.8'
freeCompilerArgs += [
'-Xjvm-default=enable'
]
}
buildFeatures {
viewBinding = true
Expand Down Expand Up @@ -73,6 +76,8 @@ dependencies {
kapt 'com.google.dagger:hilt-android-compiler:2.44'
// Firebase BoM
implementation platform('com.google.firebase:firebase-bom:31.0.2')
// Firebase Auth
implementation 'com.google.firebase:firebase-auth-ktx'

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<uses-permission android:name="android.permission.INTERNET" />

<application
android:name=".AlkemyBankBaseApp"
android:usesCleartextTraffic="true"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.Alkemy.alkemybankbase

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class AlkemyBankBaseApp: Application() {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.Alkemy.alkemybankbase.application

object AppConstants {
// API url
const val API_URL = "http://wallet-main.eba-ccwdurgr.us-east-1.elasticbeanstalk.com/"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.Alkemy.alkemybankbase.core

import android.content.Context


class UserPreferences(context: Context) {
private val sharedPreference = context.getSharedPreferences("token", Context.MODE_PRIVATE)

// Saves token in SharedPreferences
fun saveAuthToken(authToken: String){

val editor = sharedPreference.edit()
editor.putString("token",authToken)

editor.apply()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.Alkemy.alkemybankbase.data.model

data class LoginRequest(
val email: String,
val password: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.Alkemy.alkemybankbase.data.model

import com.google.gson.annotations.SerializedName

data class LoginResponse (
@SerializedName("accessToken")
val accessToken: String?,
@SerializedName("error")
val error: String?,
@SerializedName("status")
val status: Int?,
)

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.Alkemy.alkemybankbase.data.model

data class UserRemote(
val accessToken:String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.Alkemy.alkemybankbase.data.model

data class WrappedResponse<T>(
val accessToken: String?,
val error: String?,
val status: Int?,
val data: T? = null
)
7 changes: 7 additions & 0 deletions app/src/main/java/com/Alkemy/alkemybankbase/di/AppModule.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.Alkemy.alkemybankbase.di

import com.Alkemy.alkemybankbase.repository.WebService
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand All @@ -20,4 +21,10 @@ object AppModule {
.addConverterFactory(GsonConverterFactory.create())
.build()
}

@Singleton
@Provides
fun provideWebService(retrofit: Retrofit): WebService {
return retrofit.create(WebService::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.Alkemy.alkemybankbase.presentation

import androidx.lifecycle.*
import com.Alkemy.alkemybankbase.core.UserPreferences
import com.Alkemy.alkemybankbase.data.model.LoginRequest
import com.Alkemy.alkemybankbase.data.model.UserRemote
import com.Alkemy.alkemybankbase.repository.WebService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class UserViewModel: ViewModel() {
private val _state = MutableLiveData<LoginState>(LoginState.Init)
val state: LiveData<LoginState> get() = _state

private lateinit var userPreferences: UserPreferences

fun auth(email:String, password:String){
viewModelScope.launch {

_state.value = LoginState.IsLoading(true)

try {
val response = withContext(Dispatchers.IO) {
WebService.build().loginUser(LoginRequest(email, password))
}
if (response.isSuccessful) {
val response = response.body()
response?.let {
if (it.accessToken != null) {
_state.value = LoginState.Success(UserRemote(it.accessToken))
userPreferences.saveAuthToken(it.accessToken)
}
else {
_state.value = LoginState.Error(it.error!!)
}
}
} else {
_state.value = LoginState.Error(response.toString())
}
} catch (ex: Exception) {
_state.value = LoginState.Error(ex.message.toString())
} finally {
_state.value = LoginState.IsLoading(false)
}
}
}

sealed class LoginState {

object Init : LoginState()
data class IsLoading(val isLoading: Boolean) : LoginState()
data class Error(val rawResponse: String) : LoginState()
data class Success(val user: UserRemote) : LoginState()

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.Alkemy.alkemybankbase.repository

import com.Alkemy.alkemybankbase.application.AppConstants
import com.Alkemy.alkemybankbase.data.model.LoginRequest
import com.Alkemy.alkemybankbase.data.model.LoginResponse
import com.Alkemy.alkemybankbase.data.model.WrappedResponse
import com.google.gson.GsonBuilder
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.*

object WebService {

private val builder : Retrofit.Builder = Retrofit.Builder()
.baseUrl(AppConstants.API_URL)
.addConverterFactory(GsonConverterFactory.create())

interface ApiInterface{
@POST("auth/login")
suspend fun loginUser(@Body request: LoginRequest) : Response<WrappedResponse<LoginResponse>>
}

fun build() : ApiInterface{
return builder.build().create(ApiInterface::class.java)
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.Alkemy.alkemybankbase.ui.activities

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.Alkemy.alkemybankbase.R
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class InitialHostActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_initial_host)
}
}
Original file line number Diff line number Diff line change
@@ -1,93 +1,70 @@
package com.Alkemy.alkemybankbase.ui.fragments

import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup

import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import com.Alkemy.alkemybankbase.R
import com.Alkemy.alkemybankbase.core.UserPreferences
import com.Alkemy.alkemybankbase.databinding.FragmentLoginBinding
import com.Alkemy.alkemybankbase.viewmodels.loginviewmodel.LoginViewModel
import com.Alkemy.alkemybankbase.presentation.UserViewModel
import com.Alkemy.alkemybankbase.utils.toast


class LoginFragment : Fragment() {
open class LoginFragment : Fragment(R.layout.fragment_login) {
private lateinit var binding : FragmentLoginBinding

// Declaro las variables de vinculacion de datos
private var _binding: FragmentLoginBinding? = null
private val binding get() = _binding!!
private val viewModel : UserViewModel by viewModels()

// Declaro el ViewModel
val viewModel by viewModels<LoginViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

// Utilizo la funcion Textwatcher para poder controlar el cambio de los textos editables a traves del objeto
val loginTextWatcher = object : TextWatcher {
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
binding = FragmentLoginBinding.bind(view)

override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
//Mockup
binding.edtEmail.setText("juanperez@example.com")
binding.edtPassword.setText("abc123")

//Llamo al metodo de validacion para que se realice la comprobacion en el viewModel, pasando los parametros del email y la contraseña
viewModel.isValidateEmailAndPassword(
binding.edtEmail.text.toString(),
binding.edtPassword.text.toString()
)
}
events()
setupObservers()
}

override fun afterTextChanged(p0: Editable?) {
//Aplico el metodo observe para que controlo el ciclo de vida y se habilite o deshabilite
//el boton dependendiendo de la variable
viewModel.isEnable.observe(viewLifecycleOwner, Observer { enable ->
binding.btnSingIn.isEnabled = enable
navigateUP(enable)
})
}
// Do login
private fun events() = with(binding) {

btnSingIn.setOnClickListener {
val email = binding.edtEmail.text.toString()
val password = binding.edtPassword.text.toString()

viewModel.auth(email, password)
}
}

//Creamos la funcion de navegacion
private fun navigateUP(enable: Boolean) {
// Realizamos un flujo de control en el caso de que se haya habilitado el buton le permitimos el acceso
if (enable) {
binding.btnSingIn.setOnClickListener {
findNavController().navigate(R.id.action_loginFragment_to_homeFragment)
// Sets Up Login State
private fun setupObservers(){
viewModel.state.observe(viewLifecycleOwner){ state ->
when(state){
UserViewModel.LoginState.Init -> Unit
is UserViewModel.LoginState.Error -> showError(state.rawResponse)
is UserViewModel.LoginState.IsLoading -> showProgress(state.isLoading)
is UserViewModel.LoginState.Success -> {
val userRemote = state.user
// TODO Navigate to Home
requireContext().toast("Bienvenido ${userRemote.accessToken}")
}
}

}
}


override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentLoginBinding.inflate(layoutInflater)
// Inflate the layout for this fragment
return binding.root
// Show error alert dialog if login fails
private fun showError(error:String){
// TODO Show Error Dialog
requireContext().toast(error)
}

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

binding.edtEmail.addTextChangedListener(loginTextWatcher)
binding.edtPassword.addTextChangedListener(loginTextWatcher)


// Show progress indicator while loading
private fun showProgress(visibility:Boolean){
// TODO Show Pregress Indicator
}
}










}
5 changes: 5 additions & 0 deletions app/src/main/java/com/Alkemy/alkemybankbase/utils/utils.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package com.Alkemy.alkemybankbase.utils

import android.content.Context
import android.widget.Toast
import android.util.Patterns

fun Context.toast(message:String){
Toast.makeText(this,message,Toast.LENGTH_LONG).show()
}

/*
Es una funcion con tipo lo que hace es recibir dos strings(email y la contraseña) y devuelve un valor boleano
Expand Down