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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/BankApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/BankApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..eca70cfe5
--- /dev/null
+++ b/BankApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/BankApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/BankApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..eca70cfe5
--- /dev/null
+++ b/BankApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/BankApp/app/src/main/res/mipmap-hdpi/ic_launcher.png b/BankApp/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..a571e6009
Binary files /dev/null and b/BankApp/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/BankApp/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/BankApp/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 000000000..61da551c5
Binary files /dev/null and b/BankApp/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/BankApp/app/src/main/res/mipmap-mdpi/ic_launcher.png b/BankApp/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..c41dd2853
Binary files /dev/null and b/BankApp/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/BankApp/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/BankApp/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 000000000..db5080a75
Binary files /dev/null and b/BankApp/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/BankApp/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/BankApp/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..6dba46dab
Binary files /dev/null and b/BankApp/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/BankApp/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/BankApp/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..da31a871c
Binary files /dev/null and b/BankApp/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/BankApp/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/BankApp/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..15ac68172
Binary files /dev/null and b/BankApp/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/BankApp/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/BankApp/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..b216f2d31
Binary files /dev/null and b/BankApp/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/BankApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/BankApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..f25a41974
Binary files /dev/null and b/BankApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/BankApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/BankApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..e96783ccc
Binary files /dev/null and b/BankApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/BankApp/app/src/main/res/values/colors.xml b/BankApp/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..4a4940fdc
--- /dev/null
+++ b/BankApp/app/src/main/res/values/colors.xml
@@ -0,0 +1,9 @@
+
+
+ #3B49EE
+ #DCE2EE
+ #4DDBDFE3
+ #485465
+ #A8B4C4
+ #FF0000
+
\ No newline at end of file
diff --git a/BankApp/app/src/main/res/values/strings.xml b/BankApp/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..7bd60c4db
--- /dev/null
+++ b/BankApp/app/src/main/res/values/strings.xml
@@ -0,0 +1,9 @@
+
+ Bank App
+ User
+ Login
+ Password
+ Conta
+ Saldo
+ Recentes
+
\ No newline at end of file
diff --git a/BankApp/app/src/main/res/values/styles.xml b/BankApp/app/src/main/res/values/styles.xml
new file mode 100644
index 000000000..e963cea43
--- /dev/null
+++ b/BankApp/app/src/main/res/values/styles.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/BankApp/app/src/main/res/values/themes.xml b/BankApp/app/src/main/res/values/themes.xml
new file mode 100644
index 000000000..7251e540d
--- /dev/null
+++ b/BankApp/app/src/main/res/values/themes.xml
@@ -0,0 +1,6 @@
+
+
+
\ No newline at end of file
diff --git a/BankApp/app/src/test/java/com/accenture/bankapp/DashboardFragmentUnitTest.kt b/BankApp/app/src/test/java/com/accenture/bankapp/DashboardFragmentUnitTest.kt
new file mode 100644
index 000000000..7cb4bb3fd
--- /dev/null
+++ b/BankApp/app/src/test/java/com/accenture/bankapp/DashboardFragmentUnitTest.kt
@@ -0,0 +1,189 @@
+package com.accenture.bankapp
+
+import android.os.Bundle
+import android.os.Looper
+import android.widget.Button
+import androidx.appcompat.widget.LinearLayoutCompat
+import androidx.core.view.isVisible
+import com.accenture.bankapp.network.models.UserAccount
+import com.accenture.bankapp.screens.dashboard.DashboardFragment
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows.shadowOf
+import java.text.MessageFormat
+import java.text.NumberFormat
+import java.util.*
+
+@RunWith(RobolectricTestRunner::class)
+class DashboardFragmentUnitTest {
+ @Test
+ fun mainActivity_shouldNot_beNull() {
+ val mainActivity = Robolectric.buildActivity(MainActivity::class.java).create().start().resume().get()
+
+ Assert.assertNotNull(mainActivity)
+ }
+
+ @Test
+ fun loginFragment_shouldNot_beNull() {
+ val mainActivity = Robolectric.buildActivity(MainActivity::class.java).create().start().resume().get()
+ val userAccount = UserAccount(123, "Test Test Test", "1234", "123456789", 1000f)
+ val bundle = Bundle()
+ val fragmentManager = mainActivity.supportFragmentManager
+ val dashboardFragment = DashboardFragment()
+ val fragmentTransaction = fragmentManager.beginTransaction()
+
+ bundle.putSerializable("userAccount", userAccount)
+ dashboardFragment.arguments = bundle
+ fragmentTransaction.add(R.id.layoutContainer, dashboardFragment)
+ fragmentTransaction.commit()
+ shadowOf(Looper.getMainLooper()).idle()
+
+ Assert.assertNotNull(dashboardFragment)
+ }
+
+ @Test
+ fun loginFragment_should_displayUserData() {
+ val mainActivity = Robolectric.buildActivity(MainActivity::class.java).create().start().resume().get()
+ val userAccount = UserAccount(123, "Test Test Test", "1234", "123456789", 1000f)
+ val bundle = Bundle()
+ val fragmentManager = mainActivity.supportFragmentManager
+ val dashboardFragment = DashboardFragment()
+ val fragmentTransaction = fragmentManager.beginTransaction()
+
+ bundle.putSerializable("userAccount", userAccount)
+ dashboardFragment.arguments = bundle
+ fragmentTransaction.add(R.id.layoutContainer, dashboardFragment)
+ fragmentTransaction.commit()
+ shadowOf(Looper.getMainLooper()).idle()
+
+ val formattedAgency = MessageFormat("{1}{2}.{3}{4}{5}{6}{7}{8}-{9}").format(userAccount.agency.split("").toTypedArray())
+ val testName = userAccount.name
+ val testAccountAgency = String.format("%s / %s", userAccount.bankAccount, formattedAgency)
+ val testBalance = NumberFormat.getCurrencyInstance(Locale("pt", "BR")).format(userAccount.balance)
+
+ val textNameText = dashboardFragment.textName.text
+ val textAccountAgencyText = dashboardFragment.textAccountAgency.text
+ val textBalanceText = dashboardFragment.textBalance.text
+
+ Assert.assertEquals(textNameText, testName)
+ Assert.assertEquals(textAccountAgencyText, testAccountAgency)
+ Assert.assertEquals(textBalanceText, testBalance)
+ }
+
+ @Test
+ fun loginFragment_should_displayDashboardMetadata() {
+ val mainActivity = Robolectric.buildActivity(MainActivity::class.java).create().start().resume().get()
+ val userAccount = UserAccount(123, "Test Test Test", "1234", "123456789", 1000f)
+ val bundle = Bundle()
+ val fragmentManager = mainActivity.supportFragmentManager
+ val dashboardFragment = DashboardFragment()
+ val fragmentTransaction = fragmentManager.beginTransaction()
+
+ bundle.putSerializable("userAccount", userAccount)
+ dashboardFragment.arguments = bundle
+ fragmentTransaction.add(R.id.layoutContainer, dashboardFragment)
+ fragmentTransaction.commit()
+ shadowOf(Looper.getMainLooper()).idle()
+
+ runBlocking {
+ withContext(Dispatchers.Default) {
+ delay(5000L)
+ }
+ }
+ shadowOf(Looper.getMainLooper()).idle()
+
+ val listStatements = dashboardFragment.listStatements
+ val recyclerStatements = dashboardFragment.recyclerStatements
+
+ Assert.assertEquals(listStatements.size, recyclerStatements.adapter?.itemCount)
+ }
+
+ @Test
+ fun textInfo_should_beEnable() {
+ val mainActivity = Robolectric.buildActivity(MainActivity::class.java).create().start().resume().get()
+ val userAccount = UserAccount(123, "Test Test Test", "1234", "123456789", 1000f)
+ val bundle = Bundle()
+ val fragmentManager = mainActivity.supportFragmentManager
+ val dashboardFragment = DashboardFragment()
+ val fragmentTransaction = fragmentManager.beginTransaction()
+
+ bundle.putSerializable("userAccount", userAccount)
+ dashboardFragment.arguments = bundle
+ fragmentTransaction.add(R.id.layoutContainer, dashboardFragment)
+ fragmentTransaction.commit()
+ shadowOf(Looper.getMainLooper()).idle()
+
+ val textDashboardInfo = dashboardFragment.textDashboardInfo
+ val recyclerStatements = dashboardFragment.recyclerStatements
+ val testInfo = "Test info"
+
+ dashboardFragment.enableInfo(testInfo)
+
+ Assert.assertEquals(textDashboardInfo.text, testInfo)
+ Assert.assertTrue(textDashboardInfo.isVisible)
+ Assert.assertTrue(!recyclerStatements.isVisible)
+ }
+
+ @Test
+ fun textInfo_should_beDisable() {
+ val mainActivity = Robolectric.buildActivity(MainActivity::class.java).create().start().resume().get()
+ val userAccount = UserAccount(123, "Test Test Test", "1234", "123456789", 1000f)
+ val bundle = Bundle()
+ val fragmentManager = mainActivity.supportFragmentManager
+ val dashboardFragment = DashboardFragment()
+ val fragmentTransaction = fragmentManager.beginTransaction()
+
+ bundle.putSerializable("userAccount", userAccount)
+ dashboardFragment.arguments = bundle
+ fragmentTransaction.add(R.id.layoutContainer, dashboardFragment)
+ fragmentTransaction.commit()
+ shadowOf(Looper.getMainLooper()).idle()
+
+ val textDashboardInfo = dashboardFragment.textDashboardInfo
+ val recyclerStatements = dashboardFragment.recyclerStatements
+ val testInfo = "Test info"
+
+ dashboardFragment.enableInfo(testInfo)
+ dashboardFragment.disableInfo()
+
+ Assert.assertEquals(textDashboardInfo.text, "")
+ Assert.assertTrue(!textDashboardInfo.isVisible)
+ Assert.assertTrue(recyclerStatements.isVisible)
+ }
+
+ @Test
+ fun buttonLogout_should_performLogout() {
+ val mainActivity = Robolectric.buildActivity(MainActivity::class.java).create().start().resume().get()
+ val userAccount = UserAccount(123, "Test Test Test", "1234", "123456789", 1000f)
+ val bundle = Bundle()
+ val fragmentManager = mainActivity.supportFragmentManager
+ val dashboardFragment = DashboardFragment()
+ val fragmentTransaction = fragmentManager.beginTransaction()
+
+ bundle.putSerializable("userAccount", userAccount)
+ dashboardFragment.arguments = bundle
+ fragmentTransaction.add(R.id.layoutContainer, dashboardFragment)
+ fragmentTransaction.commit()
+ shadowOf(Looper.getMainLooper()).idle()
+
+ val buttonLogout = dashboardFragment.buttonLogout
+
+ buttonLogout.callOnClick()
+
+ runBlocking {
+ withContext(Dispatchers.Default) {
+ delay(5000L)
+ }
+ }
+ shadowOf(Looper.getMainLooper()).idle()
+
+ Assert.assertNotNull(mainActivity.findViewById(R.id.layoutContainer).findViewById