diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 000000000..67d85fe2c --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + 1697297474190 + + + + \ No newline at end of file diff --git a/ExtractBank/.gitignore b/ExtractBank/.gitignore new file mode 100644 index 000000000..aa724b770 --- /dev/null +++ b/ExtractBank/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/ExtractBank/.idea/.gitignore b/ExtractBank/.idea/.gitignore new file mode 100644 index 000000000..26d33521a --- /dev/null +++ b/ExtractBank/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/ExtractBank/.idea/compiler.xml b/ExtractBank/.idea/compiler.xml new file mode 100644 index 000000000..b589d56e9 --- /dev/null +++ b/ExtractBank/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/ExtractBank/.idea/gradle.xml b/ExtractBank/.idea/gradle.xml new file mode 100644 index 000000000..ae388c2a5 --- /dev/null +++ b/ExtractBank/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/ExtractBank/.idea/kotlinc.xml b/ExtractBank/.idea/kotlinc.xml new file mode 100644 index 000000000..fdf8d994a --- /dev/null +++ b/ExtractBank/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/ExtractBank/.idea/misc.xml b/ExtractBank/.idea/misc.xml new file mode 100644 index 000000000..8978d23db --- /dev/null +++ b/ExtractBank/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/ExtractBank/.idea/vcs.xml b/ExtractBank/.idea/vcs.xml new file mode 100644 index 000000000..6c0b86358 --- /dev/null +++ b/ExtractBank/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/ExtractBank/app/.gitignore b/ExtractBank/app/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/ExtractBank/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/ExtractBank/app/build.gradle.kts b/ExtractBank/app/build.gradle.kts new file mode 100644 index 000000000..41bf97541 --- /dev/null +++ b/ExtractBank/app/build.gradle.kts @@ -0,0 +1,77 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "fingerfire.com.extractbank" + compileSdk = 34 + + defaultConfig { + applicationId = "fingerfire.com.extractbank" + minSdk = 19 + targetSdk = 33 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + multiDexEnabled = true + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + viewBinding = true + } +} + +dependencies { + + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.10.0") + implementation("androidx.constraintlayout:constraintlayout:2.1.4") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + + //Retrofit + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") + + //OkHttp + implementation("com.squareup.okhttp3:okhttp:4.11.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.11.0") + + //MVVM e LiveData + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2") + + //Koin + implementation("io.insert-koin:koin-core:3.4.3") + implementation("io.insert-koin:koin-android:3.4.3") + + //Mockito + testImplementation("org.mockito:mockito-core:5.6.0") + + //Test Kotlin + testImplementation("junit:junit:4.13.2") + testImplementation("androidx.test:core:1.5.0") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") + testImplementation("androidx.arch.core:core-testing:2.2.0") +} \ No newline at end of file diff --git a/ExtractBank/app/proguard-rules.pro b/ExtractBank/app/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/ExtractBank/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/ExtractBank/app/release/app-debug.apk b/ExtractBank/app/release/app-debug.apk new file mode 100644 index 000000000..f3a66a3f7 Binary files /dev/null and b/ExtractBank/app/release/app-debug.apk differ diff --git a/ExtractBank/app/src/androidTest/java/fingerfire/com/extractbank/ExampleInstrumentedTest.kt b/ExtractBank/app/src/androidTest/java/fingerfire/com/extractbank/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..de71e8869 --- /dev/null +++ b/ExtractBank/app/src/androidTest/java/fingerfire/com/extractbank/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package fingerfire.com.extractbank + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("fingerfire.com.extractbank", appContext.packageName) + } +} \ No newline at end of file diff --git a/ExtractBank/app/src/main/AndroidManifest.xml b/ExtractBank/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..31b53476c --- /dev/null +++ b/ExtractBank/app/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ExtractBank/app/src/main/java/fingerfire/com/extractbank/ExtractBankApplication.kt b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/ExtractBankApplication.kt new file mode 100644 index 000000000..3b4ed0fcc --- /dev/null +++ b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/ExtractBankApplication.kt @@ -0,0 +1,27 @@ +package fingerfire.com.extractbank + +import android.app.Application +import fingerfire.com.extractbank.di.ApiModules +import fingerfire.com.extractbank.di.DataModule +import fingerfire.com.extractbank.di.NetworkModules +import fingerfire.com.extractbank.di.UiModules +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin + +class ExtractBankApplication : Application() { + override fun onCreate() { + super.onCreate() + + startKoin { + androidContext(this@ExtractBankApplication) + modules( + listOf( + NetworkModules().getNetworkModules(), + ApiModules().getApiModules(), + UiModules().getViewModules(), + DataModule().getDataModules() + ) + ) + } + } +} \ No newline at end of file diff --git a/ExtractBank/app/src/main/java/fingerfire/com/extractbank/api/BankApi.kt b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/api/BankApi.kt new file mode 100644 index 000000000..9aa29044e --- /dev/null +++ b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/api/BankApi.kt @@ -0,0 +1,22 @@ +package fingerfire.com.extractbank.api + +import fingerfire.com.extractbank.features.statements.data.StatementsResponse +import fingerfire.com.extractbank.model.Login +import fingerfire.com.extractbank.model.User +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path + +interface BankApi { + @POST("api/v1/login") + suspend fun login( + @Body login: Login + ): Response + + @GET("api/v1/login/{loginId}/statement") + suspend fun getStatement( + @Path("loginId") id: String + ): Response> +} \ No newline at end of file diff --git a/ExtractBank/app/src/main/java/fingerfire/com/extractbank/di/ApiModules.kt b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/di/ApiModules.kt new file mode 100644 index 000000000..7fed3df8c --- /dev/null +++ b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/di/ApiModules.kt @@ -0,0 +1,13 @@ +package fingerfire.com.extractbank.di + +import fingerfire.com.extractbank.api.BankApi +import org.koin.dsl.module +import retrofit2.Retrofit + +class ApiModules { + fun getApiModules() = module { + factory { + get().create(BankApi::class.java) + } + } +} \ No newline at end of file diff --git a/ExtractBank/app/src/main/java/fingerfire/com/extractbank/di/DataModule.kt b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/di/DataModule.kt new file mode 100644 index 000000000..4326cc55c --- /dev/null +++ b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/di/DataModule.kt @@ -0,0 +1,34 @@ +package fingerfire.com.extractbank.di + +import android.app.Application +import android.content.SharedPreferences +import fingerfire.com.extractbank.features.login.data.LoginRepository +import fingerfire.com.extractbank.features.statements.data.StatementRepository +import org.koin.android.ext.koin.androidApplication +import org.koin.dsl.module + +class DataModule { + private fun getSharedPrefs(androidApplication: Application): SharedPreferences { + return androidApplication.getSharedPreferences( + "default", + android.content.Context.MODE_PRIVATE + ) + } + + fun getDataModules() = module { + single { + getSharedPrefs(androidApplication()) + } + + single { + getSharedPrefs(androidApplication()).edit() + } + factory { + LoginRepository(get(), get()) + } + + factory { + StatementRepository(get()) + } + } +} \ No newline at end of file diff --git a/ExtractBank/app/src/main/java/fingerfire/com/extractbank/di/NetworkModules.kt b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/di/NetworkModules.kt new file mode 100644 index 000000000..f6ff5f7cb --- /dev/null +++ b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/di/NetworkModules.kt @@ -0,0 +1,12 @@ +package fingerfire.com.extractbank.di + +import fingerfire.com.extractbank.network.SetupRetrofit +import org.koin.dsl.module + +class NetworkModules { + fun getNetworkModules() = module { + single { + SetupRetrofit.getRetrofit() + } + } +} \ No newline at end of file diff --git a/ExtractBank/app/src/main/java/fingerfire/com/extractbank/di/UiModules.kt b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/di/UiModules.kt new file mode 100644 index 000000000..818094fbf --- /dev/null +++ b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/di/UiModules.kt @@ -0,0 +1,17 @@ +package fingerfire.com.extractbank.di + +import fingerfire.com.extractbank.features.login.ui.LoginViewModel +import fingerfire.com.extractbank.features.statements.ui.StatementViewModel +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module + +class UiModules { + fun getViewModules() = module { + viewModel { + LoginViewModel(get()) + } + viewModel { + StatementViewModel(get()) + } + } +} \ No newline at end of file diff --git a/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/login/data/LoginRepository.kt b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/login/data/LoginRepository.kt new file mode 100644 index 000000000..3b8d5bcd7 --- /dev/null +++ b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/login/data/LoginRepository.kt @@ -0,0 +1,34 @@ +package fingerfire.com.extractbank.features.login.data + +import android.content.SharedPreferences +import fingerfire.com.extractbank.api.BankApi +import fingerfire.com.extractbank.model.Login +import fingerfire.com.extractbank.model.User +import fingerfire.com.extractbank.network.ServiceState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class LoginRepository( + private val bankApi: BankApi, + private val sharedPreferences: SharedPreferences +) { + suspend fun login(login: Login): ServiceState { + return withContext(Dispatchers.IO) { + val response = bankApi.login(login) + if (response.isSuccessful) { + saveUser(login) + ServiceState.Success(response.body()) + } else { + ServiceState.Error() + } + } + } + + private fun saveUser(login: Login) { + sharedPreferences.edit().putString("USER_LOGIN", login.user).apply() + } + + fun getSavedUser(): String? { + return sharedPreferences.getString("USER_LOGIN", "") + } +} \ No newline at end of file diff --git a/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/login/ui/LoginActivity.kt b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/login/ui/LoginActivity.kt new file mode 100644 index 000000000..97a05bd84 --- /dev/null +++ b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/login/ui/LoginActivity.kt @@ -0,0 +1,89 @@ +package fingerfire.com.extractbank.features.login.ui + +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.core.os.bundleOf +import fingerfire.com.extractbank.R +import fingerfire.com.extractbank.databinding.ActivityLoginBinding +import fingerfire.com.extractbank.features.login.ui.viewstate.LoginViewState +import fingerfire.com.extractbank.features.statements.ui.StatementActivity +import fingerfire.com.extractbank.model.Login +import fingerfire.com.extractbank.model.User +import fingerfire.com.extractbank.utils.Utils +import org.koin.androidx.viewmodel.ext.android.viewModel + +private const val USER = "USER" + +class LoginActivity : AppCompatActivity() { + private lateinit var binding: ActivityLoginBinding + private val viewModel: LoginViewModel by viewModel() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityLoginBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) + + setupLoginButton() + observeLoginState() + observeSavedUser() + } + + private fun observeLoginState() { + viewModel.loginLiveData.observe(this) { loginViewState -> + when (loginViewState) { + is LoginViewState.Success -> { + navigateToStatements(loginViewState.user) + } + + is LoginViewState.Error -> { + Utils.showError(loginViewState.message, this) + } + } + } + } + + private fun observeSavedUser() { + viewModel.getSavedUserLiveData.observe(this) { + if (!it.isNullOrBlank()) { + binding.etEmailCpf.setText(it) + } + } + } + + private fun setupLoginButton() { + binding.btnLogin.setOnClickListener { + binding.progressBar.visibility = View.VISIBLE + val emailOrCpf = binding.etEmailCpf.text.toString() + val password = binding.etPassword.text.toString() + + val isEmailOrCpfValid = viewModel.isValidEmailOrCPF(emailOrCpf) + val (isPasswordValid, passwordErrorMessage) = viewModel.isPasswordValid(password) + + if (isEmailOrCpfValid && isPasswordValid) { + val loginData = Login(emailOrCpf, password) + viewModel.loginUser(loginData) + } else { + binding.progressBar.visibility = View.INVISIBLE + if (!isEmailOrCpfValid) { + Utils.showError(getString(R.string.error_invalid_email_or_cpf), this) + } else if (!isPasswordValid) { + Utils.showError( + passwordErrorMessage ?: getString(R.string.error_password_requirements), + this + ) + } + } + } + } + + private fun navigateToStatements(user: User) { + val intent = Intent(this, StatementActivity::class.java).apply { + putExtras(bundleOf(USER to user)) + } + binding.progressBar.visibility = View.INVISIBLE + startActivity(intent) + finish() + } +} diff --git a/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/login/ui/LoginViewModel.kt b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/login/ui/LoginViewModel.kt new file mode 100644 index 000000000..4472afafd --- /dev/null +++ b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/login/ui/LoginViewModel.kt @@ -0,0 +1,80 @@ +package fingerfire.com.extractbank.features.login.ui + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import fingerfire.com.extractbank.features.login.data.LoginRepository +import fingerfire.com.extractbank.features.login.ui.viewstate.LoginViewState +import fingerfire.com.extractbank.model.Login +import fingerfire.com.extractbank.network.ServiceState +import fingerfire.com.extractbank.utils.Validations +import fingerfire.com.extractbank.utils.Validations.isValidEmail +import kotlinx.coroutines.launch + +class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() { + private val loginMutableLiveData: MutableLiveData = MutableLiveData() + private val savedUserMutableLiveData: MutableLiveData = MutableLiveData() + private val defaultErrorMessage = "Ocorreu um erro durante o login. Tente novamente mais tarde." + + val loginLiveData: LiveData get() = loginMutableLiveData + val getSavedUserLiveData: LiveData get() = savedUserMutableLiveData + + init { + getUserSaved() + } + + private fun getUserSaved() { + savedUserMutableLiveData.value = loginRepository.getSavedUser() + } + + fun loginUser(login: Login) { + val validationResult = validateLogin(login) + if (validationResult.isValid) { + performLogin(login) + } else { + handleValidationError(validationResult.errorMessage) + } + } + + private fun validateLogin(login: Login): ValidationResult { + val passwordValidationResult = isPasswordValid(login.password) + if (!passwordValidationResult.isValid) { + return ValidationResult(false, passwordValidationResult.errorMessage) + } + + return ValidationResult(true, null) + } + + fun isValidEmailOrCPF(emailCpf: String): Boolean { + val cleanedCpf = emailCpf.replace("[.\\-]".toRegex(), "") + return emailCpf.isValidEmail() || Validations.isValidCPF(cleanedCpf) + } + + fun isPasswordValid(password: String): ValidationResult { + val validationResult = Validations.isPasswordValid(password) + return ValidationResult(validationResult.isValid, validationResult.errorMessage) + } + + private fun performLogin(login: Login) { + viewModelScope.launch { + when (val loginResponse = loginRepository.login(login)) { + is ServiceState.Success -> { + loginMutableLiveData.postValue( + loginResponse.data?.let { LoginViewState.Success(it) } + ) + } + + is ServiceState.Error -> { + handleValidationError(defaultErrorMessage) + } + } + } + } + + private fun handleValidationError(errorMessage: String?) { + loginMutableLiveData.postValue(LoginViewState.Error(errorMessage ?: defaultErrorMessage)) + } +} + +data class ValidationResult(val isValid: Boolean, val errorMessage: String?) \ No newline at end of file diff --git a/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/login/ui/viewstate/LoginViewState.kt b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/login/ui/viewstate/LoginViewState.kt new file mode 100644 index 000000000..c5551f092 --- /dev/null +++ b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/login/ui/viewstate/LoginViewState.kt @@ -0,0 +1,8 @@ +package fingerfire.com.extractbank.features.login.ui.viewstate + +import fingerfire.com.extractbank.model.User + +sealed class LoginViewState { + data class Success(val user: User) : LoginViewState() + data class Error(val message: String) : LoginViewState() +} \ No newline at end of file diff --git a/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/statements/data/StatementRepository.kt b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/statements/data/StatementRepository.kt new file mode 100644 index 000000000..b61e1dc0a --- /dev/null +++ b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/statements/data/StatementRepository.kt @@ -0,0 +1,15 @@ +package fingerfire.com.extractbank.features.statements.data + +import fingerfire.com.extractbank.api.BankApi +import fingerfire.com.extractbank.network.ServiceState + +class StatementRepository(private val bankApi: BankApi) { + suspend fun getStatement(idUser: String): ServiceState> { + val response = bankApi.getStatement(idUser) + return if (response.isSuccessful) { + ServiceState.Success(response.body()) + } else { + ServiceState.Error() + } + } +} \ No newline at end of file diff --git a/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/statements/data/StatementsResponse.kt b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/statements/data/StatementsResponse.kt new file mode 100644 index 000000000..d17a2be78 --- /dev/null +++ b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/statements/data/StatementsResponse.kt @@ -0,0 +1,9 @@ +package fingerfire.com.extractbank.features.statements.data + +data class StatementsResponse( + val idUser: String, + val date: String, + val value: Double, + val type: String, + val typeTransaction: String +) \ No newline at end of file diff --git a/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/statements/ui/StatementActivity.kt b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/statements/ui/StatementActivity.kt new file mode 100644 index 000000000..1a26f81b6 --- /dev/null +++ b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/statements/ui/StatementActivity.kt @@ -0,0 +1,82 @@ +package fingerfire.com.extractbank.features.statements.ui + +import android.content.Intent +import android.os.Build +import android.os.Bundle +import androidx.annotation.RequiresApi +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.LinearLayoutManager +import fingerfire.com.extractbank.R +import fingerfire.com.extractbank.databinding.ActivityStatementBinding +import fingerfire.com.extractbank.features.login.ui.LoginActivity +import fingerfire.com.extractbank.features.statements.data.StatementsResponse +import fingerfire.com.extractbank.features.statements.ui.adapter.StatementAdapter +import fingerfire.com.extractbank.features.statements.ui.viewstate.StatementViewState +import fingerfire.com.extractbank.model.User +import fingerfire.com.extractbank.utils.Utils +import org.koin.androidx.viewmodel.ext.android.viewModel + +class StatementActivity : AppCompatActivity() { + private lateinit var binding: ActivityStatementBinding + private lateinit var statementAdapter: StatementAdapter + private val viewModel: StatementViewModel by viewModel() + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityStatementBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) + window.statusBarColor = ContextCompat.getColor(this, R.color.blue) + + observeStatement() + viewModel.getStatementsForUser("1") + + val extras = intent.extras + if (extras != null && extras.containsKey("USER")) { + val user = extras.getSerializable("USER") as User + loadUser(user) + } + + binding.ivLogout.setOnClickListener { + val intent = Intent(this, LoginActivity::class.java) + startActivity(intent) + finish() + } + } + + private fun loadUser(user: User) { + binding.apply { + tvName.text = user.name + tvBalance.text = Utils.formatToCurrency(user.amount) + tvAccount.text = Utils.formatAccountNumber(user.agency, user.account) + } + } + + private fun observeStatement() { + viewModel.statementLiveData.observe(this) { statementViewState -> + when (statementViewState) { + is StatementViewState.Success -> { + setupRecyclerView() + initAdapter(statementViewState.data) + } + + is StatementViewState.Error -> { + Utils.showError(getString(R.string.error_statement_load), this) + } + } + } + } + + private fun setupRecyclerView() { + binding.rvStatement.layoutManager = + LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) + binding.rvStatement.setHasFixedSize(true) + } + + private fun initAdapter(statementsList: List) { + statementAdapter = StatementAdapter(statementsList) + binding.rvStatement.adapter = statementAdapter + } +} diff --git a/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/statements/ui/StatementViewModel.kt b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/statements/ui/StatementViewModel.kt new file mode 100644 index 000000000..4c444b381 --- /dev/null +++ b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/statements/ui/StatementViewModel.kt @@ -0,0 +1,39 @@ +package fingerfire.com.extractbank.features.statements.ui + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import fingerfire.com.extractbank.features.statements.data.StatementRepository +import fingerfire.com.extractbank.features.statements.ui.viewstate.StatementViewState +import fingerfire.com.extractbank.network.ServiceState +import kotlinx.coroutines.launch + +class StatementViewModel(private val statementRepository: StatementRepository) : ViewModel() { + private val statementMutableLiveData: MutableLiveData = + MutableLiveData() + + val statementLiveData: LiveData + get() = statementMutableLiveData + + fun getStatementsForUser(idUser: String) { + viewModelScope.launch { + when (val statementsResponse = statementRepository.getStatement(idUser)) { + is ServiceState.Success -> { + val statements = statementsResponse.data + if (statements != null) { + if (statements.isNotEmpty()) { + statementMutableLiveData.postValue(StatementViewState.Success(statements)) + } else { + statementMutableLiveData.postValue(StatementViewState.Error("Nenhum extrato encontrado.")) + } + } + } + + is ServiceState.Error -> { + statementMutableLiveData.postValue(StatementViewState.Error("Ocorreu um erro ao obter os extratos.")) + } + } + } + } +} diff --git a/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/statements/ui/adapter/StatementAdapter.kt b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/statements/ui/adapter/StatementAdapter.kt new file mode 100644 index 000000000..9a3f11bf1 --- /dev/null +++ b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/statements/ui/adapter/StatementAdapter.kt @@ -0,0 +1,39 @@ +package fingerfire.com.extractbank.features.statements.ui.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import fingerfire.com.extractbank.databinding.ItemStatementBinding +import fingerfire.com.extractbank.features.statements.data.StatementsResponse +import fingerfire.com.extractbank.utils.Utils + +class StatementAdapter( + private val statementList: List +) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatementViewHolder { + return StatementViewHolder( + ItemStatementBinding + .inflate(LayoutInflater.from(parent.context), parent, false) + ) + } + + override fun onBindViewHolder(holder: StatementViewHolder, position: Int) { + val statement = statementList[position] + with(holder) { + binding.tvType.text = statement.type + binding.tvValue.text = Utils.formatToCurrency(statement.value) + binding.tvDate.text = statement.date + binding.tvTypeTransaction.text = statement.typeTransaction + } + } + + + override fun getItemCount(): Int { + return statementList.size + } + + class StatementViewHolder(val binding: ItemStatementBinding) : + RecyclerView.ViewHolder(binding.root) + +} diff --git a/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/statements/ui/viewstate/StatementViewState.kt b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/statements/ui/viewstate/StatementViewState.kt new file mode 100644 index 000000000..f02b79175 --- /dev/null +++ b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/features/statements/ui/viewstate/StatementViewState.kt @@ -0,0 +1,8 @@ +package fingerfire.com.extractbank.features.statements.ui.viewstate + +import fingerfire.com.extractbank.features.statements.data.StatementsResponse + +sealed class StatementViewState { + data class Success(val data: List) : StatementViewState() + data class Error(val errorMessage: String? = null) : StatementViewState() +} diff --git a/ExtractBank/app/src/main/java/fingerfire/com/extractbank/model/Login.kt b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/model/Login.kt new file mode 100644 index 000000000..553ef68d6 --- /dev/null +++ b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/model/Login.kt @@ -0,0 +1,6 @@ +package fingerfire.com.extractbank.model + +data class Login( + val user: String, + val password: String +) diff --git a/ExtractBank/app/src/main/java/fingerfire/com/extractbank/model/User.kt b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/model/User.kt new file mode 100644 index 000000000..be1530432 --- /dev/null +++ b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/model/User.kt @@ -0,0 +1,11 @@ +package fingerfire.com.extractbank.model + +import java.io.Serializable + +data class User( + val id: Int, + val name: String, + val account: String, + val agency: String, + val amount: Double +) : Serializable diff --git a/ExtractBank/app/src/main/java/fingerfire/com/extractbank/network/ServiceState.kt b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/network/ServiceState.kt new file mode 100644 index 000000000..75ec303ea --- /dev/null +++ b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/network/ServiceState.kt @@ -0,0 +1,6 @@ +package fingerfire.com.extractbank.network + +sealed class ServiceState { + data class Success(val data: T?) : ServiceState() + data class Error(val data: T? = null) : ServiceState() +} \ No newline at end of file diff --git a/ExtractBank/app/src/main/java/fingerfire/com/extractbank/network/SetupRetrofit.kt b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/network/SetupRetrofit.kt new file mode 100644 index 000000000..8a7f1f8c4 --- /dev/null +++ b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/network/SetupRetrofit.kt @@ -0,0 +1,28 @@ +package fingerfire.com.extractbank.network + +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit + +object SetupRetrofit { + + private const val BASE_URL = "https://652ac1f24791d884f1fd552e.mockapi.io/" + + private fun client() = OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .addInterceptor(HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + }) + .build() + + fun getRetrofit(): Retrofit { + return Retrofit.Builder() + .baseUrl(BASE_URL) + .client(client()) + .addConverterFactory(GsonConverterFactory.create()) + .build() + } +} \ No newline at end of file diff --git a/ExtractBank/app/src/main/java/fingerfire/com/extractbank/utils/Utils.kt b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/utils/Utils.kt new file mode 100644 index 000000000..bcd2d1a71 --- /dev/null +++ b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/utils/Utils.kt @@ -0,0 +1,29 @@ +package fingerfire.com.extractbank.utils + +import android.content.Context +import android.widget.Toast +import java.text.NumberFormat +import java.util.Locale + +object Utils { + + fun formatToCurrency(value: Double): String { + val format = NumberFormat.getCurrencyInstance(Locale("pt", "BR")) + return format.format(value) + } + + fun formatAccountNumber(agencyNumber: String, accountNumber: String): String { + val length = accountNumber.length + return String.format( + "%s / %s.%s-%s", + agencyNumber, + accountNumber.substring(0, 2), + accountNumber.substring(2, length - 2), + accountNumber.substring(length - 1, length) + ) + } + + fun showError(errorMessage: String, context: Context) { + Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT).show() + } +} \ No newline at end of file diff --git a/ExtractBank/app/src/main/java/fingerfire/com/extractbank/utils/Validations.kt b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/utils/Validations.kt new file mode 100644 index 000000000..cc1946805 --- /dev/null +++ b/ExtractBank/app/src/main/java/fingerfire/com/extractbank/utils/Validations.kt @@ -0,0 +1,78 @@ +package fingerfire.com.extractbank.utils + +object Validations { + + private const val UPPERCASE = "A senha precisa ter ao menos uma letra maiúscula" + private const val NUMBER = "A senha precisa ter ao menos um número" + private const val SPECIAL_CHAR = "A senha precisa ter ao menos um caractere especial" + private const val PASSWORD_ERROR_REQUEST = "A senha precisa ter ao menos um caractere especial" + private const val MINIMUM_PASSWORD_LENGTH = "A senha deve ter no mínimo 6 caracteres" + private const val MINIMUM_PASSWORD = 6 + + fun isValidCPF(cpf: String): Boolean { + if (cpf.length != 11) { + return false + } + + if (cpf.all { it == cpf[0] }) { + return false + } + + var sum = 0 + for (i in 0 until 9) { + sum += Character.getNumericValue(cpf[i]) * (10 - i) + } + var remainder = 11 - sum % 11 + if (remainder == 10 || remainder == 11) { + remainder = 0 + } + if (remainder != Character.getNumericValue(cpf[9])) { + return false + } + + sum = 0 + for (i in 0 until 10) { + sum += Character.getNumericValue(cpf[i]) * (11 - i) + } + remainder = 11 - sum % 11 + if (remainder == 10 || remainder == 11) { + remainder = 0 + } + if (remainder != Character.getNumericValue(cpf[10])) { + return false + } + + return true + } + + fun String.isValidEmail(): Boolean { + val emailPattern = "^[A-Za-z](.*)(@)(.{1,})(\\.)(.{1,})" + return matches(emailPattern.toRegex()) + } + + fun isPasswordValid(password: String): ValidationResult { + if (password.length < MINIMUM_PASSWORD) { + return ValidationResult(false, MINIMUM_PASSWORD_LENGTH) + } + + val hasUppercase = "[A-Z]".toRegex().containsMatchIn(password) + val hasNumber = "\\d".toRegex().containsMatchIn(password) + val hasSpecialChar = + "[!\"#$%&'()*+,-./:;\\\\<=>?@\\[\\]^_`{|}~]".toRegex().containsMatchIn(password) + + return if (hasUppercase && hasNumber && hasSpecialChar) { + ValidationResult(true, null) + } else { + val errorMessage = when { + !hasUppercase -> UPPERCASE + !hasNumber -> NUMBER + !hasSpecialChar -> SPECIAL_CHAR + else -> PASSWORD_ERROR_REQUEST + } + ValidationResult(false, errorMessage) + } + } + + data class ValidationResult(val isValid: Boolean, val errorMessage: String? = "") + + } \ No newline at end of file diff --git a/ExtractBank/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/ExtractBank/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/ExtractBank/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/ExtractBank/app/src/main/res/drawable/bg_button_login.xml b/ExtractBank/app/src/main/res/drawable/bg_button_login.xml new file mode 100644 index 000000000..4730e4736 --- /dev/null +++ b/ExtractBank/app/src/main/res/drawable/bg_button_login.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/ExtractBank/app/src/main/res/drawable/bg_edittext_login.xml b/ExtractBank/app/src/main/res/drawable/bg_edittext_login.xml new file mode 100644 index 000000000..ca2b66eb3 --- /dev/null +++ b/ExtractBank/app/src/main/res/drawable/bg_edittext_login.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/ExtractBank/app/src/main/res/drawable/ic_launcher_background.xml b/ExtractBank/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/ExtractBank/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ExtractBank/app/src/main/res/drawable/logo_bank.png b/ExtractBank/app/src/main/res/drawable/logo_bank.png new file mode 100644 index 000000000..66bdc8d5d Binary files /dev/null and b/ExtractBank/app/src/main/res/drawable/logo_bank.png differ diff --git a/ExtractBank/app/src/main/res/drawable/logout_icon.png b/ExtractBank/app/src/main/res/drawable/logout_icon.png new file mode 100644 index 000000000..de1e4ae3c Binary files /dev/null and b/ExtractBank/app/src/main/res/drawable/logout_icon.png differ diff --git a/ExtractBank/app/src/main/res/layout/activity_login.xml b/ExtractBank/app/src/main/res/layout/activity_login.xml new file mode 100644 index 000000000..720db5040 --- /dev/null +++ b/ExtractBank/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,78 @@ + + + + + + + + + +