diff --git a/BankApp/.gitignore b/BankApp/.gitignore new file mode 100644 index 000000000..8af363702 --- /dev/null +++ b/BankApp/.gitignore @@ -0,0 +1,73 @@ +# Built application files +*.apk +*.aar +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +.idea/caches +.idea/modules.xml +.idea/navEditor.xml + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +lint/reports/ diff --git a/BankApp/.idea/.gitignore b/BankApp/.idea/.gitignore new file mode 100644 index 000000000..26d33521a --- /dev/null +++ b/BankApp/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/BankApp/.idea/.name b/BankApp/.idea/.name new file mode 100644 index 000000000..12cd6dc87 --- /dev/null +++ b/BankApp/.idea/.name @@ -0,0 +1 @@ +Bank App \ No newline at end of file diff --git a/BankApp/.idea/codeStyles/Project.xml b/BankApp/.idea/codeStyles/Project.xml new file mode 100644 index 000000000..26a774020 --- /dev/null +++ b/BankApp/.idea/codeStyles/Project.xml @@ -0,0 +1,140 @@ + + + + \ No newline at end of file diff --git a/BankApp/.idea/codeStyles/codeStyleConfig.xml b/BankApp/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 000000000..79ee123c2 --- /dev/null +++ b/BankApp/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/BankApp/.idea/compiler.xml b/BankApp/.idea/compiler.xml new file mode 100644 index 000000000..3794d5969 --- /dev/null +++ b/BankApp/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/BankApp/.idea/jarRepositories.xml b/BankApp/.idea/jarRepositories.xml new file mode 100644 index 000000000..e34606ccd --- /dev/null +++ b/BankApp/.idea/jarRepositories.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/BankApp/.idea/misc.xml b/BankApp/.idea/misc.xml new file mode 100644 index 000000000..0eefc5616 --- /dev/null +++ b/BankApp/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/BankApp/app/.gitignore b/BankApp/app/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/BankApp/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/BankApp/app/build.gradle b/BankApp/app/build.gradle new file mode 100644 index 000000000..b46d76093 --- /dev/null +++ b/BankApp/app/build.gradle @@ -0,0 +1,62 @@ +plugins { + id 'com.android.application' + id 'kotlin-android' +} + +android { + compileSdkVersion 30 + buildToolsVersion "30.0.3" + + defaultConfig { + applicationId "com.accenture.bankapp" + minSdkVersion 19 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_9 + targetCompatibility JavaVersion.VERSION_1_9 + } + + kotlinOptions { + jvmTarget = '9' + } + + testOptions { + unitTests { + includeAndroidResources = true + } + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation 'androidx.core:core-ktx:1.3.2' + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'com.google.android.material:material:1.3.0' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1' + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.retrofit2:converter-gson:2.9.0' + implementation 'br.com.colman.simplecpfvalidator:simple-cpf-validator:2.0.1' + implementation 'com.jakewharton.timber:timber:4.7.1' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.robolectric:robolectric:4.5.1' + androidTestImplementation 'androidx.test.ext:junit:1.1.2' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' +} \ No newline at end of file diff --git a/BankApp/app/proguard-rules.pro b/BankApp/app/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/BankApp/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/BankApp/app/src/main/AndroidManifest.xml b/BankApp/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..63c36d7fb --- /dev/null +++ b/BankApp/app/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/BankApp/app/src/main/java/com/accenture/bankapp/MainActivity.kt b/BankApp/app/src/main/java/com/accenture/bankapp/MainActivity.kt new file mode 100644 index 000000000..32d814890 --- /dev/null +++ b/BankApp/app/src/main/java/com/accenture/bankapp/MainActivity.kt @@ -0,0 +1,48 @@ +package com.accenture.bankapp + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import com.accenture.bankapp.screens.dashboard.DashboardFragment +import com.accenture.bankapp.screens.dashboard.DashboardFragmentListener +import com.accenture.bankapp.screens.login.LoginFragment +import com.accenture.bankapp.screens.login.LoginFragmentListener +import com.accenture.bankapp.utils.transact +import timber.log.Timber +import timber.log.Timber.DebugTree + + +class MainActivity : AppCompatActivity(), LoginFragmentListener, DashboardFragmentListener { + override fun onCreate(savedInstanceState: Bundle?) { + if (BuildConfig.DEBUG) { + Timber.plant(DebugTree()) + } + + Timber.i("onCreate: Creating the Main Activity") + + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + showFragment(LoginFragment()) + } + + override fun startDashboardFragment(dashboardFragment: DashboardFragment) { + Timber.i("startDashboardFragment: Starting Dashboard Fragment") + + showFragment(dashboardFragment) + } + + override fun startLoginFragment(loginFragment: LoginFragment) { + Timber.i("startLoginFragment: Starting Login Fragment") + + showFragment(loginFragment) + } + + private fun showFragment(fragment: Fragment) { + Timber.i("showFragment: Showing the ${fragment::class.simpleName}") + + transact { + replace(R.id.layoutContainer, fragment) + setCustomAnimations(R.anim.abc_fade_in, R.anim.abc_fade_out) + } + } +} \ No newline at end of file diff --git a/BankApp/app/src/main/java/com/accenture/bankapp/network/api/ApiService.kt b/BankApp/app/src/main/java/com/accenture/bankapp/network/api/ApiService.kt new file mode 100644 index 000000000..4a585b032 --- /dev/null +++ b/BankApp/app/src/main/java/com/accenture/bankapp/network/api/ApiService.kt @@ -0,0 +1,15 @@ +package com.accenture.bankapp.network.api + +import com.accenture.bankapp.network.models.LoginResponse +import com.accenture.bankapp.network.models.StatementsResponse +import retrofit2.Response +import retrofit2.http.* + +interface ApiService { + @FormUrlEncoded + @POST("Login") + suspend fun requestLogin(@Field("user") user: String, @Field("password") password: String): Response + + @GET("statements/{userId}") + suspend fun getStatements(@Path("userId") userId: Int): Response +} \ No newline at end of file diff --git a/BankApp/app/src/main/java/com/accenture/bankapp/network/api/RetrofitBuilder.kt b/BankApp/app/src/main/java/com/accenture/bankapp/network/api/RetrofitBuilder.kt new file mode 100644 index 000000000..22bd6ec33 --- /dev/null +++ b/BankApp/app/src/main/java/com/accenture/bankapp/network/api/RetrofitBuilder.kt @@ -0,0 +1,20 @@ +package com.accenture.bankapp.network.api + +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import timber.log.Timber + +object RetrofitBuilder { + private const val API_URL = "https://bank-app-test.herokuapp.com/api/" + + private fun getRetrofit(): Retrofit { + Timber.i("getRetrofit: Building Retrofit with URL $API_URL") + + return Retrofit.Builder() + .baseUrl(API_URL) + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + + val apiService: ApiService = getRetrofit().create(ApiService::class.java) +} \ No newline at end of file diff --git a/BankApp/app/src/main/java/com/accenture/bankapp/network/models/Error.kt b/BankApp/app/src/main/java/com/accenture/bankapp/network/models/Error.kt new file mode 100644 index 000000000..caa485832 --- /dev/null +++ b/BankApp/app/src/main/java/com/accenture/bankapp/network/models/Error.kt @@ -0,0 +1,10 @@ +package com.accenture.bankapp.network.models + +import com.google.gson.annotations.SerializedName + +data class Error( + @SerializedName("code") + var code: Int, + @SerializedName("message") + var message: String, +) \ No newline at end of file diff --git a/BankApp/app/src/main/java/com/accenture/bankapp/network/models/LoginResponse.kt b/BankApp/app/src/main/java/com/accenture/bankapp/network/models/LoginResponse.kt new file mode 100644 index 000000000..46dc743e7 --- /dev/null +++ b/BankApp/app/src/main/java/com/accenture/bankapp/network/models/LoginResponse.kt @@ -0,0 +1,10 @@ +package com.accenture.bankapp.network.models + +import com.google.gson.annotations.SerializedName + +data class LoginResponse( + @SerializedName("userAccount") + var userAccount: UserAccount, + @SerializedName("error") + var error: Error, +) diff --git a/BankApp/app/src/main/java/com/accenture/bankapp/network/models/Statement.kt b/BankApp/app/src/main/java/com/accenture/bankapp/network/models/Statement.kt new file mode 100644 index 000000000..122770a0b --- /dev/null +++ b/BankApp/app/src/main/java/com/accenture/bankapp/network/models/Statement.kt @@ -0,0 +1,14 @@ +package com.accenture.bankapp.network.models + +import com.google.gson.annotations.SerializedName + +data class Statement( + @SerializedName("title") + var title: String, + @SerializedName("desc") + var desc: String, + @SerializedName("date") + var date: String, + @SerializedName("value") + var value: Float, +) \ No newline at end of file diff --git a/BankApp/app/src/main/java/com/accenture/bankapp/network/models/StatementsResponse.kt b/BankApp/app/src/main/java/com/accenture/bankapp/network/models/StatementsResponse.kt new file mode 100644 index 000000000..0e107db8a --- /dev/null +++ b/BankApp/app/src/main/java/com/accenture/bankapp/network/models/StatementsResponse.kt @@ -0,0 +1,10 @@ +package com.accenture.bankapp.network.models + +import com.google.gson.annotations.SerializedName + +data class StatementsResponse( + @SerializedName("statementList") + var statementList: List, + @SerializedName("error") + var error: Error, +) diff --git a/BankApp/app/src/main/java/com/accenture/bankapp/network/models/UserAccount.kt b/BankApp/app/src/main/java/com/accenture/bankapp/network/models/UserAccount.kt new file mode 100644 index 000000000..1f8933b65 --- /dev/null +++ b/BankApp/app/src/main/java/com/accenture/bankapp/network/models/UserAccount.kt @@ -0,0 +1,17 @@ +package com.accenture.bankapp.network.models + +import com.google.gson.annotations.SerializedName +import java.io.Serializable + +data class UserAccount( + @SerializedName("userId") + var userId: Int, + @SerializedName("name") + var name: String, + @SerializedName("bankAccount") + var bankAccount: String, + @SerializedName("agency") + var agency: String, + @SerializedName("balance") + var balance: Float, +) : Serializable \ No newline at end of file diff --git a/BankApp/app/src/main/java/com/accenture/bankapp/screens/dashboard/DashboardConfigurator.kt b/BankApp/app/src/main/java/com/accenture/bankapp/screens/dashboard/DashboardConfigurator.kt new file mode 100644 index 000000000..cb626116f --- /dev/null +++ b/BankApp/app/src/main/java/com/accenture/bankapp/screens/dashboard/DashboardConfigurator.kt @@ -0,0 +1,26 @@ +package com.accenture.bankapp.screens.dashboard + +import timber.log.Timber +import java.lang.ref.WeakReference + +object DashboardConfigurator { + fun configureFragment(dashboardFragment: DashboardFragment) { + Timber.i("configureFragment: Configuring the Dashboard Fragment") + + val dashboardRouter = DashboardRouter() + val dashboardPresenter = DashboardPresenter() + val dashboardInteractor = DashboardInteractor() + + dashboardRouter.dashboardFragment = WeakReference(dashboardFragment) + dashboardFragment.dashboardRouter = dashboardRouter + + dashboardPresenter.dashboardFragmentInput = WeakReference(dashboardFragment) + + dashboardInteractor.dashboardFragment = WeakReference(dashboardFragment) + dashboardInteractor.dashboardFragmentInput = WeakReference(dashboardFragment) + dashboardInteractor.dashboardPresenterInput = dashboardPresenter + + dashboardFragment.dashboardRouter = dashboardRouter + dashboardFragment.dashboardInteractorInput = dashboardInteractor + } +} \ No newline at end of file diff --git a/BankApp/app/src/main/java/com/accenture/bankapp/screens/dashboard/DashboardFragment.kt b/BankApp/app/src/main/java/com/accenture/bankapp/screens/dashboard/DashboardFragment.kt new file mode 100644 index 000000000..639bf1e73 --- /dev/null +++ b/BankApp/app/src/main/java/com/accenture/bankapp/screens/dashboard/DashboardFragment.kt @@ -0,0 +1,128 @@ +package com.accenture.bankapp.screens.dashboard + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.accenture.bankapp.R +import com.accenture.bankapp.network.models.Statement +import com.accenture.bankapp.network.models.UserAccount +import com.accenture.bankapp.screens.login.LoginFragment +import timber.log.Timber +import java.text.MessageFormat +import java.text.NumberFormat +import java.util.* + +interface DashboardFragmentInput { + fun displayDashboardMetadata(dashboardViewModel: DashboardViewModel) + fun enableInfo(info: String) + fun disableInfo() +} + +interface DashboardFragmentListener { + fun startLoginFragment(loginFragment: LoginFragment) +} + +class DashboardFragment : Fragment(), DashboardFragmentInput { + var listStatements: MutableList = mutableListOf() + lateinit var statementsRecyclerAdapter: StatementsRecyclerAdapter + + lateinit var dashboardRouter: DashboardRouter + lateinit var dashboardInteractorInput: DashboardInteractorInput + lateinit var dashboardFragmentListener: DashboardFragmentListener + lateinit var userAccount: UserAccount + + lateinit var textName: TextView + lateinit var textAccountAgency: TextView + lateinit var textBalance: TextView + lateinit var textDashboardInfo: TextView + lateinit var recyclerStatements: RecyclerView + lateinit var buttonLogout: ImageButton + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + Timber.i("onCreateView: Creating the Dashboard Fragment View") + + val view = inflater.inflate(R.layout.fragment_dashboard, container, false) + userAccount = this.arguments?.getSerializable("userAccount") as UserAccount + + DashboardConfigurator.configureFragment(this) + bindViews(view) + configureViews() + displayUserData() + fetchData() + + return view + } + + override fun onAttach(context: Context) { + super.onAttach(context) + + try { + dashboardFragmentListener = activity as DashboardFragmentListener + } catch (e: ClassCastException) { + throw ClassCastException(activity!!.toString() + " must implement DashboardFragmentListener") + } + } + + override fun displayDashboardMetadata(dashboardViewModel: DashboardViewModel) { + Timber.i("displayDashboardMetaData: Displaying Dashboard metadata") + + listStatements.addAll(dashboardViewModel.listStatements!!) + + statementsRecyclerAdapter.notifyDataSetChanged() + } + + override fun enableInfo(info: String) { + textDashboardInfo.text = info + recyclerStatements.visibility = View.GONE + textDashboardInfo.visibility = View.VISIBLE + } + + override fun disableInfo() { + textDashboardInfo.text = "" + textDashboardInfo.visibility = View.GONE + recyclerStatements.visibility = View.VISIBLE + } + + private fun bindViews(view: View) { + Timber.i("bindViews: Binding Dashboard Fragment views") + + textName = view.findViewById(R.id.textName) + textAccountAgency = view.findViewById(R.id.textAccountAgency) + textBalance = view.findViewById(R.id.textBalance) + textDashboardInfo = view.findViewById(R.id.textDashboardInfo) + recyclerStatements = view.findViewById(R.id.recyclerStatements) + buttonLogout = view.findViewById(R.id.buttonLogout) + } + + private fun configureViews() { + Timber.i("configureViews: Configuring the Dashboard Fragment views") + + statementsRecyclerAdapter = StatementsRecyclerAdapter(this.context!!, listStatements) + recyclerStatements.layoutManager = LinearLayoutManager(this.context) + recyclerStatements.adapter = statementsRecyclerAdapter + + buttonLogout.setOnClickListener(dashboardRouter) + } + + private fun displayUserData() { + Timber.i("displayUserData: Displaying user data") + + val formattedAgency = MessageFormat("{1}{2}.{3}{4}{5}{6}{7}{8}-{9}").format(userAccount.agency.split("").toTypedArray()) + val formattedBalance = NumberFormat.getCurrencyInstance(Locale("pt", "BR")).format(userAccount.balance) + + textName.text = userAccount.name + textAccountAgency.text = String.format("%s / %s", userAccount.bankAccount, formattedAgency) + textBalance.text = formattedBalance + } + + private fun fetchData() { + dashboardInteractorInput.fetchDashboardData(DashboardRequest(userAccount.userId)) + } +} \ No newline at end of file diff --git a/BankApp/app/src/main/java/com/accenture/bankapp/screens/dashboard/DashboardInteractor.kt b/BankApp/app/src/main/java/com/accenture/bankapp/screens/dashboard/DashboardInteractor.kt new file mode 100644 index 000000000..f11e200f0 --- /dev/null +++ b/BankApp/app/src/main/java/com/accenture/bankapp/screens/dashboard/DashboardInteractor.kt @@ -0,0 +1,40 @@ +package com.accenture.bankapp.screens.dashboard + +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import timber.log.Timber +import java.lang.ref.WeakReference + +interface DashboardInteractorInput { + fun fetchDashboardData(dashboardRequest: DashboardRequest) +} + +class DashboardInteractor : DashboardInteractorInput { + private val mainScope = CoroutineScope(Dispatchers.Main + CoroutineName("DashboardInteractorMainScope")) + private var statementsWorkerInput: StatementsWorkerInput? = null + get() { + return field ?: StatementsWorker() + } + + var dashboardFragment: WeakReference? = null + var dashboardFragmentInput: WeakReference? = null + var dashboardPresenterInput: DashboardPresenterInput? = null + + override fun fetchDashboardData(dashboardRequest: DashboardRequest) { + mainScope.launch { + Timber.i("fetchDashboardData: Fetching Dashboard data") + + val listStatements = statementsWorkerInput?.getListStatements(dashboardFragment?.get()?.context!!, dashboardRequest.userId!!) + val dashboardResponse = DashboardResponse(listStatements) + + if (dashboardResponse.listStatements == null || dashboardResponse.listStatements!!.isEmpty()) { + Timber.i("fetchDashboardData: Statements list is null or empty") + dashboardFragmentInput?.get()?.enableInfo("Você não possui transações recentes") + } else { + dashboardPresenterInput?.presentDashboardMetadata(dashboardResponse) + } + } + } +} \ No newline at end of file diff --git a/BankApp/app/src/main/java/com/accenture/bankapp/screens/dashboard/DashboardModels.kt b/BankApp/app/src/main/java/com/accenture/bankapp/screens/dashboard/DashboardModels.kt new file mode 100644 index 000000000..6018086db --- /dev/null +++ b/BankApp/app/src/main/java/com/accenture/bankapp/screens/dashboard/DashboardModels.kt @@ -0,0 +1,16 @@ +package com.accenture.bankapp.screens.dashboard + +import com.accenture.bankapp.network.models.Statement +import java.util.* + +data class DashboardViewModel( + var listStatements: ArrayList? = null +) + +data class DashboardRequest( + var userId: Int? = null +) + +data class DashboardResponse( + var listStatements: ArrayList? = null +) \ No newline at end of file diff --git a/BankApp/app/src/main/java/com/accenture/bankapp/screens/dashboard/DashboardPresenter.kt b/BankApp/app/src/main/java/com/accenture/bankapp/screens/dashboard/DashboardPresenter.kt new file mode 100644 index 000000000..38dc7626f --- /dev/null +++ b/BankApp/app/src/main/java/com/accenture/bankapp/screens/dashboard/DashboardPresenter.kt @@ -0,0 +1,19 @@ +package com.accenture.bankapp.screens.dashboard + +import timber.log.Timber +import java.lang.ref.WeakReference + +interface DashboardPresenterInput { + fun presentDashboardMetadata(dashboardResponse: DashboardResponse) +} + +class DashboardPresenter : DashboardPresenterInput { + var dashboardFragmentInput: WeakReference? = null + + override fun presentDashboardMetadata(dashboardResponse: DashboardResponse) { + Timber.i("presentDashboardMetadata: Presenting Dashboard metadata") + + dashboardFragmentInput?.get()?.disableInfo() + dashboardFragmentInput?.get()?.displayDashboardMetadata(DashboardViewModel(dashboardResponse.listStatements)) + } +} \ No newline at end of file diff --git a/BankApp/app/src/main/java/com/accenture/bankapp/screens/dashboard/DashboardRouter.kt b/BankApp/app/src/main/java/com/accenture/bankapp/screens/dashboard/DashboardRouter.kt new file mode 100644 index 000000000..f0b796fb0 --- /dev/null +++ b/BankApp/app/src/main/java/com/accenture/bankapp/screens/dashboard/DashboardRouter.kt @@ -0,0 +1,13 @@ +package com.accenture.bankapp.screens.dashboard + +import android.view.View +import com.accenture.bankapp.screens.login.LoginFragment +import java.lang.ref.WeakReference + +class DashboardRouter: View.OnClickListener { + var dashboardFragment: WeakReference? = null + + override fun onClick(v: View?) { + dashboardFragment?.get()?.dashboardFragmentListener?.startLoginFragment(LoginFragment()) + } +} \ No newline at end of file diff --git a/BankApp/app/src/main/java/com/accenture/bankapp/screens/dashboard/StatementsRecyclerAdapter.kt b/BankApp/app/src/main/java/com/accenture/bankapp/screens/dashboard/StatementsRecyclerAdapter.kt new file mode 100644 index 000000000..1822d850d --- /dev/null +++ b/BankApp/app/src/main/java/com/accenture/bankapp/screens/dashboard/StatementsRecyclerAdapter.kt @@ -0,0 +1,58 @@ +package com.accenture.bankapp.screens.dashboard + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.accenture.bankapp.R +import com.accenture.bankapp.network.models.Statement +import timber.log.Timber +import java.text.MessageFormat +import java.text.NumberFormat +import java.util.* + +internal class StatementViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val textTitle: TextView = itemView.findViewById(R.id.textTitle) + val textDate: TextView = itemView.findViewById(R.id.textDate) + val textDesc: TextView = itemView.findViewById(R.id.textDesc) + val textValue: TextView = itemView.findViewById(R.id.textValue) +} + +class StatementsRecyclerAdapter( + private var context: Context, + private var listStatements: MutableList +) : RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + Timber.i("onCreateViewHolder: Creating View Holder") + + return StatementViewHolder( + LayoutInflater.from(context).inflate( + R.layout.card_statement, + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + if (holder is StatementViewHolder) { + val statement = listStatements[position] + + Timber.i("onBindViewHolder: Binding Holder $holder with Statement $statement") + + val formattedDate = MessageFormat("{2}/{1}/{0}").format(statement.date.split("-").toTypedArray()) + val formattedValue = NumberFormat.getCurrencyInstance(Locale("pt", "BR")).format(statement.value) + + holder.textTitle.text = statement.title + holder.textDate.text = formattedDate + holder.textDesc.text = statement.desc + holder.textValue.text = formattedValue + } + } + + override fun getItemCount(): Int { + return listStatements.size + } +} \ No newline at end of file diff --git a/BankApp/app/src/main/java/com/accenture/bankapp/screens/dashboard/StatementsWorker.kt b/BankApp/app/src/main/java/com/accenture/bankapp/screens/dashboard/StatementsWorker.kt new file mode 100644 index 000000000..a29ffb14c --- /dev/null +++ b/BankApp/app/src/main/java/com/accenture/bankapp/screens/dashboard/StatementsWorker.kt @@ -0,0 +1,65 @@ +package com.accenture.bankapp.screens.dashboard + +import android.content.Context +import android.widget.Toast +import com.accenture.bankapp.network.api.RetrofitBuilder +import com.accenture.bankapp.network.models.Statement +import kotlinx.coroutines.* +import timber.log.Timber +import java.util.* + +interface StatementsWorkerInput { + suspend fun getListStatements(context: Context, userId: Int): ArrayList +} + +class StatementsWorker : StatementsWorkerInput { + private val ioScope = CoroutineScope(Dispatchers.IO + CoroutineName("StatementsWorkerIOScope")) + private val mainScope = CoroutineScope(Dispatchers.Main + CoroutineName("StatementsWorkerMainScope")) + private val apiService = RetrofitBuilder.apiService + + override suspend fun getListStatements(context: Context, userId: Int): ArrayList { + val listStatements = withContext(ioScope.coroutineContext) { + try { + Timber.i("getListStatements: Trying to get statements from userId $userId") + + val response = apiService.getStatements(userId) + + return@withContext mainScope.async { + try { + if (response.isSuccessful) { + if (response.body()?.error?.code ?: 0 == 0) { + val listStatements = response.body()?.statementList as ArrayList + + Timber.i("${this.coroutineContext[CoroutineName]?.name}: Get successfully. Number of statements: ${listStatements.size}") + + return@async listStatements + } else { + val error = response.body()?.error!! + + Timber.i("${this.coroutineContext[CoroutineName]?.name}: Get failed: ${error.code}: ${error.message}") + Toast.makeText(context, error.message, Toast.LENGTH_SHORT).show() + } + } else { + val error = "Get failed: ${response.code()}" + + Timber.e("${this.coroutineContext[CoroutineName]?.name}: $error") + Toast.makeText(context, error, Toast.LENGTH_SHORT).show() + } + } catch (t: Throwable) { + val error = "Error while returning the statements list" + + Timber.e(t, "${this.coroutineContext[CoroutineName]?.name}: $error") + Toast.makeText(context, error, Toast.LENGTH_SHORT).show() + } + }.await() + } catch (t: Throwable) { + val error = "Error getting statements" + + Timber.e(t, "${this.coroutineContext[CoroutineName]?.name}: $error") + Toast.makeText(context, error, Toast.LENGTH_SHORT).show() + } + } + + return listStatements as ArrayList + } +} \ No newline at end of file diff --git a/BankApp/app/src/main/java/com/accenture/bankapp/screens/login/LoginConfigurator.kt b/BankApp/app/src/main/java/com/accenture/bankapp/screens/login/LoginConfigurator.kt new file mode 100644 index 000000000..02f97c46f --- /dev/null +++ b/BankApp/app/src/main/java/com/accenture/bankapp/screens/login/LoginConfigurator.kt @@ -0,0 +1,15 @@ +package com.accenture.bankapp.screens.login + +import timber.log.Timber +import java.lang.ref.WeakReference + +object LoginConfigurator { + fun configureFragment(loginFragment: LoginFragment) { + Timber.i("configureFragment: Configuring the Login Fragment") + + val loginRouter = LoginRouter() + loginRouter.loginFragment = WeakReference(loginFragment) + loginRouter.loginFragmentInput = WeakReference(loginFragment) + loginFragment.loginRouter = loginRouter + } +} \ No newline at end of file diff --git a/BankApp/app/src/main/java/com/accenture/bankapp/screens/login/LoginFragment.kt b/BankApp/app/src/main/java/com/accenture/bankapp/screens/login/LoginFragment.kt new file mode 100644 index 000000000..1ab425606 --- /dev/null +++ b/BankApp/app/src/main/java/com/accenture/bankapp/screens/login/LoginFragment.kt @@ -0,0 +1,188 @@ +package com.accenture.bankapp.screens.login + +import android.content.Context +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.os.Build +import android.os.Bundle +import android.util.Patterns +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.EditText +import android.widget.ProgressBar +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat +import androidx.fragment.app.Fragment +import br.com.colman.simplecpfvalidator.isCpf +import com.accenture.bankapp.R +import com.accenture.bankapp.screens.dashboard.DashboardFragment +import com.google.android.material.textfield.TextInputLayout +import timber.log.Timber +import java.util.regex.Pattern + +interface LoginFragmentInput { + fun verifyUser() + fun verifyPassword() + fun enableLoading() + fun disableLoading() + fun enableError(error: String) + fun disableError() +} + +interface LoginFragmentListener { + fun startDashboardFragment(dashboardFragment: DashboardFragment) +} + +class LoginFragment : Fragment(), LoginFragmentInput { + lateinit var loginRouter: LoginRouter + lateinit var loginFragmentListener: LoginFragmentListener + + lateinit var inputUser: TextInputLayout + lateinit var inputUserEditText: EditText + lateinit var inputPassword: TextInputLayout + lateinit var inputPasswordEditText: EditText + lateinit var textLoginError: TextView + lateinit var progressLoading: ProgressBar + lateinit var buttonLogin: Button + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + Timber.i("onCreateView: Creating the Login Fragment View") + + val view = inflater.inflate(R.layout.fragment_login, container, false) + + LoginConfigurator.configureFragment(this) + bindViews(view) + configureViews() + + return view + } + + override fun onAttach(context: Context) { + super.onAttach(context) + + try { + loginFragmentListener = activity as LoginFragmentListener + } catch (e: ClassCastException) { + throw ClassCastException(activity!!.toString() + " must implement LoginFragmentListener") + } + } + + override fun verifyUser() { + Timber.i("verifyUser: Validating the User Input") + + val error = "User must be a valid email or CPF" + val user = inputUser.editText?.text.toString() + + if (Patterns.EMAIL_ADDRESS.matcher(user).matches() || user.isCpf(listOf('.', '/'))) { + Timber.i("verifyUser: $user is a valid user") + inputUser.isErrorEnabled = false + inputUser.error = "" + } else { + Timber.i("verifyUser: $user is a invalid user") + inputUser.isErrorEnabled = true + inputUser.error = error + } + } + + override fun verifyPassword() { + Timber.i("verifyPassword: Validating the Password Input") + + val passwordPattern = Pattern.compile("^(?=.*[A-Z])(?=.*[!@\$%^&])(?=.*[a-zA-Z0-9]).*\$") + val error = "Password must have at least one capital letter, one special character and one alphanumeric" + val password = inputPassword.editText?.text.toString() + + if (passwordPattern.matcher(password).matches()) { + Timber.i("verifyPassword: $password is a valid password") + inputPassword.isErrorEnabled = false + inputPassword.error = "" + } else { + Timber.i("verifyPassword: $password is a invalid password") + inputPassword.isErrorEnabled = true + inputPassword.error = error + } + } + + override fun enableLoading() { + progressLoading.visibility = View.VISIBLE + } + + override fun disableLoading() { + progressLoading.visibility = View.GONE + } + + override fun enableError(error: String) { + textLoginError.text = error + textLoginError.visibility = View.VISIBLE + } + + override fun disableError() { + textLoginError.text = "" + textLoginError.visibility = View.GONE + } + + private fun bindViews(view: View) { + Timber.i("bindViews: Binding Login Fragment views") + + inputUser = view.findViewById(R.id.inputUser) + inputUserEditText = inputUser.editText!! + inputPassword = view.findViewById(R.id.inputPassword) + inputPasswordEditText = inputPassword.editText!! + textLoginError = view.findViewById(R.id.textLoginError) + progressLoading = view.findViewById(R.id.progressLoading) + buttonLogin = view.findViewById(R.id.buttonLogin) + } + + private fun configureViews() { + Timber.i("configureViews: Configuring the Login Fragment views") + + val sharedPreferences = this.context!!.getSharedPreferences(this.context!!.packageName, Context.MODE_PRIVATE) + val user = sharedPreferences.getString("user", null) + val password = sharedPreferences.getString("password", null) + + if (user != null && password != null) { + inputUserEditText.setText(user) + inputPasswordEditText.setText(password) + } + + inputUserEditText.setOnEditorActionListener(loginRouter) + inputUserEditText.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + disableError() + verifyUser() + } + } + + inputPasswordEditText.setOnEditorActionListener(loginRouter) + inputPasswordEditText.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + disableError() + verifyPassword() + } + } + + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> { + progressLoading.indeterminateDrawable.colorFilter = PorterDuffColorFilter( + this.context!!.getColor(R.color.primary_color), + PorterDuff.Mode.SRC_IN + ) + } + Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1 -> { + progressLoading.indeterminateDrawable.colorFilter = PorterDuffColorFilter( + this.context!!.resources.getColor(R.color.primary_color), + PorterDuff.Mode.SRC_IN + ) + } + else -> { + val drawableProgress = DrawableCompat.wrap(progressLoading.indeterminateDrawable) + DrawableCompat.setTint(drawableProgress, ContextCompat.getColor(this.context!!, R.color.primary_color)) + progressLoading.indeterminateDrawable = DrawableCompat.unwrap(drawableProgress) + } + } + + buttonLogin.setOnClickListener(loginRouter) + } +} \ No newline at end of file diff --git a/BankApp/app/src/main/java/com/accenture/bankapp/screens/login/LoginRouter.kt b/BankApp/app/src/main/java/com/accenture/bankapp/screens/login/LoginRouter.kt new file mode 100644 index 000000000..4a515e562 --- /dev/null +++ b/BankApp/app/src/main/java/com/accenture/bankapp/screens/login/LoginRouter.kt @@ -0,0 +1,116 @@ +package com.accenture.bankapp.screens.login + +import android.content.Context +import android.os.Bundle +import android.view.KeyEvent +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.TextView +import android.widget.Toast +import com.accenture.bankapp.network.api.RetrofitBuilder +import com.accenture.bankapp.screens.dashboard.DashboardFragment +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import timber.log.Timber +import java.lang.ref.WeakReference + +class LoginRouter : View.OnClickListener, TextView.OnEditorActionListener { + private val ioScope = CoroutineScope(Dispatchers.IO + CoroutineName("LoginRouterIOScope")) + private val mainScope = CoroutineScope(Dispatchers.Main + CoroutineName("LoginRouterMainScope")) + private val apiService = RetrofitBuilder.apiService + + var loginFragment: WeakReference? = null + var loginFragmentInput: WeakReference? = null + + override fun onClick(v: View?) { + performLogin() + } + + override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { + if (actionId == EditorInfo.IME_ACTION_DONE) { + performLogin() + + return true + } + + return false + } + + private fun saveSession(context: Context, user: String, password: String) { + Timber.i("saveSession: Saving user session") + + val sharedPreferences = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE) + + sharedPreferences.edit().putString("user", user).putString("password", password).apply() + } + + private fun performLogin() { + val context = loginFragment?.get()?.context + val inputUser = loginFragment?.get()?.inputUser + val inputUserText = loginFragment?.get()?.inputUserEditText?.text.toString() + val inputPassword = loginFragment?.get()?.inputPassword + val inputPasswordText = loginFragment?.get()?.inputPasswordEditText?.text.toString() + + loginFragmentInput?.get()?.verifyUser() + loginFragmentInput?.get()?.verifyPassword() + + if (inputUser!!.isErrorEnabled || inputPassword!!.isErrorEnabled) { + Timber.i("onClick: The user $inputUserText or password $inputPasswordText is invalid") + return + } + + loginFragmentInput?.get()?.enableLoading() + + ioScope.launch { + try { + Timber.i("onClick: Trying to login with user $inputUserText and password $inputPasswordText") + + val response = apiService.requestLogin(inputUserText, inputPasswordText) + + mainScope.launch { + try { + loginFragmentInput?.get()?.disableLoading() + + if (response.isSuccessful) { + if (response.body()?.error?.code ?: 0 == 0) { + val userAccount = response.body()?.userAccount!! + val dashboardFragment = DashboardFragment() + val args = Bundle() + + Timber.i("${this.coroutineContext[CoroutineName]?.name}: Login successfully. User: $userAccount") + + saveSession(context!!, inputUserText, inputPasswordText) + + args.putSerializable("userAccount", userAccount) + dashboardFragment.arguments = args + loginFragment?.get()?.loginFragmentListener?.startDashboardFragment(dashboardFragment) + } else { + val error = response.body()?.error!! + + Timber.i("${this.coroutineContext[CoroutineName]?.name}: Login failed: ${error.code}: ${error.message}") + loginFragmentInput?.get()?.enableError(error.message) + } + } else { + val error = "Request failed: ${response.code()}" + + Timber.e("${this.coroutineContext[CoroutineName]?.name}: $error") + Toast.makeText(context, error, Toast.LENGTH_SHORT).show() + } + } catch (t: Throwable) { + val error = "Error updating the UI" + + Timber.e(t, "${this.coroutineContext[CoroutineName]?.name}: $error") + Toast.makeText(context, error, Toast.LENGTH_SHORT).show() + } + } + } catch (t: Throwable) { + val error = "Error requesting login" + + Timber.e(t, "${this.coroutineContext[CoroutineName]?.name}: $error") + Toast.makeText(context, error, Toast.LENGTH_SHORT).show() + } + } + } +} \ No newline at end of file diff --git a/BankApp/app/src/main/java/com/accenture/bankapp/utils/FragmentUtils.kt b/BankApp/app/src/main/java/com/accenture/bankapp/utils/FragmentUtils.kt new file mode 100644 index 000000000..e77e95e69 --- /dev/null +++ b/BankApp/app/src/main/java/com/accenture/bankapp/utils/FragmentUtils.kt @@ -0,0 +1,10 @@ +package com.accenture.bankapp.utils + +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.FragmentTransaction + +inline fun AppCompatActivity.transact(action: FragmentTransaction.() -> Unit) { + supportFragmentManager.beginTransaction().apply { + action() + }.commit() +} \ No newline at end of file diff --git a/BankApp/app/src/main/res/color/login_text_input_layout_box_stroke.xml b/BankApp/app/src/main/res/color/login_text_input_layout_box_stroke.xml new file mode 100644 index 000000000..7fe66bc62 --- /dev/null +++ b/BankApp/app/src/main/res/color/login_text_input_layout_box_stroke.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/BankApp/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/BankApp/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/BankApp/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/BankApp/app/src/main/res/drawable/ic_launcher_background.xml b/BankApp/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/BankApp/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BankApp/app/src/main/res/drawable/logo.png b/BankApp/app/src/main/res/drawable/logo.png new file mode 100644 index 000000000..66bdc8d5d Binary files /dev/null and b/BankApp/app/src/main/res/drawable/logo.png differ diff --git a/BankApp/app/src/main/res/drawable/logout.png b/BankApp/app/src/main/res/drawable/logout.png new file mode 100644 index 000000000..de1e4ae3c Binary files /dev/null and b/BankApp/app/src/main/res/drawable/logout.png differ diff --git a/BankApp/app/src/main/res/layout/activity_main.xml b/BankApp/app/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..48c39beb7 --- /dev/null +++ b/BankApp/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/BankApp/app/src/main/res/layout/card_statement.xml b/BankApp/app/src/main/res/layout/card_statement.xml new file mode 100644 index 000000000..56d133180 --- /dev/null +++ b/BankApp/app/src/main/res/layout/card_statement.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/BankApp/app/src/main/res/layout/fragment_dashboard.xml b/BankApp/app/src/main/res/layout/fragment_dashboard.xml new file mode 100644 index 000000000..c6da0653f --- /dev/null +++ b/BankApp/app/src/main/res/layout/fragment_dashboard.xml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/BankApp/app/src/main/res/layout/fragment_login.xml b/BankApp/app/src/main/res/layout/fragment_login.xml new file mode 100644 index 000000000..ffd637e82 --- /dev/null +++ b/BankApp/app/src/main/res/layout/fragment_login.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + +