diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..aa724b770
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 000000000..26d33521a
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 000000000..12cd6dc87
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+Bank App
\ No newline at end of file
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 000000000..4c3743a3c
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 000000000..79ee123c2
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 000000000..61a9130cd
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 000000000..d87b21200
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 000000000..a5f05cd8c
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 000000000..d5d35ec44
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 000000000..35eb1ddfb
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Bank_App.postman_collection.json b/Bank_App.postman_collection.json
deleted file mode 100644
index 53882a1c6..000000000
--- a/Bank_App.postman_collection.json
+++ /dev/null
@@ -1,96 +0,0 @@
-{
- "info": {
- "_postman_id": "b90c97e1-4261-4a34-a348-a0604f0264a7",
- "name": "Bank App",
- "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
- },
- "item": [
- {
- "name": "Login",
- "request": {
- "method": "POST",
- "header": [
- {
- "key": "Content-Type",
- "name": "Content-Type",
- "value": "application/x-www-form-urlencoded",
- "type": "text"
- }
- ],
- "body": {
- "mode": "urlencoded",
- "urlencoded": [
- {
- "key": "user",
- "value": "test_user",
- "type": "text"
- },
- {
- "key": "password",
- "value": "Test@1",
- "type": "text"
- }
- ]
- },
- "url": {
- "raw": "https://bank-app-test.herokuapp.com/api/login",
- "protocol": "https",
- "host": [
- "bank-app-test",
- "herokuapp",
- "com"
- ],
- "path": [
- "api",
- "login"
- ]
- }
- },
- "response": []
- },
- {
- "name": "Statements",
- "request": {
- "method": "GET",
- "header": [
- {
- "key": "Content-Type",
- "name": "Content-Type",
- "value": "application/x-www-form-urlencoded",
- "type": "text"
- }
- ],
- "body": {
- "mode": "urlencoded",
- "urlencoded": [
- {
- "key": "user",
- "value": "4",
- "type": "text"
- },
- {
- "key": "password",
- "value": "asdfa",
- "type": "text"
- }
- ]
- },
- "url": {
- "raw": "https://bank-app-test.herokuapp.com/api/statements/1",
- "protocol": "https",
- "host": [
- "bank-app-test",
- "herokuapp",
- "com"
- ],
- "path": [
- "api",
- "statements",
- "1"
- ]
- }
- },
- "response": []
- }
- ]
-}
\ No newline at end of file
diff --git a/README.md b/README.md
index bd73feb5f..a0ee25d3b 100644
--- a/README.md
+++ b/README.md
@@ -1,41 +1,4 @@
-# Show me the code
+# Teste entrevista Santander
-Esse repositório contem todo o material necessário para realizar o teste:
-- A especificação do layout está na pasta 'bank_app_layout' abrindo o index.html, utilizar os Styles do Android
-
-- Os dados da Api estão mockados, os exemplos e a especificação dos serviços (login e statements) se encontram no arquivo BankApp.postman_collection.json ( é necessário instalar o postman e importar a colection https://www.getpostman.com/apps)
-
-
-
-### # DESAFIO:
-
-Na primeira tela teremos um formulario de login, o campo user deve aceitar email ou cpf,
-o campo password deve validar se a senha tem pelo menos uma letra maiuscula, um caracter especial e um caracter alfanumérico.
-Apos a validação, realizar o login no endpoint https://bank-app-test.herokuapp.com/api/login e exibir os dados de retorno na próxima tela.
-O ultimo usuário logado deve ser salvo de forma segura localmente, e exibido na tela de login se houver algum salvo.
-
-Na segunda tela será exibido os dados formatados do retorno do login e será necessário fazer um segundo request para obter os lançamentos do usuário, no endpoint https://bank-app-test.herokuapp.com/api/statements/{idUser} que retornará uma lista de lançamentos
-
-### # Avaliação
-
-Você será avaliado pela usabilidade, por respeitar o design e pela arquitetura do app. É esperado que você consiga explicar as decisões que tomou durante o desenvolvimento através de commits.
-
-Obrigatórios:
-
-* Java ou Kotlin
-* Material Design
-* O app deve funcionar a partir do android 4.4
-* Testes unitários, pode usar a ferramenta que você tem mais experiência, só nos explique o que ele tem de bom.
-* Arquitetura a ser utilizada: Android Clean Code (https://github.com/kmmraj/android-clean-code && https://medium.com/@kmmraj/android-clean-code-part-1-c66da6551d1)
-* Uso do git.
-
-### # Observações gerais
-
-Adicione um arquivo [README.md](http://README.md) com os procedimentos para executar o projeto.
-Pedimos que trabalhe sozinho e não divulgue o resultado na internet.
-
-Faça um fork desse desse repositório em seu Github e ao finalizar nos envie um Pull Request com o resultado, por favor informe por qual empresa você esta se candidatando.
-
-# Importante: não há prazo de entrega, faça com qualidade!
-
-# BOA SORTE!
+O projeto está na Master, para executá-lo basta apenas abrir o diretório raiz no android studio e compilar.
+Para rodar os testes de UI deve utilizar um emulador com API 26 > com as animações desabilitadas.
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 000000000..096c1ee96
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,89 @@
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+ id 'kotlin-kapt'
+ id 'kotlin-android-extensions'
+}
+
+android {
+ compileSdkVersion rootProject.ext.compile_sdk_version
+ buildToolsVersion rootProject.ext.build_tools_module_version
+
+ defaultConfig {
+ applicationId "com.jeanjnap.bankapp"
+ minSdkVersion rootProject.ext.min_sdk_version
+ targetSdkVersion rootProject.ext.target_sdk_version
+ versionCode 1
+ versionName "1.0"
+ multiDexEnabled true
+ vectorDrawables.useSupportLibrary = true
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = rootProject.ext.jvm_target_version
+ }
+ dataBinding {
+ enabled true
+ }
+ testOptions {
+ unitTests.includeAndroidResources = true
+ unitTests.all {
+ jvmArgs '-noverify'
+ }
+ }
+}
+
+dependencies {
+
+ implementation project(':data')
+ implementation project(':domain')
+ implementation project(':infrastructure')
+
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+ implementation "androidx.core:core-ktx:$core_ktx_version"
+ implementation "androidx.appcompat:appcompat:$appcompat_version"
+ implementation "com.google.android.material:material:$material_version"
+ implementation "androidx.constraintlayout:constraintlayout:$constraintlayout_version"
+ implementation "org.koin:koin-core:$koin_version"
+ implementation "org.koin:koin-androidx-scope:$koin_version"
+ implementation "org.koin:koin-androidx-viewmodel:$koin_version"
+ implementation "org.koin:koin-androidx-ext:$koin_version"
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
+ implementation "com.facebook.shimmer:shimmer:$shimmer_version"
+ kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_codegen_version"
+
+ testImplementation "junit:junit:$junit_4_version"
+ testImplementation "io.mockk:mockk:$mockk_version"
+ testImplementation "android.arch.core:core-testing:$lifecycle_version"
+ testImplementation "org.robolectric:robolectric:$robolectric_version"
+ testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
+ testImplementation "org.koin:koin-test:$koin_version"
+
+ debugImplementation "androidx.test.ext:junit:$junit_version"
+ debugImplementation "androidx.test.espresso:espresso-core:$espresso_version"
+ debugImplementation "androidx.test.espresso:espresso-core:$espresso_version"
+ debugImplementation "androidx.test.espresso:espresso-contrib:$espresso_version"
+ debugImplementation "androidx.test.espresso:espresso-idling-resource:$espresso_version"
+ debugImplementation "androidx.test.espresso:espresso-intents:$espresso_version"
+ debugImplementation "androidx.test.espresso:espresso-web:$espresso_version"
+ debugImplementation "androidx.test.ext:junit:$junit_version"
+ debugImplementation "androidx.test:runner:$androidx_runner_version"
+ debugImplementation "io.mockk:mockk-android:$mockk_version"
+ debugImplementation "io.mockk:mockk:$mockk_version"
+ debugImplementation "androidx.arch.core:core-testing:$arch_core_testing_version"
+ debugImplementation "org.koin:koin-test:$koin_version"
+ debugImplementation "androidx.test.uiautomator:uiautomator:$ui_automator_version"
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 000000000..481bb4348
--- /dev/null
+++ b/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/app/src/androidTest/java/com/jeanjnap/bankapp/BaseTest.kt b/app/src/androidTest/java/com/jeanjnap/bankapp/BaseTest.kt
new file mode 100644
index 000000000..56db34cb1
--- /dev/null
+++ b/app/src/androidTest/java/com/jeanjnap/bankapp/BaseTest.kt
@@ -0,0 +1,35 @@
+package com.jeanjnap.bankapp
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.jeanjnap.bankapp.di.AppModules
+import com.jeanjnap.bankapp.util.EspressoCoroutinesRule
+import com.jeanjnap.infrastructure.network.Network
+import io.mockk.MockKAnnotations
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import org.junit.Before
+import org.junit.Rule
+import org.koin.core.context.unloadKoinModules
+import org.koin.test.KoinTest
+
+open class BaseTest : KoinTest {
+
+ @Rule
+ @JvmField
+ val coroutinesRule = EspressoCoroutinesRule()
+
+ @Rule
+ @JvmField
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @MockK
+ protected lateinit var network: Network
+
+ @Before
+ fun initialization() {
+ MockKAnnotations.init(this)
+ unloadKoinModules(AppModules.viewModelModules)
+
+ every { network.hasActiveInternetConnection() } returns true
+ }
+}
diff --git a/app/src/androidTest/java/com/jeanjnap/bankapp/ui/login/LoginActivityRobot.kt b/app/src/androidTest/java/com/jeanjnap/bankapp/ui/login/LoginActivityRobot.kt
new file mode 100644
index 000000000..0dffcc0f0
--- /dev/null
+++ b/app/src/androidTest/java/com/jeanjnap/bankapp/ui/login/LoginActivityRobot.kt
@@ -0,0 +1,96 @@
+package com.jeanjnap.bankapp.ui.login
+
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.action.ViewActions.pressImeActionButton
+import androidx.test.espresso.action.ViewActions.typeText
+import androidx.test.espresso.intent.Intents.intended
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
+import androidx.test.espresso.matcher.ViewMatchers.withHint
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import com.jeanjnap.bankapp.R
+import com.jeanjnap.bankapp.ui.statements.StatementsActivity
+import com.jeanjnap.bankapp.util.extension.onView
+import com.jeanjnap.bankapp.util.extension.verify
+import com.jeanjnap.bankapp.util.extension.verifyText
+import com.jeanjnap.bankapp.util.matcher.hasTextInputLayoutError
+import com.jeanjnap.bankapp.util.matcher.withDrawable
+import com.jeanjnap.bankapp.util.matcher.withWidthSize
+
+fun loginActivityRobot(func: LoginActivityRobot.() -> Unit) = LoginActivityRobot().apply(func)
+
+class LoginActivityRobot {
+
+ fun elementsMustBeConfiguredCorrectly() = apply {
+ withId(R.id.iv_logo).onView {
+ verify(withWidthSize(124))
+ verify(withDrawable(R.drawable.logo))
+ }
+ withId(R.id.tiet_user).onView {
+ verify(withHint("User"))
+ verifyText(
+ R.color.cadet_blue,
+ "",
+ 15,
+ R.font.helvetica_neue
+ )
+ }
+ withId(R.id.tiet_pass).onView {
+ verify(withHint("Password"))
+ verifyText(
+ R.color.cadet_blue,
+ "",
+ 15,
+ R.font.helvetica_neue
+ )
+ }
+ withId(R.id.bt_login).onView {
+ verify(withWidthSize(190))
+ verify(withText("Login"))
+ }
+ }
+
+ fun verifyUsername(username: String) {
+ withId(R.id.tiet_user).onView {
+ verify(withHint("User"))
+ verifyText(
+ R.color.cadet_blue,
+ username,
+ 15,
+ R.font.helvetica_neue
+ )
+ }
+ }
+
+ fun typeUser(isValid: Boolean) {
+ withId(R.id.tiet_user).onView {
+ perform(click())
+ perform(typeText(if (isValid) "user@test.com" else "123"))
+ perform(pressImeActionButton())
+ }
+ }
+
+ fun typePass(isValid: Boolean) {
+ withId(R.id.tiet_pass).onView {
+ perform(click())
+ perform(typeText(if (isValid) "aA!" else "123"))
+ perform(pressImeActionButton())
+ }
+ }
+
+ fun hasUserErrorMessage(error: String?) {
+ withId(R.id.til_user).onView {
+ verify(hasTextInputLayoutError(error))
+ }
+ }
+
+ fun hasPassErrorMessage(error: String?) {
+ withId(R.id.til_pass).onView {
+ verify(hasTextInputLayoutError(error))
+ }
+ }
+
+ fun verifyStatementsScreen() = apply {
+ intended(hasComponent(StatementsActivity::class.java.name))
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/jeanjnap/bankapp/ui/login/LoginActivityTest.kt b/app/src/androidTest/java/com/jeanjnap/bankapp/ui/login/LoginActivityTest.kt
new file mode 100644
index 000000000..22b610de1
--- /dev/null
+++ b/app/src/androidTest/java/com/jeanjnap/bankapp/ui/login/LoginActivityTest.kt
@@ -0,0 +1,140 @@
+package com.jeanjnap.bankapp.ui.login
+
+import androidx.test.core.app.ActivityScenario
+import androidx.test.espresso.intent.Intents
+import com.jeanjnap.bankapp.BaseTest
+import com.jeanjnap.bankapp.ui.statements.StatementsViewModel
+import com.jeanjnap.domain.entity.Statement
+import com.jeanjnap.domain.entity.SuccessResponse
+import com.jeanjnap.domain.entity.UserAccount
+import com.jeanjnap.domain.usecase.BankUseCase
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.core.context.loadKoinModules
+import org.koin.dsl.module
+import java.math.BigDecimal
+import java.util.Date
+
+class LoginActivityTest : BaseTest() {
+
+ @MockK
+ private lateinit var bankUseCase: BankUseCase
+
+ private lateinit var activityScenario: ActivityScenario
+
+ private lateinit var activity: LoginActivity
+
+ private lateinit var viewModel: LoginViewModel
+
+ @Before
+ fun setup() {
+ viewModel = LoginViewModel(network, bankUseCase)
+
+ loadKoinModules(module {
+ viewModel { viewModel }
+ viewModel { StatementsViewModel(network, bankUseCase) }
+ })
+ Intents.init()
+ }
+
+ @After
+ fun tearDown() {
+ Intents.release()
+ activityScenario.close()
+ }
+
+ @Test
+ fun onCreate() {
+ every { bankUseCase.getUser() } returns null
+ startActivity()
+
+ loginActivityRobot {
+ elementsMustBeConfiguredCorrectly()
+ }
+ }
+
+ @Test
+ fun onCreate_withSavedUsername_shouldSetInUserField() {
+ val user = "user@test.com"
+ every { bankUseCase.getUser() } returns user
+ startActivity()
+
+ loginActivityRobot {
+ verifyUsername(user)
+ }
+ }
+
+ @Test
+ fun invalidFields() {
+ every { bankUseCase.getUser() } returns null
+ startActivity()
+
+ loginActivityRobot {
+ typeUser(false)
+ typePass(false)
+ hasUserErrorMessage("Usuário inválido")
+ hasPassErrorMessage("Senha inválida")
+ }
+ }
+
+ @Test
+ fun validFields() {
+ every { bankUseCase.getUser() } returns null
+ startActivity()
+
+ loginActivityRobot {
+ typeUser(true)
+ typePass(true)
+ hasUserErrorMessage(null)
+ hasPassErrorMessage(null)
+ }
+ }
+
+ @Test
+ fun loginSuccess() {
+ every { bankUseCase.getUser() } returns null
+ coEvery { bankUseCase.login(any(), any()) } returns SuccessResponse(mockUserAccount())
+ coEvery { bankUseCase.getStatements(any()) } returns SuccessResponse(mockStatementList())
+ startActivity()
+
+ loginActivityRobot {
+ typeUser(true)
+ typePass(true)
+ verifyStatementsScreen()
+ }
+ }
+
+ private fun mockUserAccount() = UserAccount(
+ userId = 1L,
+ name = "Jose da Silva Teste",
+ bankAccount = "1234",
+ agency = "123456",
+ balance = BigDecimal.TEN
+ )
+
+ private fun mockStatementList() = listOf(
+ mockStatement(),
+ mockStatement(),
+ mockStatement(),
+ mockStatement(),
+ mockStatement()
+ )
+
+ private fun mockStatement() = Statement(
+ title = "Pagamento",
+ desc = "Conta de luz",
+ date = Date(1610852400000),
+ value = BigDecimal.TEN
+ )
+
+ private fun startActivity() {
+ activityScenario = ActivityScenario.launch(LoginActivity::class.java).onActivity {
+ activity = it
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/jeanjnap/bankapp/ui/statements/StatementsActivityRobot.kt b/app/src/androidTest/java/com/jeanjnap/bankapp/ui/statements/StatementsActivityRobot.kt
new file mode 100644
index 000000000..d6aeef46c
--- /dev/null
+++ b/app/src/androidTest/java/com/jeanjnap/bankapp/ui/statements/StatementsActivityRobot.kt
@@ -0,0 +1,123 @@
+package com.jeanjnap.bankapp.ui.statements
+
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.intent.Intents.intended
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import com.jeanjnap.bankapp.R
+import com.jeanjnap.bankapp.ui.login.LoginActivity
+import com.jeanjnap.bankapp.util.extension.onView
+import com.jeanjnap.bankapp.util.extension.verify
+import com.jeanjnap.bankapp.util.extension.verifyText
+import com.jeanjnap.bankapp.util.matcher.RecyclerViewMatcher.Companion.withItemCount
+import com.jeanjnap.bankapp.util.matcher.RecyclerViewMatcher.Companion.withRecyclerViewItemIdAtPosition
+import com.jeanjnap.bankapp.util.matcher.withDrawable
+import com.jeanjnap.bankapp.util.matcher.withHeightSize
+import com.jeanjnap.bankapp.util.matcher.withWidthSize
+
+fun statementsActivityRobot(func: StatementsActivityRobot.() -> Unit) =
+ StatementsActivityRobot().apply(func)
+
+class StatementsActivityRobot {
+
+ fun elementsMustBeConfiguredCorrectly() = apply {
+ withId(R.id.tv_name).onView {
+ verifyText(
+ R.color.white,
+ "Jose da Silva Teste",
+ 25,
+ R.font.helvetica_neue_light
+ )
+ }
+ withId(R.id.iv_logout).onView {
+ verify(withWidthSize(28))
+ verify(withHeightSize(28))
+ verify(withDrawable(R.drawable.ic_logout))
+ }
+ withId(R.id.tv_account).onView {
+ verifyText(
+ R.color.white,
+ "Conta",
+ 12,
+ R.font.helvetica_neue
+ )
+ }
+ withId(R.id.tv_account_number_value).onView {
+ verifyText(
+ R.color.white,
+ "2050 / 01.111222-4",
+ 25,
+ R.font.helvetica_neue_light
+ )
+ }
+ withId(R.id.tv_balance).onView {
+ verifyText(
+ R.color.white,
+ "Saldo",
+ 12,
+ R.font.helvetica_neue
+ )
+ }
+ withId(R.id.tv_balance_value).onView {
+ verifyText(
+ R.color.white,
+ "R$ 10,00",
+ 25,
+ R.font.helvetica_neue_light
+ )
+ }
+ withId(R.id.tv_recent).onView {
+ verifyText(
+ R.color.river_bed,
+ "Recentes",
+ 16,
+ R.font.helvetica_neue
+ )
+ }
+ withId(R.id.rv_statements).onView {
+ verify(withItemCount(5))
+ }
+ withRecyclerViewItemIdAtPosition(R.id.rv_statements, 0, R.id.tv_title).onView {
+ verifyText(
+ R.color.cadet_blue,
+ "Pagamento",
+ 16,
+ R.font.helvetica_neue
+ )
+ }
+ withRecyclerViewItemIdAtPosition(R.id.rv_statements, 0, R.id.tv_date).onView {
+ verifyText(
+ R.color.cadet_blue,
+ "17/01/2021",
+ 12,
+ R.font.helvetica_neue
+ )
+ }
+ withRecyclerViewItemIdAtPosition(R.id.rv_statements, 0, R.id.tv_desc).onView {
+ verifyText(
+ R.color.river_bed,
+ "Conta de luz",
+ 16,
+ R.font.helvetica_neue
+ )
+ }
+ withRecyclerViewItemIdAtPosition(R.id.rv_statements, 0, R.id.tv_value).onView {
+ verifyText(
+ R.color.river_bed,
+ "R$ 10,00",
+ 20,
+ R.font.helvetica_neue_light
+ )
+ }
+ }
+
+ fun logout() {
+ withId(R.id.iv_logout).onView {
+ perform(click())
+ }
+ }
+
+ fun verifyLoginScreen() {
+ intended(hasComponent(LoginActivity::class.java.name))
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/jeanjnap/bankapp/ui/statements/StatementsActivityTest.kt b/app/src/androidTest/java/com/jeanjnap/bankapp/ui/statements/StatementsActivityTest.kt
new file mode 100644
index 000000000..f8bcbf2f4
--- /dev/null
+++ b/app/src/androidTest/java/com/jeanjnap/bankapp/ui/statements/StatementsActivityTest.kt
@@ -0,0 +1,113 @@
+package com.jeanjnap.bankapp.ui.statements
+
+import android.content.Intent
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ApplicationProvider.getApplicationContext
+import androidx.test.espresso.intent.Intents
+import com.jeanjnap.bankapp.BaseTest
+import com.jeanjnap.bankapp.ui.login.LoginViewModel
+import com.jeanjnap.domain.entity.Statement
+import com.jeanjnap.domain.entity.SuccessResponse
+import com.jeanjnap.domain.entity.UserAccount
+import com.jeanjnap.domain.usecase.BankUseCase
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.core.context.loadKoinModules
+import org.koin.dsl.module
+import java.math.BigDecimal
+import java.util.Date
+
+class StatementsActivityTest : BaseTest() {
+
+ @MockK
+ private lateinit var bankUseCase: BankUseCase
+
+ private lateinit var activityScenario: ActivityScenario
+
+ private lateinit var activity: StatementsActivity
+
+ private lateinit var viewModel: StatementsViewModel
+
+ @Before
+ fun setup() {
+ viewModel = StatementsViewModel(network, bankUseCase)
+
+ loadKoinModules(module {
+ viewModel { viewModel }
+ viewModel { LoginViewModel(network, bankUseCase) }
+ })
+ Intents.init()
+ }
+
+ @After
+ fun tearDown() {
+ Intents.release()
+ activityScenario.close()
+ }
+
+ @Test
+ fun onCreate() {
+ coEvery { bankUseCase.getStatements(any()) } returns SuccessResponse(mockStatementList())
+ startActivity()
+
+ statementsActivityRobot {
+ elementsMustBeConfiguredCorrectly()
+ }
+ }
+
+ @Test
+ fun logoutClick() {
+ every { bankUseCase.getUser() } returns "user@test.com"
+ coEvery { bankUseCase.getStatements(any()) } returns SuccessResponse(mockStatementList())
+ startActivity()
+
+ statementsActivityRobot {
+ logout()
+ verifyLoginScreen()
+ }
+ }
+
+ private fun mockUserAccount() = UserAccount(
+ userId = 1L,
+ name = "Jose da Silva Teste",
+ bankAccount = "2050",
+ agency = "011112224",
+ balance = BigDecimal.TEN
+ )
+
+ private fun mockStatementList() = listOf(
+ mockStatement(),
+ mockStatement(),
+ mockStatement(),
+ mockStatement(),
+ mockStatement()
+ )
+
+ private fun mockStatement() = Statement(
+ title = "Pagamento",
+ desc = "Conta de luz",
+ date = Date(1610852400000),
+ value = BigDecimal.TEN
+ )
+
+ private fun startActivity() {
+ activityScenario = ActivityScenario.launch(
+ Intent(getApplicationContext(),StatementsActivity::class.java).apply {
+ putExtra(USER_ACCOUNT_EXTRA, mockUserAccount())
+ }
+ )
+
+ activityScenario.onActivity {
+ activity = it
+ }
+ }
+
+ companion object {
+ private const val USER_ACCOUNT_EXTRA = "userAccount"
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/jeanjnap/bankapp/util/CoroutinesExecutor.kt b/app/src/androidTest/java/com/jeanjnap/bankapp/util/CoroutinesExecutor.kt
new file mode 100644
index 000000000..dcfd90968
--- /dev/null
+++ b/app/src/androidTest/java/com/jeanjnap/bankapp/util/CoroutinesExecutor.kt
@@ -0,0 +1,36 @@
+package com.jeanjnap.bankapp.util
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+
+typealias CoroutinesLaunchExecutor = (CoroutineDispatcher, suspend CoroutineScope.() -> Unit) -> Unit
+
+typealias CoroutinesAsyncExecutor = (CoroutineDispatcher, suspend CoroutineScope.() -> T) -> Deferred
+
+class CoroutinesExecutor private constructor() {
+
+ var launchExecutor: CoroutinesLaunchExecutor? = null
+
+ var asyncExecutor: CoroutinesAsyncExecutor<*>? = null
+
+ private val defaultLaunchExecutor: CoroutinesLaunchExecutor
+
+ private val defaultAsyncExecutor: CoroutinesAsyncExecutor<*>
+
+ init {
+ defaultLaunchExecutor = { asyncContext, block ->
+ CoroutineScope(asyncContext).launch(block = block)
+ }
+
+ defaultAsyncExecutor = { asyncContext, block ->
+ CoroutineScope(asyncContext).async(block = block)
+ }
+ }
+
+ companion object {
+ var instance: CoroutinesExecutor = CoroutinesExecutor()
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/jeanjnap/bankapp/util/EspressoCoroutinesRule.kt b/app/src/androidTest/java/com/jeanjnap/bankapp/util/EspressoCoroutinesRule.kt
new file mode 100644
index 000000000..466ab8db7
--- /dev/null
+++ b/app/src/androidTest/java/com/jeanjnap/bankapp/util/EspressoCoroutinesRule.kt
@@ -0,0 +1,44 @@
+package com.jeanjnap.bankapp.util
+
+import androidx.test.espresso.IdlingRegistry
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+
+class EspressoCoroutinesRule : TestWatcher() {
+
+ override fun starting(description: Description?) {
+ super.starting(description)
+
+ IdlingRegistry.getInstance().register(EspressoCountingIdlingResource)
+
+ CoroutinesExecutor.instance.launchExecutor = { asyncContext, block ->
+ EspressoCountingIdlingResource.increment()
+
+ CoroutineScope(asyncContext).launch(block = block)
+ .invokeOnCompletion {
+ EspressoCountingIdlingResource.decrement()
+ }
+ }
+
+ CoroutinesExecutor.instance.asyncExecutor = { asyncContext, block ->
+ EspressoCountingIdlingResource.increment()
+
+ CoroutineScope(asyncContext).async(block = block).apply {
+ invokeOnCompletion {
+ EspressoCountingIdlingResource.decrement()
+ }
+ }
+ }
+ }
+
+ override fun finished(description: Description?) {
+ super.finished(description)
+ CoroutinesExecutor.instance.launchExecutor = null
+ CoroutinesExecutor.instance.asyncExecutor = null
+ IdlingRegistry.getInstance().unregister(EspressoCountingIdlingResource)
+ }
+
+}
diff --git a/app/src/androidTest/java/com/jeanjnap/bankapp/util/EspressoCountingIdlingResource.kt b/app/src/androidTest/java/com/jeanjnap/bankapp/util/EspressoCountingIdlingResource.kt
new file mode 100644
index 000000000..ad476ce0a
--- /dev/null
+++ b/app/src/androidTest/java/com/jeanjnap/bankapp/util/EspressoCountingIdlingResource.kt
@@ -0,0 +1,40 @@
+package com.jeanjnap.bankapp.util
+
+import androidx.test.espresso.IdlingResource
+import java.util.concurrent.atomic.AtomicInteger
+
+object EspressoCountingIdlingResource : IdlingResource {
+
+ private val counter = AtomicInteger(0)
+
+ @Volatile
+ private var resourceCallback: IdlingResource.ResourceCallback? = null
+
+ override fun getName(): String {
+ return EspressoCountingIdlingResource::class.java.simpleName
+ }
+
+ override fun isIdleNow(): Boolean {
+ return counter.get() == 0
+ }
+
+ override fun registerIdleTransitionCallback(resourceCallback: IdlingResource.ResourceCallback) {
+ EspressoCountingIdlingResource.resourceCallback = resourceCallback
+ }
+
+ fun increment() {
+ counter.getAndIncrement()
+ }
+
+ fun decrement() {
+ with(counter.decrementAndGet()) {
+ if (this == 0) {
+ resourceCallback?.onTransitionToIdle()
+ }
+
+ if (this < 0) {
+ throw IllegalArgumentException("O contador foi corrompido")
+ }
+ }
+ }
+}
diff --git a/app/src/androidTest/java/com/jeanjnap/bankapp/util/extension/ContextExtensions.kt b/app/src/androidTest/java/com/jeanjnap/bankapp/util/extension/ContextExtensions.kt
new file mode 100644
index 000000000..ea39f5f5a
--- /dev/null
+++ b/app/src/androidTest/java/com/jeanjnap/bankapp/util/extension/ContextExtensions.kt
@@ -0,0 +1,7 @@
+package com.jeanjnap.bankapp.util.extension
+
+import android.content.Context
+import androidx.annotation.DrawableRes
+import androidx.core.content.ContextCompat
+
+fun Context.getContextCompactDrawable(@DrawableRes drawableId: Int) = ContextCompat.getDrawable(this, drawableId)
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/jeanjnap/bankapp/util/extension/MatcherExtensions.kt b/app/src/androidTest/java/com/jeanjnap/bankapp/util/extension/MatcherExtensions.kt
new file mode 100644
index 000000000..5f4318916
--- /dev/null
+++ b/app/src/androidTest/java/com/jeanjnap/bankapp/util/extension/MatcherExtensions.kt
@@ -0,0 +1,12 @@
+package com.jeanjnap.bankapp.util.extension
+
+import android.view.View
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.ViewInteraction
+import org.hamcrest.Matcher
+
+fun Matcher.onView(func: ViewInteraction.() -> Unit): ViewInteraction {
+ val viewInteraction = Espresso.onView(this)
+ viewInteraction.func()
+ return viewInteraction
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/jeanjnap/bankapp/util/extension/ViewInteractionExtensions.kt b/app/src/androidTest/java/com/jeanjnap/bankapp/util/extension/ViewInteractionExtensions.kt
new file mode 100644
index 000000000..62f32e7de
--- /dev/null
+++ b/app/src/androidTest/java/com/jeanjnap/bankapp/util/extension/ViewInteractionExtensions.kt
@@ -0,0 +1,135 @@
+package com.jeanjnap.bankapp.util.extension
+
+import android.view.View
+import androidx.annotation.ColorRes
+import androidx.annotation.DrawableRes
+import androidx.annotation.FontRes
+import androidx.annotation.StringRes
+import androidx.test.espresso.UiController
+import androidx.test.espresso.ViewAction
+import androidx.test.espresso.ViewAssertion
+import androidx.test.espresso.ViewInteraction
+import androidx.test.espresso.assertion.ViewAssertions
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import com.jeanjnap.bankapp.util.matcher.withBackgroundDrawable
+import com.jeanjnap.bankapp.util.matcher.withFont
+import com.jeanjnap.bankapp.util.matcher.withFontSize
+import com.jeanjnap.bankapp.util.matcher.withTextColor
+import junit.framework.AssertionFailedError
+import org.hamcrest.Matcher
+import org.hamcrest.Matchers.not
+
+private fun ViewInteraction.verify(
+ matcher: Matcher,
+ retryCount: Int
+): ViewInteraction {
+ withFailureHandler { error, _ ->
+ when {
+ error !is AssertionFailedError -> throw error
+ retryCount > 0 -> {
+ perform(waitFor())
+ verify(matcher, retryCount - 1)
+ }
+ else -> throw AssertionFailedError("${error.message} after retries")
+ }
+ }
+ return check(ViewAssertions.matches(matcher))
+}
+
+fun ViewInteraction.verify(matcher: Matcher) = verify(matcher, 5)
+
+fun ViewInteraction.verifyText(
+ @ColorRes colorId: Int,
+ text: String?,
+ fontSize: Int,
+ @FontRes fontId: Int = 0,
+ isDisplayed: Boolean = true
+) {
+ verify(if (isDisplayed) ViewMatchers.isDisplayed() else not(ViewMatchers.isDisplayed()))
+ verify(withTextColor(colorId))
+ verify(withText(text))
+ verify(withFontSize(fontSize))
+ if (fontId != 0) verify(withFont(fontId))
+}
+
+fun ViewInteraction.verifyIconText(
+ @ColorRes colorId: Int,
+ @StringRes textId: Int,
+ fontSize: Int,
+ isDisplayed: Boolean = true
+) {
+ verify(if (isDisplayed) ViewMatchers.isDisplayed() else not(ViewMatchers.isDisplayed()))
+ verify(withTextColor(colorId))
+ verify(withText(textId))
+ verify(withFontSize(fontSize))
+}
+
+fun ViewInteraction.verifyButton(
+ @ColorRes textColorId: Int,
+ text: String,
+ fontSize: Int,
+ @FontRes fontId: Int,
+ @DrawableRes drawableId: Int,
+ isDisplayed: Boolean = true,
+ isEnabled: Boolean = true
+) {
+ verify(if (isDisplayed) ViewMatchers.isDisplayed() else not(ViewMatchers.isDisplayed()))
+ verify(if (isEnabled) ViewMatchers.isEnabled() else not(ViewMatchers.isEnabled()))
+ verify(withTextColor(textColorId))
+ verify(withText(text))
+ verify(withFontSize(fontSize))
+ verify(withFont(fontId))
+ if (isDisplayed) verify(withBackgroundDrawable(drawableId))
+}
+
+private fun waitFor(millis: Long = 200): ViewAction {
+ return object : ViewAction {
+ override fun getConstraints() = ViewMatchers.isAssignableFrom(View::class.java)
+
+ override fun getDescription() = "Wait for $millis milliseconds."
+
+ override fun perform(uiController: UiController, view: View) {
+ uiController.loopMainThreadUntilIdle()
+ uiController.loopMainThreadForAtLeast(millis)
+ }
+ }
+}
+
+private fun ViewInteraction.verify(
+ assertion: ViewAssertion,
+ retryCount: Int
+): ViewInteraction {
+ withFailureHandler { error, _ ->
+ when {
+ error !is AssertionFailedError -> throw error
+ retryCount > 0 -> {
+ perform(waitFor())
+ verify(assertion, retryCount - 1)
+ }
+ else -> throw AssertionFailedError("${error.message} after retries")
+ }
+ }
+ return check(assertion)
+}
+
+fun ViewInteraction.verify(assertion: ViewAssertion) = verify(assertion, 5)
+
+private fun ViewInteraction.act(
+ action: ViewAction,
+ retryCount: Int
+): ViewInteraction {
+ withFailureHandler { error, _ ->
+ when {
+ error !is AssertionFailedError -> throw error
+ retryCount > 0 -> {
+ perform(waitFor(200))
+ act(action, retryCount - 1)
+ }
+ else -> throw AssertionFailedError("${error.message} after retries")
+ }
+ }
+ return perform(action)
+}
+
+fun ViewInteraction.act(action: ViewAction) = act(action, 5)
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/jeanjnap/bankapp/util/matcher/ImageViewMatchers.kt b/app/src/androidTest/java/com/jeanjnap/bankapp/util/matcher/ImageViewMatchers.kt
new file mode 100644
index 000000000..dd03f9ee1
--- /dev/null
+++ b/app/src/androidTest/java/com/jeanjnap/bankapp/util/matcher/ImageViewMatchers.kt
@@ -0,0 +1,22 @@
+package com.jeanjnap.bankapp.util.matcher
+
+import android.view.View
+import android.widget.ImageView
+import androidx.annotation.DrawableRes
+import androidx.core.content.res.ResourcesCompat
+import androidx.core.graphics.drawable.toBitmap
+import androidx.test.espresso.matcher.BoundedMatcher
+import org.hamcrest.Description
+
+fun withDrawable(@DrawableRes drawableId: Int) =
+ object : BoundedMatcher(ImageView::class.java) {
+
+ override fun describeTo(description: Description) {
+ description.appendText("checking Drawable is $drawableId")
+ }
+
+ override fun matchesSafely(item: ImageView): Boolean {
+ val desired = ResourcesCompat.getDrawable(item.resources, drawableId, null)
+ return item.drawable.toBitmap().sameAs(desired?.toBitmap())
+ }
+ }
diff --git a/app/src/androidTest/java/com/jeanjnap/bankapp/util/matcher/RecyclerViewItemCountAssertion.kt b/app/src/androidTest/java/com/jeanjnap/bankapp/util/matcher/RecyclerViewItemCountAssertion.kt
new file mode 100644
index 000000000..88998ec77
--- /dev/null
+++ b/app/src/androidTest/java/com/jeanjnap/bankapp/util/matcher/RecyclerViewItemCountAssertion.kt
@@ -0,0 +1,21 @@
+package com.jeanjnap.bankapp.util.matcher
+
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.espresso.NoMatchingViewException
+import androidx.test.espresso.ViewAssertion
+import androidx.test.espresso.matcher.ViewMatchers
+import org.hamcrest.Matcher
+
+class RecyclerViewItemCountAssertion(private val matcher: Matcher) : ViewAssertion {
+
+ override fun check(view: View, noViewFoundException: NoMatchingViewException?) {
+ if (noViewFoundException != null) {
+ throw noViewFoundException
+ }
+
+ val recyclerView = view as RecyclerView
+ val actualItemCount = recyclerView.adapter?.itemCount ?: 0
+ ViewMatchers.assertThat(actualItemCount, matcher)
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/jeanjnap/bankapp/util/matcher/RecyclerViewMatcher.kt b/app/src/androidTest/java/com/jeanjnap/bankapp/util/matcher/RecyclerViewMatcher.kt
new file mode 100644
index 000000000..abecc3d56
--- /dev/null
+++ b/app/src/androidTest/java/com/jeanjnap/bankapp/util/matcher/RecyclerViewMatcher.kt
@@ -0,0 +1,93 @@
+package com.jeanjnap.bankapp.util.matcher
+
+import android.content.res.Resources
+import android.view.View
+import androidx.annotation.IdRes
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.espresso.matcher.BoundedMatcher
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.Description
+import org.hamcrest.Matcher
+import org.hamcrest.TypeSafeMatcher
+
+class RecyclerViewMatcher constructor(private val recyclerViewId: Int) {
+
+ companion object {
+ private fun withRecyclerView(recyclerViewId: Int): RecyclerViewMatcher {
+ return RecyclerViewMatcher(
+ recyclerViewId
+ )
+ }
+
+ fun withItemCount(matcher: Matcher): RecyclerViewItemCountAssertion {
+ return RecyclerViewItemCountAssertion(
+ matcher
+ )
+ }
+
+ fun atPosition(position: Int, itemMatcher: Matcher): Matcher? {
+ return object : BoundedMatcher(RecyclerView::class.java) {
+ override fun describeTo(description: Description) {
+ description.appendText("has item at position $position: ")
+ itemMatcher.describeTo(description)
+ }
+
+ override fun matchesSafely(view: RecyclerView): Boolean {
+ val viewHolder =
+ view.findViewHolderForAdapterPosition(position) ?: return false
+ return itemMatcher.matches(viewHolder.itemView)
+ }
+ }
+ }
+
+ fun withRecyclerViewItemIdAtPosition(@IdRes recyclerView: Int, position: Int, @IdRes childViewId: Int) =
+ withRecyclerView(
+ recyclerView
+ ).atPositionOnView(position, childViewId)
+
+ fun withItemCount(expectedCount: Int): RecyclerViewItemCountAssertion {
+ return withItemCount(
+ `is`(expectedCount)
+ )
+ }
+ }
+
+ fun atPositionOnView(position: Int, targetViewId: Int): Matcher {
+ return object : TypeSafeMatcher() {
+ var resources: Resources? = null
+ var childView: View? = null
+
+ override fun describeTo(description: Description) {
+ var idDescription = "$recyclerViewId (resource name not found)"
+ this.resources?.let {
+ idDescription = it.getResourceName(recyclerViewId)
+ }
+
+ description.appendText("with id: $idDescription")
+ }
+
+ public override fun matchesSafely(view: View): Boolean {
+ this.resources = view.resources
+
+ val recyclerView =
+ view.rootView.findViewById(recyclerViewId) as RecyclerView
+ if (recyclerView.id == recyclerViewId) {
+ val viewHolder =
+ recyclerView.findViewHolderForAdapterPosition(position)
+ if (viewHolder != null) {
+ childView = viewHolder.itemView
+ }
+ } else {
+ return false
+ }
+
+ return if (targetViewId == -1) {
+ view === childView
+ } else {
+ val targetView = childView?.findViewById(targetViewId)
+ view === targetView
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/jeanjnap/bankapp/util/matcher/TextInputMatcher.kt b/app/src/androidTest/java/com/jeanjnap/bankapp/util/matcher/TextInputMatcher.kt
new file mode 100644
index 000000000..e33c998f8
--- /dev/null
+++ b/app/src/androidTest/java/com/jeanjnap/bankapp/util/matcher/TextInputMatcher.kt
@@ -0,0 +1,20 @@
+package com.jeanjnap.bankapp.util.matcher
+
+import android.view.View
+import com.google.android.material.textfield.TextInputLayout
+import org.hamcrest.Description
+import org.hamcrest.Matcher
+import org.hamcrest.TypeSafeMatcher
+
+fun hasTextInputLayoutError(errorText: String?): Matcher = object : TypeSafeMatcher() {
+
+ override fun describeTo(description: Description?) {
+ description?.appendText("checking if error is $errorText")
+ }
+
+ override fun matchesSafely(item: View?): Boolean {
+ if (item !is TextInputLayout) return false
+
+ return errorText == item.error
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/jeanjnap/bankapp/util/matcher/TextViewMatchers.kt b/app/src/androidTest/java/com/jeanjnap/bankapp/util/matcher/TextViewMatchers.kt
new file mode 100644
index 000000000..e68db9f8a
--- /dev/null
+++ b/app/src/androidTest/java/com/jeanjnap/bankapp/util/matcher/TextViewMatchers.kt
@@ -0,0 +1,49 @@
+package com.jeanjnap.bankapp.util.matcher
+
+import android.view.View
+import android.widget.TextView
+import androidx.annotation.ColorRes
+import androidx.annotation.FontRes
+import androidx.core.content.ContextCompat
+import androidx.core.content.res.ResourcesCompat
+import androidx.test.espresso.matcher.BoundedMatcher
+import org.hamcrest.Description
+import kotlin.math.roundToInt
+
+fun withTextColor(@ColorRes colorId: Int) =
+ object : BoundedMatcher(TextView::class.java) {
+
+ override fun describeTo(description: Description) {
+ description.appendText("checking textColor is $colorId")
+ }
+
+ override fun matchesSafely(item: TextView) =
+ item.currentTextColor == ContextCompat.getColor(item.context, colorId)
+
+ }
+
+fun withFontSize(expectedSize: Int) =
+ object : BoundedMatcher(TextView::class.java) {
+
+ override fun describeTo(description: Description) {
+ description.appendText("checking textSize is $expectedSize")
+ }
+
+ override fun matchesSafely(item: TextView): Boolean {
+ val pixels = item.textSize
+ val actualSize = (pixels / item.resources.displayMetrics.density).roundToInt()
+ return actualSize == expectedSize
+ }
+
+ }
+
+fun withFont(@FontRes fontId: Int) = object : BoundedMatcher(TextView::class.java) {
+
+ override fun describeTo(description: Description) {
+ description.appendText("checking font: $fontId")
+ }
+
+ override fun matchesSafely(item: TextView): Boolean {
+ return item.typeface == ResourcesCompat.getFont(item.context, fontId)
+ }
+}
diff --git a/app/src/androidTest/java/com/jeanjnap/bankapp/util/matcher/ViewMatchers.kt b/app/src/androidTest/java/com/jeanjnap/bankapp/util/matcher/ViewMatchers.kt
new file mode 100644
index 000000000..f56c5a936
--- /dev/null
+++ b/app/src/androidTest/java/com/jeanjnap/bankapp/util/matcher/ViewMatchers.kt
@@ -0,0 +1,124 @@
+package com.jeanjnap.bankapp.util.matcher
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Rect
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.ColorDrawable
+import android.graphics.drawable.GradientDrawable
+import android.graphics.drawable.StateListDrawable
+import android.graphics.drawable.VectorDrawable
+import android.view.View
+import androidx.test.espresso.matcher.BoundedMatcher
+import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
+import com.jeanjnap.bankapp.util.extension.getContextCompactDrawable
+import org.hamcrest.Description
+import kotlin.math.roundToInt
+
+fun withBackgroundDrawable(drawableId: Int) =
+ object : BoundedMatcher(View::class.java) {
+
+ override fun describeTo(description: Description) {
+ description.appendText("checking background is $drawableId")
+ }
+
+ override fun matchesSafely(item: View): Boolean {
+ var expectedDrawable = item.context.getContextCompactDrawable(drawableId)
+ var drawable = item.background
+
+ if (drawable == null || expectedDrawable == null) return false
+
+ if (drawable is StateListDrawable && expectedDrawable is StateListDrawable) {
+ drawable = drawable.getCurrent()
+ expectedDrawable = expectedDrawable.getCurrent()
+ }
+
+ if (drawable is ColorDrawable && expectedDrawable is ColorDrawable) {
+ return drawable.color == expectedDrawable.color
+ }
+
+ if (drawable is BitmapDrawable && expectedDrawable is BitmapDrawable) {
+ return drawable.bitmap.sameAs(expectedDrawable.bitmap)
+ }
+
+ if (drawable is GradientDrawable && expectedDrawable is GradientDrawable) {
+ return drawable.colors?.first() == expectedDrawable.colors?.first() &&
+ drawable.colors?.last() == expectedDrawable.colors?.last()
+ }
+
+ if (drawable is VectorDrawable || drawable is VectorDrawableCompat) {
+ val drawableRect: Rect = drawable.bounds
+ val bitmap = Bitmap.createBitmap(
+ drawableRect.width(),
+ drawableRect.height(),
+ Bitmap.Config.ARGB_8888
+ )
+
+ val canvas = Canvas(bitmap)
+ drawable.setBounds(0, 0, canvas.width, canvas.height)
+ drawable.draw(canvas)
+
+ val otherBitmap = Bitmap.createBitmap(
+ drawableRect.width(),
+ drawableRect.height(),
+ Bitmap.Config.ARGB_8888
+ )
+ val otherCanvas = Canvas(otherBitmap)
+ expectedDrawable.setBounds(0, 0, otherCanvas.width, otherCanvas.height)
+ expectedDrawable.draw(otherCanvas)
+
+ return bitmap.sameAs(otherBitmap)
+ }
+
+ return false
+ }
+ }
+
+fun withBackgroundColorDrawable(hexString: String) =
+ object : BoundedMatcher(View::class.java) {
+
+ override fun describeTo(description: Description?) {
+ description?.appendText("checking background is $hexString")
+ }
+
+ override fun matchesSafely(item: View?) = if (item != null) {
+ val desired = Color.parseColor(hexString)
+ val background = item.background as? ColorDrawable
+ if (background != null) {
+ background.color == desired
+ } else {
+ false
+ }
+ } else {
+ false
+ }
+ }
+
+fun withWidthSize(width: Int) = object : BoundedMatcher(View::class.java) {
+ override fun describeTo(description: Description?) {
+ description?.appendText("checking width is $width")
+ }
+
+ override fun matchesSafely(item: View?) = if (item != null) {
+ val factor = item.context.resources.displayMetrics.density
+ val pixels = (width * factor)
+ item.width in arrayOf(pixels.toInt(), pixels.roundToInt())
+ } else {
+ false
+ }
+}
+
+fun withHeightSize(height: Int) = object : BoundedMatcher(View::class.java) {
+ override fun describeTo(description: Description?) {
+ description?.appendText("checking height is $height")
+ }
+
+ override fun matchesSafely(item: View?) = if (item != null) {
+ val factor = item.context.resources.displayMetrics.density
+ val pixels = (height * factor)
+ item.height in arrayOf(pixels.toInt(), pixels.roundToInt())
+ } else {
+ false
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..a4c24b0c4
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/jeanjnap/bankapp/BankApplication.kt b/app/src/main/java/com/jeanjnap/bankapp/BankApplication.kt
new file mode 100644
index 000000000..02ee19c15
--- /dev/null
+++ b/app/src/main/java/com/jeanjnap/bankapp/BankApplication.kt
@@ -0,0 +1,22 @@
+package com.jeanjnap.bankapp
+
+import androidx.multidex.MultiDexApplication
+import com.jeanjnap.bankapp.di.AppComponent.getAllModules
+import org.koin.android.ext.koin.androidContext
+import org.koin.android.ext.koin.androidLogger
+import org.koin.core.KoinComponent
+import org.koin.core.context.startKoin
+
+@Suppress("unused")
+open class BankApplication : MultiDexApplication(), KoinComponent {
+
+ override fun onCreate() {
+ super.onCreate()
+
+ startKoin {
+ androidLogger()
+ androidContext(this@BankApplication)
+ modules(getAllModules())
+ }
+ }
+}
diff --git a/app/src/main/java/com/jeanjnap/bankapp/di/AppComponent.kt b/app/src/main/java/com/jeanjnap/bankapp/di/AppComponent.kt
new file mode 100644
index 000000000..14f06a387
--- /dev/null
+++ b/app/src/main/java/com/jeanjnap/bankapp/di/AppComponent.kt
@@ -0,0 +1,17 @@
+package com.jeanjnap.bankapp.di
+
+import com.jeanjnap.bankapp.di.AppModules.utilsModules
+import com.jeanjnap.bankapp.di.AppModules.viewModelModules
+import com.jeanjnap.bankapp.di.DataModules.dataModulesItems
+import com.jeanjnap.bankapp.di.DataModules.serviceModulesItems
+import com.jeanjnap.bankapp.di.DomainModules.domainModulesItems
+import com.jeanjnap.bankapp.di.InfrastructureModules.infrastructureModulesItems
+import com.jeanjnap.data.di.MapperModules.mapperModulesItems
+
+object AppComponent {
+
+ fun getAllModules() = listOf(
+ utilsModules, viewModelModules, domainModulesItems, serviceModulesItems, dataModulesItems,
+ infrastructureModulesItems, mapperModulesItems
+ )
+}
diff --git a/app/src/main/java/com/jeanjnap/bankapp/di/AppModules.kt b/app/src/main/java/com/jeanjnap/bankapp/di/AppModules.kt
new file mode 100644
index 000000000..9ab64f049
--- /dev/null
+++ b/app/src/main/java/com/jeanjnap/bankapp/di/AppModules.kt
@@ -0,0 +1,20 @@
+package com.jeanjnap.bankapp.di
+
+import com.jeanjnap.bankapp.ui.login.LoginViewModel
+import com.jeanjnap.bankapp.ui.statements.StatementsViewModel
+import com.jeanjnap.bankapp.util.ResourcesStringImpl
+import com.jeanjnap.domain.boundary.ResourcesString
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.dsl.module
+
+object AppModules {
+
+ val utilsModules = module {
+ single { ResourcesStringImpl(get()) }
+ }
+
+ val viewModelModules = module {
+ viewModel { LoginViewModel(get(), get()) }
+ viewModel { StatementsViewModel(get(), get()) }
+ }
+}
diff --git a/app/src/main/java/com/jeanjnap/bankapp/di/DataModules.kt b/app/src/main/java/com/jeanjnap/bankapp/di/DataModules.kt
new file mode 100644
index 000000000..9673c25b3
--- /dev/null
+++ b/app/src/main/java/com/jeanjnap/bankapp/di/DataModules.kt
@@ -0,0 +1,43 @@
+package com.jeanjnap.bankapp.di
+
+import com.jeanjnap.data.client.ApiClient.makeService
+import com.jeanjnap.data.mapper.StatementSummaryResponseToStatementListMapper
+import com.jeanjnap.data.mapper.UserDataResponseToUserAccountMapper
+import com.jeanjnap.data.repository.BankRepositoryImpl
+import com.jeanjnap.data.source.local.BankLocalDataSource
+import com.jeanjnap.data.source.local.BankLocalDataSourceImpl
+import com.jeanjnap.data.source.local.Cache
+import com.jeanjnap.data.source.local.CacheImpl
+import com.jeanjnap.data.source.remote.BankRemoteDataSource
+import com.jeanjnap.data.source.remote.BankRemoteDataSourceImpl
+import com.jeanjnap.data.source.remote.service.BankService
+import com.jeanjnap.data.util.moshi.InternalMoshi
+import com.jeanjnap.data.util.moshi.InternalMoshiImpl
+import com.jeanjnap.domain.repository.BankRepository
+import org.koin.core.qualifier.named
+import org.koin.dsl.module
+
+object DataModules {
+
+ val serviceModulesItems = module {
+ single { makeService(get()) }
+ }
+
+ val dataModulesItems = module {
+ single { InternalMoshiImpl() }
+ single { CacheImpl(get(), get()) }
+
+ // DATA SOURCES
+ single {
+ BankRemoteDataSourceImpl(
+ get(),
+ get(named(UserDataResponseToUserAccountMapper::class.java.name)),
+ get(named(StatementSummaryResponseToStatementListMapper::class.java.name))
+ )
+ }
+ single { BankLocalDataSourceImpl(get()) }
+
+ // REPOSITORIES
+ single { BankRepositoryImpl(get(), get()) }
+ }
+}
diff --git a/app/src/main/java/com/jeanjnap/bankapp/di/DomainModules.kt b/app/src/main/java/com/jeanjnap/bankapp/di/DomainModules.kt
new file mode 100644
index 000000000..a6fbdd347
--- /dev/null
+++ b/app/src/main/java/com/jeanjnap/bankapp/di/DomainModules.kt
@@ -0,0 +1,13 @@
+package com.jeanjnap.bankapp.di
+
+import com.jeanjnap.domain.usecase.BankUseCase
+import com.jeanjnap.domain.usecase.BankUseCaseImpl
+import org.koin.dsl.module
+
+object DomainModules {
+
+ val domainModulesItems = module {
+ //USECASES
+ single { BankUseCaseImpl(get(), get()) }
+ }
+}
diff --git a/app/src/main/java/com/jeanjnap/bankapp/di/InfrastructureModules.kt b/app/src/main/java/com/jeanjnap/bankapp/di/InfrastructureModules.kt
new file mode 100644
index 000000000..51db9aaa0
--- /dev/null
+++ b/app/src/main/java/com/jeanjnap/bankapp/di/InfrastructureModules.kt
@@ -0,0 +1,14 @@
+package com.jeanjnap.bankapp.di
+
+import com.jeanjnap.infrastructure.crypto.RSACrypto
+import com.jeanjnap.infrastructure.crypto.RSACryptoImpl
+import com.jeanjnap.infrastructure.network.Network
+import com.jeanjnap.infrastructure.network.NetworkImpl
+import org.koin.dsl.module
+
+object InfrastructureModules {
+ val infrastructureModulesItems = module {
+ single { NetworkImpl(get()) }
+ single { RSACryptoImpl(get()) }
+ }
+}
diff --git a/app/src/main/java/com/jeanjnap/bankapp/ui/base/BaseActivity.kt b/app/src/main/java/com/jeanjnap/bankapp/ui/base/BaseActivity.kt
new file mode 100644
index 000000000..0f9d782b9
--- /dev/null
+++ b/app/src/main/java/com/jeanjnap/bankapp/ui/base/BaseActivity.kt
@@ -0,0 +1,25 @@
+package com.jeanjnap.bankapp.ui.base
+
+import android.os.Bundle
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import com.jeanjnap.bankapp.util.extension.observe
+
+abstract class BaseActivity : AppCompatActivity() {
+
+ abstract val viewModel: BaseViewModel
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ lifecycle.addObserver(viewModel)
+ listenUi()
+ }
+
+ private fun listenUi() {
+ observe(viewModel.error, ::showToast)
+ }
+
+ private fun showToast(msg: String, duration: Int = Toast.LENGTH_SHORT) {
+ Toast.makeText(this, msg, duration).show()
+ }
+}
diff --git a/app/src/main/java/com/jeanjnap/bankapp/ui/base/BaseAdapter.kt b/app/src/main/java/com/jeanjnap/bankapp/ui/base/BaseAdapter.kt
new file mode 100644
index 000000000..8ee7a3b9a
--- /dev/null
+++ b/app/src/main/java/com/jeanjnap/bankapp/ui/base/BaseAdapter.kt
@@ -0,0 +1,43 @@
+package com.jeanjnap.bankapp.ui.base
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.annotation.LayoutRes
+import androidx.databinding.DataBindingUtil
+import androidx.databinding.ViewDataBinding
+import androidx.recyclerview.widget.ListAdapter
+
+open class BaseAdapter(
+ @LayoutRes val view: Int,
+ val onBind: (T, ViewDataBinding) -> Unit
+) : ListAdapter(BaseDiffUtil()) {
+
+ private var items: MutableList = mutableListOf()
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
+ return BaseViewHolder(
+ DataBindingUtil.inflate(
+ LayoutInflater.from(parent.context),
+ view,
+ parent,
+ false
+ )
+ )
+ }
+
+ override fun getItemCount() = items.size
+
+ override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
+ items.getOrNull(position)?.let {
+ onBind(it, holder.viewDataBinding)
+ return
+ }
+ }
+
+ override fun submitList(list: List?) {
+ if (items.isNotEmpty()) items.clear()
+ list?.let { items.addAll(it) }
+ super.submitList(items)
+ notifyDataSetChanged()
+ }
+}
diff --git a/app/src/main/java/com/jeanjnap/bankapp/ui/base/BaseDiffUtil.kt b/app/src/main/java/com/jeanjnap/bankapp/ui/base/BaseDiffUtil.kt
new file mode 100644
index 000000000..89036362b
--- /dev/null
+++ b/app/src/main/java/com/jeanjnap/bankapp/ui/base/BaseDiffUtil.kt
@@ -0,0 +1,13 @@
+
+package com.jeanjnap.bankapp.ui.base
+
+import androidx.recyclerview.widget.DiffUtil
+
+class BaseDiffUtil : DiffUtil.ItemCallback() {
+
+ override fun areItemsTheSame(oldItem: T, newItem: T) = compare(oldItem, newItem)
+
+ override fun areContentsTheSame(oldItem: T, newItem: T) = compare(oldItem, newItem)
+
+ private fun compare(oldItem: T, newItem: T) = oldItem == newItem
+}
diff --git a/app/src/main/java/com/jeanjnap/bankapp/ui/base/BaseViewHolder.kt b/app/src/main/java/com/jeanjnap/bankapp/ui/base/BaseViewHolder.kt
new file mode 100644
index 000000000..9fff3205c
--- /dev/null
+++ b/app/src/main/java/com/jeanjnap/bankapp/ui/base/BaseViewHolder.kt
@@ -0,0 +1,7 @@
+package com.jeanjnap.bankapp.ui.base
+
+import androidx.databinding.ViewDataBinding
+import androidx.recyclerview.widget.RecyclerView
+
+class BaseViewHolder(val viewDataBinding: ViewDataBinding) :
+ RecyclerView.ViewHolder(viewDataBinding.root)
diff --git a/app/src/main/java/com/jeanjnap/bankapp/ui/base/BaseViewModel.kt b/app/src/main/java/com/jeanjnap/bankapp/ui/base/BaseViewModel.kt
new file mode 100644
index 000000000..fabb17435
--- /dev/null
+++ b/app/src/main/java/com/jeanjnap/bankapp/ui/base/BaseViewModel.kt
@@ -0,0 +1,58 @@
+package com.jeanjnap.bankapp.ui.base
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.jeanjnap.domain.boundary.ResourcesString
+import com.jeanjnap.infrastructure.network.Network
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import org.koin.core.inject
+import java.util.concurrent.CancellationException
+
+abstract class BaseViewModel(
+ private val network: Network
+) : DefaultViewModel() {
+
+ protected val resourcesString: ResourcesString by inject()
+
+ val loading: LiveData get() = _loading
+ val error: LiveData get() = _error
+
+ private val _loading = MutableLiveData()
+ private val _error = MutableLiveData()
+
+ private val viewModelJob = Job()
+ private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
+
+ fun launchDataLoad(
+ checkConnection: Boolean = true,
+ block: suspend CoroutineScope.() -> Unit
+ ): Job {
+ return uiScope.launch {
+ if (checkConnection && network.hasActiveInternetConnection()) {
+ try {
+ _loading.value = true
+ block()
+ } catch (exception: Exception) {
+ doOnError(exception)
+ } finally {
+ _loading.value = false
+ }
+ } else {
+ _error.value = resourcesString.noConnectionError
+ }
+ }
+ }
+
+ fun displayError(message: String? = null) {
+ _error.value = message ?: resourcesString.genericError
+ }
+
+ private fun doOnError(exception: Exception) {
+ if (exception !is CancellationException) {
+ displayError()
+ }
+ }
+}
diff --git a/app/src/main/java/com/jeanjnap/bankapp/ui/base/DefaultViewModel.kt b/app/src/main/java/com/jeanjnap/bankapp/ui/base/DefaultViewModel.kt
new file mode 100644
index 000000000..30f01d84c
--- /dev/null
+++ b/app/src/main/java/com/jeanjnap/bankapp/ui/base/DefaultViewModel.kt
@@ -0,0 +1,7 @@
+package com.jeanjnap.bankapp.ui.base
+
+import androidx.lifecycle.LifecycleObserver
+import androidx.lifecycle.ViewModel
+import org.koin.core.KoinComponent
+
+abstract class DefaultViewModel : ViewModel(), LifecycleObserver, KoinComponent
diff --git a/app/src/main/java/com/jeanjnap/bankapp/ui/login/LoginActivity.kt b/app/src/main/java/com/jeanjnap/bankapp/ui/login/LoginActivity.kt
new file mode 100644
index 000000000..4c0176d05
--- /dev/null
+++ b/app/src/main/java/com/jeanjnap/bankapp/ui/login/LoginActivity.kt
@@ -0,0 +1,78 @@
+package com.jeanjnap.bankapp.ui.login
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.inputmethod.EditorInfo
+import androidx.databinding.DataBindingUtil
+import com.jeanjnap.bankapp.R
+import com.jeanjnap.bankapp.databinding.ActivityLoginBinding
+import com.jeanjnap.bankapp.ui.statements.StatementsActivity
+import com.jeanjnap.bankapp.ui.base.BaseActivity
+import com.jeanjnap.bankapp.util.extension.changeStatusBarColorToWhite
+import com.jeanjnap.bankapp.util.extension.observe
+
+import org.koin.androidx.viewmodel.ext.android.viewModel
+
+class LoginActivity : BaseActivity() {
+
+ private lateinit var binding: ActivityLoginBinding
+
+ override val viewModel: LoginViewModel by viewModel()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = DataBindingUtil.setContentView(this, R.layout.activity_login)
+ binding.viewModel = viewModel
+ configureElements()
+ listenUi()
+ }
+
+ private fun configureElements() {
+ changeStatusBarColorToWhite()
+ binding.tietPass.setOnEditorActionListener { _, actionId, _ ->
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ viewModel.onLoginClick()
+ }
+ false
+ }
+ }
+
+ private fun listenUi() {
+ observe(viewModel.username, binding.tietUser::setText)
+ observe(viewModel.loading, ::onLoading)
+ observe(viewModel.usernameError, ::onUsernameError)
+ observe(viewModel.passwordError, ::onPasswordError)
+ observe(viewModel.loginSuccess) { StatementsActivity.clearTopStart(this, it) }
+ }
+
+ private fun onLoading(isLoading: Boolean) {
+ with(binding.btLogin) {
+ text = getString(
+ if (isLoading) R.string.loading
+ else R.string.login
+ )
+ isEnabled = !isLoading
+ }
+ }
+
+ private fun onUsernameError(isWrong: Boolean) {
+ binding.tilUser.error = if (isWrong) getString(R.string.invalid_username) else null
+ }
+
+ private fun onPasswordError(isWrong: Boolean) {
+ binding.tilPass.error = if (isWrong) getString(R.string.invalid_password) else null
+ }
+
+ companion object {
+ fun clearTopStart(context: Context) {
+ context.startActivity(
+ Intent(context, LoginActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/jeanjnap/bankapp/ui/login/LoginForm.kt b/app/src/main/java/com/jeanjnap/bankapp/ui/login/LoginForm.kt
new file mode 100644
index 000000000..7e5cffd46
--- /dev/null
+++ b/app/src/main/java/com/jeanjnap/bankapp/ui/login/LoginForm.kt
@@ -0,0 +1,19 @@
+package com.jeanjnap.bankapp.ui.login
+
+import com.jeanjnap.domain.util.extensions.isCPF
+
+class LoginForm {
+
+ var user: String = EMPTY_TEXT
+ var pass: String = EMPTY_TEXT
+
+ fun isValidPassword() = PASSWORD_REGEX.matches(pass)
+ fun isValidUsername() = EMAIL_REGEX.matches(user) || user.isCPF()
+ fun isFormValid() = isValidPassword() && isValidUsername()
+
+ companion object {
+ private const val EMPTY_TEXT = ""
+ private val PASSWORD_REGEX = "^(?=.*[A-Z])(?=.*[!@#\$%&*])(?=.*[a-z0-9]).{3,}\$".toRegex()
+ private val EMAIL_REGEX = "^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}$".toRegex()
+ }
+}
diff --git a/app/src/main/java/com/jeanjnap/bankapp/ui/login/LoginViewModel.kt b/app/src/main/java/com/jeanjnap/bankapp/ui/login/LoginViewModel.kt
new file mode 100644
index 000000000..9cc9666cd
--- /dev/null
+++ b/app/src/main/java/com/jeanjnap/bankapp/ui/login/LoginViewModel.kt
@@ -0,0 +1,66 @@
+package com.jeanjnap.bankapp.ui.login
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.OnLifecycleEvent
+import com.jeanjnap.bankapp.ui.base.BaseViewModel
+import com.jeanjnap.domain.entity.Response
+import com.jeanjnap.domain.entity.SuccessResponse
+import com.jeanjnap.domain.entity.UserAccount
+import com.jeanjnap.domain.usecase.BankUseCase
+import com.jeanjnap.infrastructure.network.Network
+
+class LoginViewModel(
+ network: Network,
+ private val bankUseCase: BankUseCase
+): BaseViewModel(network) {
+
+ val username: LiveData get() = _username
+ val passwordError: LiveData get() = _passwordError
+ val usernameError: LiveData get() = _usernameError
+ val loginSuccess: LiveData get() = _loginSuccess
+
+ private val _username = MutableLiveData()
+ private val _passwordError = MutableLiveData()
+ private val _usernameError = MutableLiveData()
+ private val _loginSuccess = MutableLiveData()
+
+ private val form = LoginForm()
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
+ fun onCreate() {
+ bankUseCase.getUser()?.let {
+ form.user = it
+ _username.value = it
+ }
+ }
+
+ fun onUserNameChanged(text: CharSequence) {
+ form.user = text.toString()
+ _usernameError.value = false
+ }
+
+ fun onPasswordChanged(text: CharSequence) {
+ form.pass = text.toString()
+ _passwordError.value = false
+ }
+
+ fun onLoginClick() {
+ _usernameError.value = !form.isValidUsername()
+ _passwordError.value = !form.isValidPassword()
+ if (form.isFormValid()) {
+ launchDataLoad {
+ bankUseCase.login(form.user, form.pass).handleLoginResult()
+ }
+ }
+ }
+
+ private fun Response.handleLoginResult() {
+ if (this is SuccessResponse) {
+ _loginSuccess.value = body
+ } else {
+ displayError(resourcesString.genericError)
+ }
+ }
+}
diff --git a/app/src/main/java/com/jeanjnap/bankapp/ui/statements/StatementsActivity.kt b/app/src/main/java/com/jeanjnap/bankapp/ui/statements/StatementsActivity.kt
new file mode 100644
index 000000000..9caa11476
--- /dev/null
+++ b/app/src/main/java/com/jeanjnap/bankapp/ui/statements/StatementsActivity.kt
@@ -0,0 +1,66 @@
+package com.jeanjnap.bankapp.ui.statements
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.core.view.isVisible
+import androidx.databinding.DataBindingUtil
+import com.jeanjnap.bankapp.R
+import com.jeanjnap.bankapp.databinding.ActivityStatementsBinding
+import com.jeanjnap.bankapp.ui.base.BaseActivity
+import com.jeanjnap.bankapp.ui.login.LoginActivity
+import com.jeanjnap.bankapp.util.extension.observe
+import com.jeanjnap.domain.entity.UserAccount
+import org.koin.androidx.viewmodel.ext.android.viewModel
+
+class StatementsActivity : BaseActivity() {
+
+ override val viewModel: StatementsViewModel by viewModel()
+
+ private lateinit var binding: ActivityStatementsBinding
+ private lateinit var statementsAdapter: StatementsAdapter
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = DataBindingUtil.setContentView(this, R.layout.activity_statements)
+ binding.viewModel = viewModel
+
+ (intent.extras?.getSerializable(USER_ACCOUNT_EXTRA) as? UserAccount)?.let {
+ binding.userAccount = it
+ viewModel.userId = it.userId
+ }
+ setupElements()
+ listenUi()
+ }
+
+ private fun setupElements() {
+ statementsAdapter = StatementsAdapter()
+ binding.rvStatements.adapter = statementsAdapter
+ }
+
+ private fun listenUi() {
+ observe(viewModel.loading, ::onLoading)
+ observe(viewModel.statements, statementsAdapter::submitList)
+ observe(viewModel.onLogout) { LoginActivity.clearTopStart(this) }
+ }
+
+ private fun onLoading(isLoading: Boolean) {
+ binding.slLoading.isVisible = isLoading
+ binding.rvStatements.isVisible = !isLoading
+ }
+
+ companion object {
+ private const val USER_ACCOUNT_EXTRA = "userAccount"
+
+ fun clearTopStart(context: Context, userAccount: UserAccount) {
+ context.startActivity(
+ Intent(context, StatementsActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ putExtra(USER_ACCOUNT_EXTRA, userAccount)
+ }
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/jeanjnap/bankapp/ui/statements/StatementsAdapter.kt b/app/src/main/java/com/jeanjnap/bankapp/ui/statements/StatementsAdapter.kt
new file mode 100644
index 000000000..480c30d27
--- /dev/null
+++ b/app/src/main/java/com/jeanjnap/bankapp/ui/statements/StatementsAdapter.kt
@@ -0,0 +1,13 @@
+package com.jeanjnap.bankapp.ui.statements
+
+import com.jeanjnap.bankapp.R
+import com.jeanjnap.bankapp.databinding.ItemStatementBinding
+import com.jeanjnap.bankapp.ui.base.BaseAdapter
+import com.jeanjnap.bankapp.util.extension.adapterDataBindingCast
+import com.jeanjnap.domain.entity.Statement
+
+class StatementsAdapter : BaseAdapter(R.layout.item_statement, { statement, view ->
+ with(view.adapterDataBindingCast()) {
+ this.statement = statement
+ }
+})
\ No newline at end of file
diff --git a/app/src/main/java/com/jeanjnap/bankapp/ui/statements/StatementsViewModel.kt b/app/src/main/java/com/jeanjnap/bankapp/ui/statements/StatementsViewModel.kt
new file mode 100644
index 000000000..566bf6a2a
--- /dev/null
+++ b/app/src/main/java/com/jeanjnap/bankapp/ui/statements/StatementsViewModel.kt
@@ -0,0 +1,46 @@
+package com.jeanjnap.bankapp.ui.statements
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.jeanjnap.bankapp.ui.base.BaseViewModel
+import com.jeanjnap.domain.entity.Response
+import com.jeanjnap.domain.entity.Statement
+import com.jeanjnap.domain.entity.SuccessResponse
+import com.jeanjnap.domain.usecase.BankUseCase
+import com.jeanjnap.infrastructure.network.Network
+
+class StatementsViewModel(
+ network: Network,
+ private val bankUseCase: BankUseCase
+) : BaseViewModel(network) {
+
+ val statements: LiveData> get() = _statements
+ val onLogout: LiveData get() = _onLogout
+
+ private val _statements = MutableLiveData>()
+ private val _onLogout = MutableLiveData()
+
+ var userId: Long? = null
+ set(value) {
+ field = value
+ fetchStatements()
+ }
+
+ fun onLogoutClick() {
+ _onLogout.value = true
+ }
+
+ private fun fetchStatements() {
+ launchDataLoad {
+ bankUseCase.getStatements(userId).handleStatementsResponse()
+ }
+ }
+
+ private fun Response>.handleStatementsResponse() {
+ if (this is SuccessResponse) {
+ _statements.value = body
+ } else {
+ displayError(resourcesString.genericError)
+ }
+ }
+}
diff --git a/app/src/main/java/com/jeanjnap/bankapp/util/ResourcesStringImpl.kt b/app/src/main/java/com/jeanjnap/bankapp/util/ResourcesStringImpl.kt
new file mode 100644
index 000000000..fbdcaa94d
--- /dev/null
+++ b/app/src/main/java/com/jeanjnap/bankapp/util/ResourcesStringImpl.kt
@@ -0,0 +1,12 @@
+package com.jeanjnap.bankapp.util
+
+import android.content.Context
+import com.jeanjnap.bankapp.R
+import com.jeanjnap.domain.boundary.ResourcesString
+
+class ResourcesStringImpl(
+ private val context: Context
+) : ResourcesString {
+ override val genericError: String get() = context.getString(R.string.generic_error)
+ override val noConnectionError: String get() = context.getString(R.string.no_connection_error)
+}
diff --git a/app/src/main/java/com/jeanjnap/bankapp/util/bindadapter/TextViewBindAdapters.kt b/app/src/main/java/com/jeanjnap/bankapp/util/bindadapter/TextViewBindAdapters.kt
new file mode 100644
index 000000000..61b07a413
--- /dev/null
+++ b/app/src/main/java/com/jeanjnap/bankapp/util/bindadapter/TextViewBindAdapters.kt
@@ -0,0 +1,37 @@
+package com.jeanjnap.bankapp.util.bindadapter
+
+import android.widget.TextView
+import androidx.databinding.BindingAdapter
+import com.jeanjnap.domain.entity.UserAccount
+import com.jeanjnap.domain.util.extensions.formatAgency
+import com.jeanjnap.domain.util.extensions.formatCurrency
+import com.jeanjnap.domain.util.extensions.formatToString
+import java.math.BigDecimal
+import java.util.Date
+
+@BindingAdapter("nullableText")
+fun nullableText(tv: TextView, value: String?) {
+ value?.let {
+ tv.text = it
+ }
+}
+@BindingAdapter("formatAccount")
+fun formatAccount(tv: TextView, value: UserAccount?) {
+ value?.let {
+ tv.text = "${it.bankAccount} / ${it.agency?.formatAgency()}"
+ }
+}
+
+@BindingAdapter("formatCurrency")
+fun formatCurrency(tv: TextView, value: BigDecimal?) {
+ value?.let {
+ tv.text = it.formatCurrency()
+ }
+}
+
+@BindingAdapter("formatDate")
+fun formatDate(tv: TextView, value: Date?) {
+ value?.let {
+ tv.text = it.formatToString()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/jeanjnap/bankapp/util/extension/ActivityExtensions.kt b/app/src/main/java/com/jeanjnap/bankapp/util/extension/ActivityExtensions.kt
new file mode 100644
index 000000000..a82deac13
--- /dev/null
+++ b/app/src/main/java/com/jeanjnap/bankapp/util/extension/ActivityExtensions.kt
@@ -0,0 +1,14 @@
+package com.jeanjnap.bankapp.util.extension
+
+import android.app.Activity
+import android.os.Build
+import android.view.View
+import com.jeanjnap.bankapp.R
+
+@Suppress("DEPRECATION")
+fun Activity.changeStatusBarColorToWhite() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ window.statusBarColor = resources.getColor(R.color.white, theme)
+ window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+ }
+}
diff --git a/app/src/main/java/com/jeanjnap/bankapp/util/extension/LifecycleOwnerExtensions.kt b/app/src/main/java/com/jeanjnap/bankapp/util/extension/LifecycleOwnerExtensions.kt
new file mode 100644
index 000000000..cf0e21ed0
--- /dev/null
+++ b/app/src/main/java/com/jeanjnap/bankapp/util/extension/LifecycleOwnerExtensions.kt
@@ -0,0 +1,8 @@
+package com.jeanjnap.bankapp.util.extension
+
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LiveData
+
+fun LifecycleOwner.observe(data: LiveData, function: (id: T) -> Unit) {
+ data.observeSmart(this) { function.invoke(it) }
+}
diff --git a/app/src/main/java/com/jeanjnap/bankapp/util/extension/LiveDataExtensions.kt b/app/src/main/java/com/jeanjnap/bankapp/util/extension/LiveDataExtensions.kt
new file mode 100644
index 000000000..cb6db9407
--- /dev/null
+++ b/app/src/main/java/com/jeanjnap/bankapp/util/extension/LiveDataExtensions.kt
@@ -0,0 +1,8 @@
+package com.jeanjnap.bankapp.util.extension
+
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LiveData
+
+fun LiveData.observeSmart(owner: LifecycleOwner, observer: (T) -> Unit) {
+ observe(owner, { it?.let { source -> observer(source) } })
+}
diff --git a/app/src/main/java/com/jeanjnap/bankapp/util/extension/ViewDataBindingViewExtensions.kt b/app/src/main/java/com/jeanjnap/bankapp/util/extension/ViewDataBindingViewExtensions.kt
new file mode 100644
index 000000000..f2d9b7329
--- /dev/null
+++ b/app/src/main/java/com/jeanjnap/bankapp/util/extension/ViewDataBindingViewExtensions.kt
@@ -0,0 +1,7 @@
+package com.jeanjnap.bankapp.util.extension
+
+import androidx.databinding.ViewDataBinding
+
+inline fun ViewDataBinding.adapterDataBindingCast() =
+ this as? CLASS_DATA_BINDING_GENERATED
+ ?: throw ClassCastException("${CLASS_DATA_BINDING_GENERATED::class.java}")
diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 000000000..2b068d114
--- /dev/null
+++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 000000000..07d5da9cb
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bank_app_layout/assets/logout 2.png b/app/src/main/res/drawable/ic_logout.png
similarity index 100%
rename from bank_app_layout/assets/logout 2.png
rename to app/src/main/res/drawable/ic_logout.png
diff --git a/bank_app_layout/assets/Logo.png b/app/src/main/res/drawable/logo.png
similarity index 100%
rename from bank_app_layout/assets/Logo.png
rename to app/src/main/res/drawable/logo.png
diff --git a/app/src/main/res/drawable/shape_gray_fill_roud_corners.xml b/app/src/main/res/drawable/shape_gray_fill_roud_corners.xml
new file mode 100644
index 000000000..e5d42e263
--- /dev/null
+++ b/app/src/main/res/drawable/shape_gray_fill_roud_corners.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/font/helvetica_neue.ttf b/app/src/main/res/font/helvetica_neue.ttf
new file mode 100644
index 000000000..373e545f7
Binary files /dev/null and b/app/src/main/res/font/helvetica_neue.ttf differ
diff --git a/app/src/main/res/font/helvetica_neue_light.ttf b/app/src/main/res/font/helvetica_neue_light.ttf
new file mode 100644
index 000000000..7e9b412d2
Binary files /dev/null and b/app/src/main/res/font/helvetica_neue_light.ttf differ
diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml
new file mode 100644
index 000000000..45903e017
--- /dev/null
+++ b/app/src/main/res/layout/activity_login.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_statements.xml b/app/src/main/res/layout/activity_statements.xml
new file mode 100644
index 000000000..634865e36
--- /dev/null
+++ b/app/src/main/res/layout/activity_statements.xml
@@ -0,0 +1,225 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_shimmer.xml b/app/src/main/res/layout/item_shimmer.xml
new file mode 100644
index 000000000..92ee394de
--- /dev/null
+++ b/app/src/main/res/layout/item_shimmer.xml
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_statement.xml b/app/src/main/res/layout/item_statement.xml
new file mode 100644
index 000000000..2b7adb4f3
--- /dev/null
+++ b/app/src/main/res/layout/item_statement.xml
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..eca70cfe5
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..eca70cfe5
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..a571e6009
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 000000000..61da551c5
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..c41dd2853
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 000000000..db5080a75
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..6dba46dab
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..da31a871c
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..15ac68172
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..b216f2d31
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..f25a41974
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..e96783ccc
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
new file mode 100644
index 000000000..d00b90e3d
--- /dev/null
+++ b/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..52dd9b320
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,12 @@
+
+
+ #FFBB86FC
+ #FF3B48EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+ #FFA8B4C4
+ #FF485465
+
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 000000000..1c7951b09
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,22 @@
+
+
+ 0dp
+ 4dp
+ 6dp
+ 8dp
+ 12dp
+ 16dp
+ 20dp
+ 24dp
+ 28dp
+ 36dp
+ 90dp
+ 124dp
+ 190dp
+
+ 12sp
+ 15sp
+ 16sp
+ 20sp
+ 25sp
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..c03064dd4
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,14 @@
+
+ Bank App
+ Ops! Algo está errado. Tente mais tarde
+ Sem conexão à Internet
+ Login
+ User
+ Password
+ Loading…
+ Senha inválida
+ Usuário inválido
+ Conta
+ Saldo
+ Recentes
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings_accessibility.xml b/app/src/main/res/values/strings_accessibility.xml
new file mode 100644
index 000000000..d9d33d9d4
--- /dev/null
+++ b/app/src/main/res/values/strings_accessibility.xml
@@ -0,0 +1,5 @@
+
+
+ Logo
+ Logauti
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings_ex.xml b/app/src/main/res/values/strings_ex.xml
new file mode 100644
index 000000000..3e70a0839
--- /dev/null
+++ b/app/src/main/res/values/strings_ex.xml
@@ -0,0 +1,9 @@
+
+
+ Jose da Silva Teste
+ 2050 / 01.111222–4
+ R$1.000,00
+ Pagamento
+ 12/12/2018
+ Conta de Luz
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 000000000..fa4a9c26c
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/com/jeanjnap/bankapp/RobolectricBaseTest.kt b/app/src/test/java/com/jeanjnap/bankapp/RobolectricBaseTest.kt
new file mode 100644
index 000000000..bd61f0b1a
--- /dev/null
+++ b/app/src/test/java/com/jeanjnap/bankapp/RobolectricBaseTest.kt
@@ -0,0 +1,29 @@
+package com.jeanjnap.bankapp
+
+import android.app.Application
+import android.content.Context
+import android.os.Build
+import androidx.test.platform.app.InstrumentationRegistry
+import io.mockk.MockKAnnotations
+import org.junit.Before
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+
+@Suppress("DEPRECATION")
+@RunWith(RobolectricTestRunner::class)
+@Config(application = Application::class, sdk = [Build.VERSION_CODES.P])
+@LooperMode(LooperMode.Mode.LEGACY)
+abstract class RobolectricBaseTest {
+
+ lateinit var context: Context
+ lateinit var application: Application
+
+ @Before
+ fun initializeApplication() {
+ MockKAnnotations.init(this)
+ context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
+ application = InstrumentationRegistry.getInstrumentation().targetContext as Application
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/jeanjnap/bankapp/ui/ViewModelBaseTest.kt b/app/src/test/java/com/jeanjnap/bankapp/ui/ViewModelBaseTest.kt
new file mode 100644
index 000000000..1fd4453cb
--- /dev/null
+++ b/app/src/test/java/com/jeanjnap/bankapp/ui/ViewModelBaseTest.kt
@@ -0,0 +1,78 @@
+package com.jeanjnap.bankapp.ui
+
+import android.app.Application
+import android.content.Context
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.jeanjnap.bankapp.R
+import com.jeanjnap.bankapp.di.AppComponent
+import com.jeanjnap.bankapp.di.AppModules.utilsModules
+import com.jeanjnap.bankapp.di.DomainModules.domainModulesItems
+import com.jeanjnap.domain.boundary.ResourcesString
+import com.jeanjnap.infrastructure.network.Network
+import io.mockk.MockKAnnotations
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.impl.annotations.RelaxedMockK
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.koin.android.ext.koin.androidContext
+import org.koin.core.context.loadKoinModules
+import org.koin.core.context.startKoin
+import org.koin.core.context.stopKoin
+import org.koin.dsl.module
+import org.mockito.ArgumentMatchers.anyString
+import org.robolectric.annotation.Config
+
+@RunWith(JUnit4::class)
+@Config(application = Application::class)
+@ExperimentalCoroutinesApi
+abstract class ViewModelBaseTest {
+
+ @Rule
+ @JvmField
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @RelaxedMockK
+ protected lateinit var context: Context
+
+ @MockK
+ protected lateinit var network: Network
+
+ @MockK
+ protected lateinit var resourcesString: ResourcesString
+
+ @Before
+ fun mockitoInit() {
+ MockKAnnotations.init(this)
+ Dispatchers.setMain(Dispatchers.Unconfined)
+
+ startKoin {
+ androidContext(context)
+ modules(AppComponent.getAllModules())
+ unloadModules(utilsModules, domainModulesItems)
+ loadKoinModules(
+ module {
+ single { resourcesString }
+ }
+ )
+ }
+ every { resourcesString.genericError } returns anyString()
+ every { network.hasActiveInternetConnection() } returns true
+ every { context.getString(R.string.base_url) } returns DEFAULT_URL
+ }
+
+ @After
+ fun setUpAfter() {
+ stopKoin()
+ }
+
+ companion object {
+ private const val DEFAULT_URL = "https://localhost"
+ }
+}
diff --git a/app/src/test/java/com/jeanjnap/bankapp/ui/login/LoginFormTest.kt b/app/src/test/java/com/jeanjnap/bankapp/ui/login/LoginFormTest.kt
new file mode 100644
index 000000000..00808efc6
--- /dev/null
+++ b/app/src/test/java/com/jeanjnap/bankapp/ui/login/LoginFormTest.kt
@@ -0,0 +1,65 @@
+package com.jeanjnap.bankapp.ui.login
+
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyString
+
+class LoginFormTest {
+
+ private lateinit var form: LoginForm
+
+ @Before
+ fun setup() {
+ form = LoginForm()
+ }
+
+ @Test
+ fun setUser_withInvalidValue_shouldReturnsInvalidValidation() {
+ form.user = anyString()
+ assertFalse(form.isValidUsername())
+ }
+
+ @Test
+ fun setUser_withValidValue_shouldReturnsValidValidation() {
+ form.user = "user123@test.com"
+ assertTrue(form.isValidUsername())
+ }
+
+ @Test
+ fun setPassword_withInvalidValue_shouldReturnsInvalidValidation() {
+ form.pass = anyString()
+ assertFalse(form.isValidPassword())
+ }
+
+ @Test
+ fun setPassword_withValidValue_shouldReturnsValidValidation() {
+ form.pass = "aA!"
+ assertTrue(form.isValidPassword())
+ }
+
+ @Test
+ fun isFormValid_withValidUserAndInvalidPassword_shouldReturnsFalse() {
+ form.user = "user123@test.com"
+ form.pass = anyString()
+
+ assertFalse(form.isFormValid())
+ }
+
+ @Test
+ fun isFormValid_withInvalidUserAndValidPassword_shouldReturnsFalse() {
+ form.user = anyString()
+ form.pass = "aA!"
+
+ assertFalse(form.isFormValid())
+ }
+
+ @Test
+ fun isFormValid_withValidUserAndValidPassword_shouldReturnsTrue() {
+ form.user = "user123@test.com"
+ form.pass = "aA!"
+
+ assertTrue(form.isFormValid())
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/jeanjnap/bankapp/ui/login/LoginViewModelTest.kt b/app/src/test/java/com/jeanjnap/bankapp/ui/login/LoginViewModelTest.kt
new file mode 100644
index 000000000..7e49eb72f
--- /dev/null
+++ b/app/src/test/java/com/jeanjnap/bankapp/ui/login/LoginViewModelTest.kt
@@ -0,0 +1,105 @@
+package com.jeanjnap.bankapp.ui.login
+
+import com.jeanjnap.bankapp.ui.ViewModelBaseTest
+import com.jeanjnap.bankapp.util.extension.getOrAwaitValue
+import com.jeanjnap.domain.entity.ErrorResponse
+import com.jeanjnap.domain.entity.SuccessResponse
+import com.jeanjnap.domain.usecase.BankUseCase
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.mockk
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyString
+
+@ExperimentalCoroutinesApi
+class LoginViewModelTest : ViewModelBaseTest() {
+
+ private lateinit var viewModel: LoginViewModel
+
+ @MockK
+ private lateinit var bankUseCase: BankUseCase
+
+ @Before
+ fun setup() {
+ viewModel = LoginViewModel(network, bankUseCase)
+ }
+
+ @Test
+ fun onCreate_with_savedUser_shouldSetValues() {
+ every { bankUseCase.getUser() } returns anyString()
+
+ viewModel.onCreate()
+
+ assertEquals(anyString(), viewModel.username.getOrAwaitValue())
+ }
+
+ @Test
+ fun onCreate_without_savedUser_shouldNotSetValues() {
+ every { bankUseCase.getUser() } returns null
+
+ viewModel.onCreate()
+
+ assertNull(viewModel.username.value)
+ }
+
+ @Test
+ fun onUsernameChanged_shouldCleanUsernameError() {
+ viewModel.onUserNameChanged(anyString())
+
+ assertFalse(viewModel.usernameError.getOrAwaitValue())
+ }
+
+ @Test
+ fun onPasswordChanged_shouldCleanPasswordError() {
+ viewModel.onPasswordChanged(anyString())
+
+ assertFalse(viewModel.passwordError.getOrAwaitValue())
+ }
+
+ @Test
+ fun onLoginClick_withInvalidUsername_shouldSetUsernameError() {
+ viewModel.onPasswordChanged("aA!")
+ viewModel.onLoginClick()
+
+ assertTrue(viewModel.usernameError.getOrAwaitValue())
+ }
+
+ @Test
+ fun onLoginClick_withInvalidPassword_shouldSetPasswordError() {
+ viewModel.onUserNameChanged("user123@test.com")
+ viewModel.onLoginClick()
+
+ assertTrue(viewModel.passwordError.getOrAwaitValue())
+ }
+
+ @Test
+ fun onLoginClick_withInvalidValuesAndSuccessResponse_shouldSetLoginSuccess() {
+ coEvery { bankUseCase.login(any(), any()) } returns SuccessResponse(mockk())
+
+ viewModel.onUserNameChanged("user123@test.com")
+ viewModel.onPasswordChanged("aA!")
+ viewModel.onLoginClick()
+
+ assertNotNull(viewModel.loginSuccess.getOrAwaitValue())
+ }
+
+ @Test
+ fun onLoginClick_withInvalidValuesAndErrorResponse_shouldSetError() {
+ every { resourcesString.genericError } returns anyString()
+ coEvery { bankUseCase.login(any(), any()) } returns ErrorResponse(mockk())
+
+ viewModel.onUserNameChanged("user123@test.com")
+ viewModel.onPasswordChanged("aA!")
+ viewModel.onLoginClick()
+
+ assertEquals(anyString(), viewModel.error.getOrAwaitValue())
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/jeanjnap/bankapp/ui/statements/StatementsAdapterTest.kt b/app/src/test/java/com/jeanjnap/bankapp/ui/statements/StatementsAdapterTest.kt
new file mode 100644
index 000000000..821513c19
--- /dev/null
+++ b/app/src/test/java/com/jeanjnap/bankapp/ui/statements/StatementsAdapterTest.kt
@@ -0,0 +1,33 @@
+package com.jeanjnap.bankapp.ui.statements
+
+import android.widget.FrameLayout
+import com.jeanjnap.bankapp.R
+import com.jeanjnap.bankapp.RobolectricBaseTest
+import com.jeanjnap.domain.entity.Statement
+import io.mockk.impl.annotations.RelaxedMockK
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+
+class StatementsAdapterTest : RobolectricBaseTest() {
+
+ private lateinit var adapter: StatementsAdapter
+
+ @RelaxedMockK
+ private lateinit var statementlist: List
+
+ @Before
+ fun setup() {
+ adapter = StatementsAdapter()
+ }
+
+ @Test
+ fun onCreateViewHolder_shouldInflateItemReceiptLayout() {
+ adapter.submitList(statementlist)
+ val holder = adapter.onCreateViewHolder(FrameLayout(context), 0)
+ adapter.onBindViewHolder(holder, 0)
+
+ assertEquals(R.id.cl_statement_item, holder.itemView.rootView.id)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/jeanjnap/bankapp/ui/statements/StatementsViewModelTest.kt b/app/src/test/java/com/jeanjnap/bankapp/ui/statements/StatementsViewModelTest.kt
new file mode 100644
index 000000000..59a639edf
--- /dev/null
+++ b/app/src/test/java/com/jeanjnap/bankapp/ui/statements/StatementsViewModelTest.kt
@@ -0,0 +1,67 @@
+package com.jeanjnap.bankapp.ui.statements
+
+import com.jeanjnap.bankapp.ui.ViewModelBaseTest
+import com.jeanjnap.bankapp.util.extension.getOrAwaitValue
+import com.jeanjnap.domain.entity.ErrorResponse
+import com.jeanjnap.domain.entity.SuccessResponse
+import com.jeanjnap.domain.usecase.BankUseCase
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.impl.annotations.MockK
+import io.mockk.mockk
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyLong
+
+@ExperimentalCoroutinesApi
+class StatementsViewModelTest: ViewModelBaseTest() {
+
+ private lateinit var viewModel: StatementsViewModel
+
+ @MockK
+ private lateinit var bankUseCase: BankUseCase
+
+ @Before
+ fun setup() {
+ viewModel = StatementsViewModel(network, bankUseCase)
+ }
+
+ @Test
+ fun setUserId_shouldCallsGetStatements() {
+ coEvery { bankUseCase.getStatements(any()) } returns mockk()
+
+ viewModel.userId = anyLong()
+
+ coVerify { bankUseCase.getStatements(any()) }
+ }
+
+ @Test
+ fun onLogoutClick_shouldSetLogout() {
+ viewModel.onLogoutClick()
+
+ assertTrue(viewModel.onLogout.getOrAwaitValue())
+ }
+
+ @Test
+ fun handleStatementsResponse_withSuccessResponse_shouldSetStatements() {
+ coEvery { bankUseCase.getStatements(any()) } returns SuccessResponse(mockk())
+
+ viewModel.userId = anyLong()
+
+ coVerify { bankUseCase.getStatements(any()) }
+ assertNotNull(viewModel.statements.getOrAwaitValue())
+ }
+
+ @Test
+ fun handleStatementsResponse_withErrorResponse_shouldSetError() {
+ coEvery { bankUseCase.getStatements(any()) } returns ErrorResponse(mockk())
+
+ viewModel.userId = anyLong()
+
+ coVerify { bankUseCase.getStatements(any()) }
+ assertNotNull(viewModel.error.getOrAwaitValue())
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/jeanjnap/bankapp/util/extension/LiveDataExtensions.kt b/app/src/test/java/com/jeanjnap/bankapp/util/extension/LiveDataExtensions.kt
new file mode 100644
index 000000000..c31c4ade8
--- /dev/null
+++ b/app/src/test/java/com/jeanjnap/bankapp/util/extension/LiveDataExtensions.kt
@@ -0,0 +1,30 @@
+package com.jeanjnap.bankapp.util.extension
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Observer
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+
+fun LiveData.getOrAwaitValue(
+ time: Long = 2,
+ timeUnit: TimeUnit = TimeUnit.SECONDS
+): T {
+ var data: T? = null
+ val latch = CountDownLatch(1)
+ val observer = object : Observer {
+ override fun onChanged(o: T?) {
+ data = o
+ latch.countDown()
+ this@getOrAwaitValue.removeObserver(this)
+ }
+ }
+
+ this.observeForever(observer)
+ if (!latch.await(time, timeUnit)) {
+ throw TimeoutException("LiveData value was never set.")
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ return data as T
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/jeanjnap/bankapp/util/extension/ResourcesStringTest.kt b/app/src/test/java/com/jeanjnap/bankapp/util/extension/ResourcesStringTest.kt
new file mode 100644
index 000000000..5135e9ea0
--- /dev/null
+++ b/app/src/test/java/com/jeanjnap/bankapp/util/extension/ResourcesStringTest.kt
@@ -0,0 +1,28 @@
+package com.jeanjnap.bankapp.util.extension
+
+import com.jeanjnap.bankapp.RobolectricBaseTest
+import com.jeanjnap.bankapp.util.ResourcesStringImpl
+import com.jeanjnap.domain.boundary.ResourcesString
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+
+class ResourcesStringTest : RobolectricBaseTest() {
+
+ private lateinit var resourcesString: ResourcesString
+
+ @Before
+ fun setup() {
+ resourcesString = ResourcesStringImpl(context)
+ }
+
+ @Test
+ fun genericError_shouldReturnsMattingText() {
+ assertEquals("Ops! Algo está errado. Tente mais tarde", resourcesString.genericError)
+ }
+
+ @Test
+ fun noConnectionError_shouldReturnsMattingText() {
+ assertEquals("Sem conexão à Internet", resourcesString.noConnectionError)
+ }
+}
\ No newline at end of file
diff --git a/bank_app.sketch b/bank_app.sketch
deleted file mode 100644
index d4ed8cafc..000000000
Binary files a/bank_app.sketch and /dev/null differ
diff --git a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/3817562E-CBB6-4CAD-BBCF-FD468BAA446F@0.5x.png b/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/3817562E-CBB6-4CAD-BBCF-FD468BAA446F@0.5x.png
deleted file mode 100644
index 53a1cdeb7..000000000
Binary files a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/3817562E-CBB6-4CAD-BBCF-FD468BAA446F@0.5x.png and /dev/null differ
diff --git a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/3817562E-CBB6-4CAD-BBCF-FD468BAA446F@1x.png b/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/3817562E-CBB6-4CAD-BBCF-FD468BAA446F@1x.png
deleted file mode 100644
index fb33afda6..000000000
Binary files a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/3817562E-CBB6-4CAD-BBCF-FD468BAA446F@1x.png and /dev/null differ
diff --git a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/3817562E-CBB6-4CAD-BBCF-FD468BAA446F@2x.png b/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/3817562E-CBB6-4CAD-BBCF-FD468BAA446F@2x.png
deleted file mode 100644
index 5f7313021..000000000
Binary files a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/3817562E-CBB6-4CAD-BBCF-FD468BAA446F@2x.png and /dev/null differ
diff --git a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/3817562E-CBB6-4CAD-BBCF-FD468BAA446F@3x.png b/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/3817562E-CBB6-4CAD-BBCF-FD468BAA446F@3x.png
deleted file mode 100644
index 1eda7465e..000000000
Binary files a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/3817562E-CBB6-4CAD-BBCF-FD468BAA446F@3x.png and /dev/null differ
diff --git a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/B27D4452-A3F9-4F53-991A-9E914520B88A@0.5x.png b/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/B27D4452-A3F9-4F53-991A-9E914520B88A@0.5x.png
deleted file mode 100644
index b79e2ecfb..000000000
Binary files a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/B27D4452-A3F9-4F53-991A-9E914520B88A@0.5x.png and /dev/null differ
diff --git a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/B27D4452-A3F9-4F53-991A-9E914520B88A@1x.png b/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/B27D4452-A3F9-4F53-991A-9E914520B88A@1x.png
deleted file mode 100644
index b961132ed..000000000
Binary files a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/B27D4452-A3F9-4F53-991A-9E914520B88A@1x.png and /dev/null differ
diff --git a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/B27D4452-A3F9-4F53-991A-9E914520B88A@2x.png b/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/B27D4452-A3F9-4F53-991A-9E914520B88A@2x.png
deleted file mode 100644
index a9483ea0f..000000000
Binary files a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/B27D4452-A3F9-4F53-991A-9E914520B88A@2x.png and /dev/null differ
diff --git a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/B27D4452-A3F9-4F53-991A-9E914520B88A@3x.png b/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/B27D4452-A3F9-4F53-991A-9E914520B88A@3x.png
deleted file mode 100644
index af2a6f7bb..000000000
Binary files a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/B27D4452-A3F9-4F53-991A-9E914520B88A@3x.png and /dev/null differ
diff --git a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/CA2B476F-0F1E-42F9-B119-2A2B3380A4D4@0.5x.png b/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/CA2B476F-0F1E-42F9-B119-2A2B3380A4D4@0.5x.png
deleted file mode 100644
index 08ae82bd9..000000000
Binary files a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/CA2B476F-0F1E-42F9-B119-2A2B3380A4D4@0.5x.png and /dev/null differ
diff --git a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/CA2B476F-0F1E-42F9-B119-2A2B3380A4D4@1x.png b/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/CA2B476F-0F1E-42F9-B119-2A2B3380A4D4@1x.png
deleted file mode 100644
index 66948cabd..000000000
Binary files a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/CA2B476F-0F1E-42F9-B119-2A2B3380A4D4@1x.png and /dev/null differ
diff --git a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/CA2B476F-0F1E-42F9-B119-2A2B3380A4D4@2x.png b/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/CA2B476F-0F1E-42F9-B119-2A2B3380A4D4@2x.png
deleted file mode 100644
index 0d20e8026..000000000
Binary files a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/CA2B476F-0F1E-42F9-B119-2A2B3380A4D4@2x.png and /dev/null differ
diff --git a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/CA2B476F-0F1E-42F9-B119-2A2B3380A4D4@3x.png b/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/CA2B476F-0F1E-42F9-B119-2A2B3380A4D4@3x.png
deleted file mode 100644
index 8d42293ac..000000000
Binary files a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/CA2B476F-0F1E-42F9-B119-2A2B3380A4D4@3x.png and /dev/null differ
diff --git a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/artboard.png b/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/artboard.png
deleted file mode 100644
index 20c1aa4bb..000000000
Binary files a/bank_app_layout/83F6DA3D-9F1B-4816-92B9-A74AAED40206/artboard.png and /dev/null differ
diff --git a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/39D04EF6-4A67-4ED6-A6AC-624922CFD8A1@0.5x.png b/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/39D04EF6-4A67-4ED6-A6AC-624922CFD8A1@0.5x.png
deleted file mode 100644
index fed7233bc..000000000
Binary files a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/39D04EF6-4A67-4ED6-A6AC-624922CFD8A1@0.5x.png and /dev/null differ
diff --git a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/39D04EF6-4A67-4ED6-A6AC-624922CFD8A1@1x.png b/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/39D04EF6-4A67-4ED6-A6AC-624922CFD8A1@1x.png
deleted file mode 100644
index 5899f020f..000000000
Binary files a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/39D04EF6-4A67-4ED6-A6AC-624922CFD8A1@1x.png and /dev/null differ
diff --git a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/39D04EF6-4A67-4ED6-A6AC-624922CFD8A1@2x.png b/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/39D04EF6-4A67-4ED6-A6AC-624922CFD8A1@2x.png
deleted file mode 100644
index 2315098f8..000000000
Binary files a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/39D04EF6-4A67-4ED6-A6AC-624922CFD8A1@2x.png and /dev/null differ
diff --git a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/39D04EF6-4A67-4ED6-A6AC-624922CFD8A1@3x.png b/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/39D04EF6-4A67-4ED6-A6AC-624922CFD8A1@3x.png
deleted file mode 100644
index e47f46018..000000000
Binary files a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/39D04EF6-4A67-4ED6-A6AC-624922CFD8A1@3x.png and /dev/null differ
diff --git a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/4F2CC5EF-170E-45CA-94D1-9BCD2FA4A805@0.5x.png b/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/4F2CC5EF-170E-45CA-94D1-9BCD2FA4A805@0.5x.png
deleted file mode 100644
index 242c3fdaf..000000000
Binary files a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/4F2CC5EF-170E-45CA-94D1-9BCD2FA4A805@0.5x.png and /dev/null differ
diff --git a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/4F2CC5EF-170E-45CA-94D1-9BCD2FA4A805@1x.png b/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/4F2CC5EF-170E-45CA-94D1-9BCD2FA4A805@1x.png
deleted file mode 100644
index 23ff4626f..000000000
Binary files a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/4F2CC5EF-170E-45CA-94D1-9BCD2FA4A805@1x.png and /dev/null differ
diff --git a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/4F2CC5EF-170E-45CA-94D1-9BCD2FA4A805@2x.png b/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/4F2CC5EF-170E-45CA-94D1-9BCD2FA4A805@2x.png
deleted file mode 100644
index 35d07a14f..000000000
Binary files a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/4F2CC5EF-170E-45CA-94D1-9BCD2FA4A805@2x.png and /dev/null differ
diff --git a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/4F2CC5EF-170E-45CA-94D1-9BCD2FA4A805@3x.png b/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/4F2CC5EF-170E-45CA-94D1-9BCD2FA4A805@3x.png
deleted file mode 100644
index 97b68a58f..000000000
Binary files a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/4F2CC5EF-170E-45CA-94D1-9BCD2FA4A805@3x.png and /dev/null differ
diff --git a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/68A7CB4F-8C7B-480B-B544-271AB428EE8A@0.5x.png b/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/68A7CB4F-8C7B-480B-B544-271AB428EE8A@0.5x.png
deleted file mode 100644
index 117de969e..000000000
Binary files a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/68A7CB4F-8C7B-480B-B544-271AB428EE8A@0.5x.png and /dev/null differ
diff --git a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/68A7CB4F-8C7B-480B-B544-271AB428EE8A@1x.png b/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/68A7CB4F-8C7B-480B-B544-271AB428EE8A@1x.png
deleted file mode 100644
index 5ff77a675..000000000
Binary files a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/68A7CB4F-8C7B-480B-B544-271AB428EE8A@1x.png and /dev/null differ
diff --git a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/68A7CB4F-8C7B-480B-B544-271AB428EE8A@2x.png b/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/68A7CB4F-8C7B-480B-B544-271AB428EE8A@2x.png
deleted file mode 100644
index 73b2edbac..000000000
Binary files a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/68A7CB4F-8C7B-480B-B544-271AB428EE8A@2x.png and /dev/null differ
diff --git a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/68A7CB4F-8C7B-480B-B544-271AB428EE8A@3x.png b/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/68A7CB4F-8C7B-480B-B544-271AB428EE8A@3x.png
deleted file mode 100644
index beba6c0cf..000000000
Binary files a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/68A7CB4F-8C7B-480B-B544-271AB428EE8A@3x.png and /dev/null differ
diff --git a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/artboard.png b/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/artboard.png
deleted file mode 100644
index 9befa0cc5..000000000
Binary files a/bank_app_layout/CCA3949D-B416-466A-8949-425E5DCD35B5/artboard.png and /dev/null differ
diff --git a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/05376719-28C6-4537-A0B4-097E3C72AB41@0.5x.png b/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/05376719-28C6-4537-A0B4-097E3C72AB41@0.5x.png
deleted file mode 100644
index 10ad360f4..000000000
Binary files a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/05376719-28C6-4537-A0B4-097E3C72AB41@0.5x.png and /dev/null differ
diff --git a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/05376719-28C6-4537-A0B4-097E3C72AB41@1x.png b/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/05376719-28C6-4537-A0B4-097E3C72AB41@1x.png
deleted file mode 100644
index 5fbb12417..000000000
Binary files a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/05376719-28C6-4537-A0B4-097E3C72AB41@1x.png and /dev/null differ
diff --git a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/05376719-28C6-4537-A0B4-097E3C72AB41@2x.png b/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/05376719-28C6-4537-A0B4-097E3C72AB41@2x.png
deleted file mode 100644
index fde4b9b75..000000000
Binary files a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/05376719-28C6-4537-A0B4-097E3C72AB41@2x.png and /dev/null differ
diff --git a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/05376719-28C6-4537-A0B4-097E3C72AB41@3x.png b/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/05376719-28C6-4537-A0B4-097E3C72AB41@3x.png
deleted file mode 100644
index af0e0ac72..000000000
Binary files a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/05376719-28C6-4537-A0B4-097E3C72AB41@3x.png and /dev/null differ
diff --git a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/372B9348-D602-4A3B-8E91-A66250670461@0.5x.png b/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/372B9348-D602-4A3B-8E91-A66250670461@0.5x.png
deleted file mode 100644
index ec991238a..000000000
Binary files a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/372B9348-D602-4A3B-8E91-A66250670461@0.5x.png and /dev/null differ
diff --git a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/372B9348-D602-4A3B-8E91-A66250670461@1x.png b/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/372B9348-D602-4A3B-8E91-A66250670461@1x.png
deleted file mode 100644
index 7307b9e13..000000000
Binary files a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/372B9348-D602-4A3B-8E91-A66250670461@1x.png and /dev/null differ
diff --git a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/372B9348-D602-4A3B-8E91-A66250670461@2x.png b/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/372B9348-D602-4A3B-8E91-A66250670461@2x.png
deleted file mode 100644
index 1178f890d..000000000
Binary files a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/372B9348-D602-4A3B-8E91-A66250670461@2x.png and /dev/null differ
diff --git a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/372B9348-D602-4A3B-8E91-A66250670461@3x.png b/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/372B9348-D602-4A3B-8E91-A66250670461@3x.png
deleted file mode 100644
index f0d400bea..000000000
Binary files a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/372B9348-D602-4A3B-8E91-A66250670461@3x.png and /dev/null differ
diff --git a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/D78AA366-D361-4FA8-AACE-0B91A0581654@0.5x.png b/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/D78AA366-D361-4FA8-AACE-0B91A0581654@0.5x.png
deleted file mode 100644
index 5066a058b..000000000
Binary files a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/D78AA366-D361-4FA8-AACE-0B91A0581654@0.5x.png and /dev/null differ
diff --git a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/D78AA366-D361-4FA8-AACE-0B91A0581654@1x.png b/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/D78AA366-D361-4FA8-AACE-0B91A0581654@1x.png
deleted file mode 100644
index 831e7bb47..000000000
Binary files a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/D78AA366-D361-4FA8-AACE-0B91A0581654@1x.png and /dev/null differ
diff --git a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/D78AA366-D361-4FA8-AACE-0B91A0581654@2x.png b/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/D78AA366-D361-4FA8-AACE-0B91A0581654@2x.png
deleted file mode 100644
index d857b805a..000000000
Binary files a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/D78AA366-D361-4FA8-AACE-0B91A0581654@2x.png and /dev/null differ
diff --git a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/D78AA366-D361-4FA8-AACE-0B91A0581654@3x.png b/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/D78AA366-D361-4FA8-AACE-0B91A0581654@3x.png
deleted file mode 100644
index d91b98e6d..000000000
Binary files a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/D78AA366-D361-4FA8-AACE-0B91A0581654@3x.png and /dev/null differ
diff --git a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/artboard.png b/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/artboard.png
deleted file mode 100644
index 2acdd074b..000000000
Binary files a/bank_app_layout/D66AAA24-D61F-44D2-8D53-8B219D009DBF/artboard.png and /dev/null differ
diff --git a/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/CDEBC6CD-73C4-47CB-B879-8A426D8163E1@0.5x.png b/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/CDEBC6CD-73C4-47CB-B879-8A426D8163E1@0.5x.png
deleted file mode 100644
index 535dafe15..000000000
Binary files a/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/CDEBC6CD-73C4-47CB-B879-8A426D8163E1@0.5x.png and /dev/null differ
diff --git a/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/CDEBC6CD-73C4-47CB-B879-8A426D8163E1@1x.png b/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/CDEBC6CD-73C4-47CB-B879-8A426D8163E1@1x.png
deleted file mode 100644
index 466ef1c09..000000000
Binary files a/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/CDEBC6CD-73C4-47CB-B879-8A426D8163E1@1x.png and /dev/null differ
diff --git a/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/CDEBC6CD-73C4-47CB-B879-8A426D8163E1@2x.png b/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/CDEBC6CD-73C4-47CB-B879-8A426D8163E1@2x.png
deleted file mode 100644
index cb118e91b..000000000
Binary files a/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/CDEBC6CD-73C4-47CB-B879-8A426D8163E1@2x.png and /dev/null differ
diff --git a/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/CDEBC6CD-73C4-47CB-B879-8A426D8163E1@3x.png b/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/CDEBC6CD-73C4-47CB-B879-8A426D8163E1@3x.png
deleted file mode 100644
index a650b8199..000000000
Binary files a/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/CDEBC6CD-73C4-47CB-B879-8A426D8163E1@3x.png and /dev/null differ
diff --git a/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/E3013759-B45A-4E98-8B34-278A50F98591@0.5x.png b/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/E3013759-B45A-4E98-8B34-278A50F98591@0.5x.png
deleted file mode 100644
index de8e4b18f..000000000
Binary files a/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/E3013759-B45A-4E98-8B34-278A50F98591@0.5x.png and /dev/null differ
diff --git a/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/E3013759-B45A-4E98-8B34-278A50F98591@1x.png b/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/E3013759-B45A-4E98-8B34-278A50F98591@1x.png
deleted file mode 100644
index d720a4f0d..000000000
Binary files a/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/E3013759-B45A-4E98-8B34-278A50F98591@1x.png and /dev/null differ
diff --git a/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/E3013759-B45A-4E98-8B34-278A50F98591@2x.png b/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/E3013759-B45A-4E98-8B34-278A50F98591@2x.png
deleted file mode 100644
index 9f57e7902..000000000
Binary files a/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/E3013759-B45A-4E98-8B34-278A50F98591@2x.png and /dev/null differ
diff --git a/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/E3013759-B45A-4E98-8B34-278A50F98591@3x.png b/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/E3013759-B45A-4E98-8B34-278A50F98591@3x.png
deleted file mode 100644
index fee69c198..000000000
Binary files a/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/E3013759-B45A-4E98-8B34-278A50F98591@3x.png and /dev/null differ
diff --git a/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/artboard.png b/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/artboard.png
deleted file mode 100644
index c620ef44d..000000000
Binary files a/bank_app_layout/E822A087-B20C-42DD-8665-FE03A4AA8211/artboard.png and /dev/null differ
diff --git a/bank_app_layout/data.js b/bank_app_layout/data.js
deleted file mode 100644
index a692702f5..000000000
--- a/bank_app_layout/data.js
+++ /dev/null
@@ -1 +0,0 @@
-var pageData = {"exportEveryLayer":1,"sketchName":"","language":"en","I18N":{"UNIT":"Unit","SIZE":"Size","SHOWSLICE":"Show slices","DRAGTOSAVE":"Drag to desktop to save.","WIDTH":"Width","HEIGHT":"Height","BORDER":"Border","COLOR":"Color","FILLCOLOR":"Fill","RADIUS":"Radius","LAYERTEXT":"Content","FONTSIZE":"Font Size","CODE":"Code","EXPORT":"Export","FORMAT":"Format","EXPORTLAYER":"Export Activity Layer","COPYSUCCESS":"Copy Success","TEXTSHAREDSTYLE":"Text Styles","SHAPESHAREDSTYLE":"Layer Styles"},"pageOrder":["E1DD8600-6E88-4482-9447-55D03DD87354","6774A219-5AFB-46B1-8AD2-8BE278B93378"],"pageData":{"E1DD8600-6E88-4482-9447-55D03DD87354":{"pageId":"E1DD8600-6E88-4482-9447-55D03DD87354","name":"Symbols","artboardId":["D66AAA24-D61F-44D2-8D53-8B219D009DBF","CCA3949D-B416-466A-8949-425E5DCD35B5"]},"6774A219-5AFB-46B1-8AD2-8BE278B93378":{"pageId":"6774A219-5AFB-46B1-8AD2-8BE278B93378","name":"Page%201","artboardId":["E822A087-B20C-42DD-8665-FE03A4AA8211","83F6DA3D-9F1B-4816-92B9-A74AAED40206"]}},"artboard":{"D66AAA24-D61F-44D2-8D53-8B219D009DBF":{"id":"D66AAA24-D61F-44D2-8D53-8B219D009DBF","src":"D66AAA24-D61F-44D2-8D53-8B219D009DBF","name":"Status%20Bar%2FBlack%2F100%25","type":"MSSymbolMaster","x":100,"y":0,"zIndex":0,"width":375,"height":20,"sharedStyleType":"","sharedStyle":"","symbolId":"8F28FC9A-816F-4541-8734-BC8EF14D685F","slice":[],"layer":[{"id":"372B9348-D602-4A3B-8E91-A66250670461","src":"372B9348-D602-4A3B-8E91-A66250670461","name":"Battery","type":"MSShapeGroup","x":445,"y":6,"zIndex":1,"width":25,"height":10,"sharedStyleType":"","sharedStyle":"","style":{"background":"#030303","border-radius":"1.5px","width":"25px","height":"10px"},"radius":"1.5","background":"#030303"},{"id":"6CE93654-D8D2-4BD3-85AF-C2D9A33E8979","src":"6CE93654-D8D2-4BD3-85AF-C2D9A33E8979","name":"100%25","type":"MSTextLayer","x":409,"y":4,"zIndex":2,"width":33,"height":14,"sharedStyleType":"","sharedStyle":"","html":"100%25","style":{"font-family":"Helvetica","font-size":"12px","color":"#030303","letter-spacing":"0","text-align":"right"}},{"id":"5DE5A561-10FF-40B7-864C-B93F09A30B82","src":"5DE5A561-10FF-40B7-864C-B93F09A30B82","name":"9%3A41%20AM","type":"MSTextLayer","x":263,"y":4,"zIndex":3,"width":49,"height":14,"sharedStyleType":"","sharedStyle":"","html":"9%3A41%20AM","style":{"font-family":"Helvetica","font-size":"12px","color":"#030303","letter-spacing":"0","text-align":"center"}},{"id":"05376719-28C6-4537-A0B4-097E3C72AB41","src":"05376719-28C6-4537-A0B4-097E3C72AB41","name":"Wi-Fi","type":"MSShapeGroup","x":188,"y":6,"zIndex":4,"width":13,"height":10,"sharedStyleType":"","sharedStyle":"","style":{"background":"#030303","width":"13px","height":"10px"},"background":"#030303"},{"id":"5B618E12-BBE6-411D-85B1-8668F4646AC1","src":"5B618E12-BBE6-411D-85B1-8668F4646AC1","name":"Carrier","type":"MSTextLayer","x":144,"y":4,"zIndex":5,"width":40,"height":14,"sharedStyleType":"","sharedStyle":"","html":"Sketch","style":{"font-family":"Helvetica","font-size":"12px","color":"#030303","letter-spacing":"0","text-align":"left"}},{"id":"D78AA366-D361-4FA8-AACE-0B91A0581654","src":"D78AA366-D361-4FA8-AACE-0B91A0581654","name":"Mobile%20Signal","type":"MSShapeGroup","x":107,"y":8,"zIndex":6,"width":34,"height":6,"sharedStyleType":"","sharedStyle":"","style":{"background":"#030303","width":"34px","height":"6px"},"background":"#030303"}],"mask":{}},"CCA3949D-B416-466A-8949-425E5DCD35B5":{"id":"CCA3949D-B416-466A-8949-425E5DCD35B5","src":"CCA3949D-B416-466A-8949-425E5DCD35B5","name":"Status%20Bar%2FWhite%2F100%25","type":"MSSymbolMaster","x":100,"y":120,"zIndex":0,"width":375,"height":20,"sharedStyleType":"","sharedStyle":"","symbolId":"FC386540-E94F-407E-88AC-44E4621D13CF","slice":[],"layer":[{"id":"68A7CB4F-8C7B-480B-B544-271AB428EE8A","src":"68A7CB4F-8C7B-480B-B544-271AB428EE8A","name":"Battery","type":"MSShapeGroup","x":445,"y":126,"zIndex":1,"width":25,"height":10,"sharedStyleType":"","sharedStyle":"","style":{"background":"#ffffff","border-radius":"1.5px","width":"25px","height":"10px"},"radius":"1.5","background":"#ffffff"},{"id":"D4D25776-86EB-4EB6-8037-476B91D3D811","src":"D4D25776-86EB-4EB6-8037-476B91D3D811","name":"100%25","type":"MSTextLayer","x":409,"y":124,"zIndex":2,"width":33,"height":14,"sharedStyleType":"","sharedStyle":"","html":"100%25","style":{"font-family":"Helvetica","font-size":"12px","color":"#ffffff","letter-spacing":"0","text-align":"right"}},{"id":"A14DAE1C-2900-467F-B2EA-41189DAF5B7D","src":"A14DAE1C-2900-467F-B2EA-41189DAF5B7D","name":"9%3A41%20AM","type":"MSTextLayer","x":263,"y":124,"zIndex":3,"width":49,"height":14,"sharedStyleType":"","sharedStyle":"","html":"9%3A41%20AM","style":{"font-family":"Helvetica","font-size":"12px","color":"#ffffff","letter-spacing":"0","text-align":"center"}},{"id":"39D04EF6-4A67-4ED6-A6AC-624922CFD8A1","src":"39D04EF6-4A67-4ED6-A6AC-624922CFD8A1","name":"Wi-Fi","type":"MSShapeGroup","x":188,"y":126,"zIndex":4,"width":13,"height":10,"sharedStyleType":"","sharedStyle":"","style":{"background":"#ffffff","width":"13px","height":"10px"},"background":"#ffffff"},{"id":"FDA2694C-D63C-41E9-A93A-4BF70C3F3364","src":"FDA2694C-D63C-41E9-A93A-4BF70C3F3364","name":"Carrier","type":"MSTextLayer","x":144,"y":124,"zIndex":5,"width":40,"height":14,"sharedStyleType":"","sharedStyle":"","html":"Sketch","style":{"font-family":"Helvetica","font-size":"12px","color":"#ffffff","letter-spacing":"0","text-align":"left"}},{"id":"4F2CC5EF-170E-45CA-94D1-9BCD2FA4A805","src":"4F2CC5EF-170E-45CA-94D1-9BCD2FA4A805","name":"Mobile%20Signal","type":"MSShapeGroup","x":107,"y":128,"zIndex":6,"width":34,"height":6,"sharedStyleType":"","sharedStyle":"","style":{"background":"#ffffff","width":"34px","height":"6px"},"background":"#ffffff"}],"mask":{}},"E822A087-B20C-42DD-8665-FE03A4AA8211":{"id":"E822A087-B20C-42DD-8665-FE03A4AA8211","src":"E822A087-B20C-42DD-8665-FE03A4AA8211","name":"Location","type":"MSArtboardGroup","x":513,"y":55,"zIndex":0,"width":375,"height":667,"sharedStyleType":"","sharedStyle":"","slice":[],"layer":[{"id":"12DBC3B3-F637-40BB-AE47-0B1653A29FFC","src":"12DBC3B3-F637-40BB-AE47-0B1653A29FFC","name":"login","type":"MSTextLayer","x":680,"y":644,"zIndex":0,"width":42,"height":36,"sharedStyleType":"","sharedStyle":"","html":"Login%0A","style":{"font-family":"HelveticaNeue","font-size":"16px","color":"#ffffff","letter-spacing":"0.44px","text-align":"left"}},{"id":"A9F8A6FB-8379-4EF3-A6A9-B93581CF3A25","src":"A9F8A6FB-8379-4EF3-A6A9-B93581CF3A25","name":"Password","type":"MSTextLayer","x":543,"y":370,"zIndex":1,"width":69,"height":23,"sharedStyleType":"","sharedStyle":"","html":"Password","style":{"font-family":"HelveticaNeue","font-size":"15px","color":"#a8b4c4","letter-spacing":"0.29px","text-align":"center","line-height":"23px"}},{"id":"4D1D953A-F49D-42D4-AC1B-051E802681E8","src":"4D1D953A-F49D-42D4-AC1B-051E802681E8","name":"User","type":"MSTextLayer","x":543,"y":298,"zIndex":2,"width":33,"height":23,"sharedStyleType":"","sharedStyle":"","html":"User","style":{"font-family":"HelveticaNeue","font-size":"15px","color":"#a8b4c4","letter-spacing":"0.29px","text-align":"center","line-height":"23px"}},{"id":"CDEBC6CD-73C4-47CB-B879-8A426D8163E1","src":"CDEBC6CD-73C4-47CB-B879-8A426D8163E1","name":"Screen%20Shot%202018-10-18%20at%2022.49.51","type":"MSBitmapLayer","x":639,"y":111,"zIndex":3,"width":125,"height":70,"sharedStyleType":"","sharedStyle":""},{"id":"E3013759-B45A-4E98-8B34-278A50F98591","src":"E3013759-B45A-4E98-8B34-278A50F98591","name":"Status%20Bar%2FBlack%2F100%25","type":"MSSymbolInstance","x":513,"y":55,"zIndex":4,"width":375,"height":20,"sharedStyleType":"","sharedStyle":"","symbolId":"8F28FC9A-816F-4541-8734-BC8EF14D685F"}],"mask":{}},"83F6DA3D-9F1B-4816-92B9-A74AAED40206":{"id":"83F6DA3D-9F1B-4816-92B9-A74AAED40206","src":"83F6DA3D-9F1B-4816-92B9-A74AAED40206","name":"Currency","type":"MSArtboardGroup","x":988,"y":55,"zIndex":0,"width":375,"height":667,"sharedStyleType":"","sharedStyle":"","slice":[],"layer":[{"id":"09BAE240-CC06-46A5-B01E-A15117109EAC","src":"09BAE240-CC06-46A5-B01E-A15117109EAC","name":"Find%20out%20where","type":"MSTextLayer","x":1127,"y":491,"zIndex":0,"width":118,"height":18,"sharedStyleType":"","sharedStyle":"","html":"Find%20out%20where%20","style":{"font-family":"HelveticaNeue","font-size":"16px","color":"#ffffff","letter-spacing":"0.44px","text-align":"left"}},{"id":"2E3D38C9-2ED3-499B-BF39-008733AAE30C","src":"2E3D38C9-2ED3-499B-BF39-008733AAE30C","name":"Market%20rate%3A%201%20USD%20%3D","type":"MSTextLayer","x":1064,"y":420,"zIndex":1,"width":212,"height":17,"sharedStyleType":"","sharedStyle":"","html":"Market%20rate%3A%201%20USD%20%3D%200.8900%20EUR","style":{"font-family":"HelveticaNeue","font-size":"14px","color":"#a8b4c4","text-align":"center"}},{"id":"07B6D69B-B539-432D-A0F1-45428FCF5C13","src":"07B6D69B-B539-432D-A0F1-45428FCF5C13","name":"890%2C10","type":"MSTextLayer","x":1239,"y":351,"zIndex":2,"width":89,"height":33,"sharedStyleType":"","sharedStyle":"","html":"890%2C10","style":{"font-family":"HelveticaNeue-Light","font-size":"28px","color":"#485465","letter-spacing":"0.5px","text-align":"center"}},{"id":"99544A31-44A6-4BAD-B88E-99DDD345E9EA","src":"99544A31-44A6-4BAD-B88E-99DDD345E9EA","name":"1%20EUR%20%3D%201.123%20USD","type":"MSTextLayer","x":1216,"y":331,"zIndex":3,"width":110,"height":14,"sharedStyleType":"","sharedStyle":"","html":"1%20EUR%20%3D%201.123%20USD","style":{"font-family":"HelveticaNeue","font-size":"12px","color":"#a8b4c4","letter-spacing":"0.2px","text-align":"center"}},{"id":"66E60EFF-B648-4673-85F3-323550B705D3","src":"66E60EFF-B648-4673-85F3-323550B705D3","name":"EUR","type":"MSTextLayer","x":1065,"y":356,"zIndex":4,"width":49,"height":29,"sharedStyleType":"","sharedStyle":"","html":"EUR","style":{"font-family":"HelveticaNeue-Light","font-size":"25px","color":"#485465","text-align":"center"}},{"id":"B9A6B04C-614B-4F9E-8BD4-E59A0D050361","src":"B9A6B04C-614B-4F9E-8BD4-E59A0D050361","name":"I%20want","type":"MSTextLayer","x":1024,"y":325,"zIndex":5,"width":44,"height":18,"sharedStyleType":"","sharedStyle":"","html":"I%20want","style":{"font-family":"HelveticaNeue","font-size":"16px","color":"#a8b4c4","text-align":"center"}},{"id":"2B156AE2-C7CC-403A-AD6A-8414CCB60279","src":"2B156AE2-C7CC-403A-AD6A-8414CCB60279","name":"1%20000%2C00","type":"MSTextLayer","x":1214,"y":245,"zIndex":6,"width":113,"height":33,"sharedStyleType":"","sharedStyle":"","html":"1%20000%2C00","style":{"font-family":"HelveticaNeue-Light","font-size":"28px","color":"#485465","letter-spacing":"0.5px","text-align":"center"}},{"id":"246AE036-104B-41CF-B0F4-FCAEDF1CFC25","src":"246AE036-104B-41CF-B0F4-FCAEDF1CFC25","name":"1%20USD%20%3D%200.8900%20EUR","type":"MSTextLayer","x":1209,"y":224,"zIndex":7,"width":117,"height":14,"sharedStyleType":"","sharedStyle":"","html":"1%20USD%20%3D%200.8900%20EUR","style":{"font-family":"HelveticaNeue","font-size":"12px","color":"#a8b4c4","letter-spacing":"0.2px","text-align":"center"}},{"id":"3817562E-CBB6-4CAD-BBCF-FD468BAA446F","src":"3817562E-CBB6-4CAD-BBCF-FD468BAA446F","name":"Shape","type":"MSShapeGroup","x":1025,"y":247,"zIndex":8,"width":15,"height":15,"sharedStyleType":"","sharedStyle":"","style":{"background":"#0052b4","width":"15px","height":"15px"},"background":"#0052b4"},{"id":"D8815B79-8031-4DBF-AC66-0A035AD9905D","src":"D8815B79-8031-4DBF-AC66-0A035AD9905D","name":"USD","type":"MSTextLayer","x":1065,"y":248,"zIndex":9,"width":50,"height":29,"sharedStyleType":"","sharedStyle":"","html":"USD","style":{"font-family":"HelveticaNeue-Light","font-size":"25px","color":"#485465","text-align":"center"}},{"id":"4D7068B6-0C46-4E07-A300-CF23125F168C","src":"4D7068B6-0C46-4E07-A300-CF23125F168C","name":"I%20have","type":"MSTextLayer","x":1024,"y":218,"zIndex":10,"width":43,"height":18,"sharedStyleType":"","sharedStyle":"","html":"I%20have","style":{"font-family":"HelveticaNeue","font-size":"16px","color":"#a8b4c4","text-align":"center"}},{"id":"B27D4452-A3F9-4F53-991A-9E914520B88A","src":"B27D4452-A3F9-4F53-991A-9E914520B88A","name":"Shape","type":"MSShapeGroup","x":1016,"y":142,"zIndex":11,"width":11,"height":16,"sharedStyleType":"","sharedStyle":"","style":{"background":"#485465","width":"11px","height":"16px"},"background":"#485465"},{"id":"904C402D-FC98-45E0-9270-068D9717010D","src":"904C402D-FC98-45E0-9270-068D9717010D","name":"Paris%2C%20France","type":"MSTextLayer","x":1042,"y":143,"zIndex":12,"width":100,"height":20,"sharedStyleType":"","sharedStyle":"","html":"Paris%2C%20France","style":{"font-family":"HelveticaNeue","font-size":"17px","color":"#485465","text-align":"center"}},{"id":"4EE21DE4-3DDC-4464-B264-4223F0236C08","src":"4EE21DE4-3DDC-4464-B264-4223F0236C08","name":"Looking%20for%20the%20best","type":"MSTextLayer","x":1012,"y":168,"zIndex":13,"width":284,"height":16,"sharedStyleType":"","sharedStyle":"","html":"Looking%20for%20the%20best%20exchange%20rates%20in%20Paris%3F","style":{"font-family":"HelveticaNeue","font-size":"14px","color":"#a8b4c4","text-align":"center"}},{"id":"45B169DE-B3B5-4D12-B1B3-25F826B339CB","src":"45B169DE-B3B5-4D12-B1B3-25F826B339CB","name":"Currency","type":"MSTextLayer","x":1142,"y":87,"zIndex":14,"width":67,"height":20,"sharedStyleType":"","sharedStyle":"","html":"Currency","style":{"font-family":".SFNSText","font-size":"17px","color":"#ffffff","letter-spacing":"-0.41px","text-align":"center"}},{"id":"CA2B476F-0F1E-42F9-B119-2A2B3380A4D4","src":"CA2B476F-0F1E-42F9-B119-2A2B3380A4D4","name":"Status%20Bar%2FWhite%2F100%25","type":"MSSymbolInstance","x":988,"y":55,"zIndex":15,"width":375,"height":20,"sharedStyleType":"","sharedStyle":"","symbolId":"FC386540-E94F-407E-88AC-44E4621D13CF"}],"mask":{}}}}
\ No newline at end of file
diff --git a/bank_app_layout/index.html b/bank_app_layout/index.html
deleted file mode 100644
index 28125fac2..000000000
--- a/bank_app_layout/index.html
+++ /dev/null
@@ -1,1569 +0,0 @@
-
-
-
-
-
-
- Spec Export - Sketch Measure 2.4
-
-
-
-
-
-
-
diff --git a/bank_app_layout/links/page-1-currency.html b/bank_app_layout/links/page-1-currency.html
deleted file mode 100644
index 5652fa211..000000000
--- a/bank_app_layout/links/page-1-currency.html
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/bank_app_layout/links/page-1-location.html b/bank_app_layout/links/page-1-location.html
deleted file mode 100644
index 5ca1854a1..000000000
--- a/bank_app_layout/links/page-1-location.html
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/bank_app_layout/preview/page-1-currency.png b/bank_app_layout/preview/page-1-currency.png
deleted file mode 100644
index d0c171355..000000000
Binary files a/bank_app_layout/preview/page-1-currency.png and /dev/null differ
diff --git a/bank_app_layout/preview/page-1-location.png b/bank_app_layout/preview/page-1-location.png
deleted file mode 100644
index c620ef44d..000000000
Binary files a/bank_app_layout/preview/page-1-location.png and /dev/null differ
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 000000000..f64e07ccd
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,61 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+
+ ext {
+ min_sdk_version = 19
+ target_sdk_version = 30
+ compile_sdk_version = 30
+ build_tools_module_version = "30.0.0"
+ jvm_target_version = "1.8"
+ kotlin_version = "1.4.21"
+ build_tools_version = "4.1.1"
+ core_ktx_version = "1.3.2"
+ appcompat_version = "1.2.0"
+ material_version = "1.2.1"
+ constraintlayout_version = "2.0.4"
+ junit_4_version = "4.13.1"
+ junit_version = "1.1.2"
+ espresso_version = "3.3.0"
+ koin_version = '2.0.1'
+ coroutines_version = '1.4.1'
+ coroutines_test_version = "1.1.1"
+ retrofit_version = "2.5.0"
+ retrofit2_coroutines_adapter_version = "0.9.2"
+ retrofit_converter_moshi_version = "2.6.0"
+ moshi_version = '1.8.0'
+ moshi_codegen_version = "1.8.0"
+ kotlin_coroutines_adapter_version = "0.9.2"
+ kotlin_coroutines_android_version = "1.4.1"
+ logging_interceptor_version = "3.12.8"
+ shimmer_version = "0.5.0"
+ mockk_version = "1.10.5"
+ lifecycle_version = '1.1.1'
+ robolectric_version = '4.3.1'
+ androidx_runner_version = '1.2.0'
+ ui_automator_version = '2.2.0'
+ arch_core_testing_version = '2.1.0'
+ }
+
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath "com.android.tools.build:gradle:$build_tools_version"
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
\ No newline at end of file
diff --git a/data/.gitignore b/data/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/data/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/data/build.gradle b/data/build.gradle
new file mode 100644
index 000000000..48ac9671b
--- /dev/null
+++ b/data/build.gradle
@@ -0,0 +1,44 @@
+plugins {
+ id 'com.android.library'
+ id 'kotlin-android'
+}
+
+android {
+ compileSdkVersion rootProject.ext.compile_sdk_version
+ buildToolsVersion rootProject.ext.build_tools_module_version
+
+ defaultConfig {
+ minSdkVersion rootProject.ext.min_sdk_version
+ targetSdkVersion rootProject.ext.target_sdk_version
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = rootProject.ext.jvm_target_version
+ }
+}
+
+dependencies {
+
+ implementation project(':domain')
+ implementation project(':infrastructure')
+
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+ implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
+ implementation "com.squareup.moshi:moshi-kotlin:$moshi_version"
+ implementation "com.squareup.retrofit2:converter-moshi:$retrofit_converter_moshi_version"
+ implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:$kotlin_coroutines_adapter_version"
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_android_version"
+ implementation "org.koin:koin-core:$koin_version"
+ implementation "com.squareup.okhttp3:logging-interceptor:$logging_interceptor_version"
+
+ testImplementation "junit:junit:$junit_4_version"
+ testImplementation "io.mockk:mockk:$mockk_version"
+ testImplementation "android.arch.core:core-testing:$lifecycle_version"
+ testImplementation "org.robolectric:robolectric:$robolectric_version"
+ testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
+ testImplementation "org.koin:koin-test:$koin_version"
+}
\ No newline at end of file
diff --git a/data/consumer-rules.pro b/data/consumer-rules.pro
new file mode 100644
index 000000000..e69de29bb
diff --git a/data/proguard-rules.pro b/data/proguard-rules.pro
new file mode 100644
index 000000000..481bb4348
--- /dev/null
+++ b/data/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/data/src/main/AndroidManifest.xml b/data/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..51dcbbb9b
--- /dev/null
+++ b/data/src/main/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/data/src/main/java/com/jeanjnap/data/api/ApiResponse.kt b/data/src/main/java/com/jeanjnap/data/api/ApiResponse.kt
new file mode 100644
index 000000000..c0688b1af
--- /dev/null
+++ b/data/src/main/java/com/jeanjnap/data/api/ApiResponse.kt
@@ -0,0 +1,83 @@
+package com.jeanjnap.data.api
+
+import com.jeanjnap.data.mapper.Mapper
+import com.jeanjnap.data.model.error.ApiError
+import com.jeanjnap.data.util.moshi.InternalMoshiImpl
+import com.jeanjnap.domain.entity.EmptyResponse
+import com.jeanjnap.domain.entity.ErrorResponse
+import com.jeanjnap.domain.entity.SuccessResponse
+import com.jeanjnap.domain.entity.error.ErrorRequestType
+import com.jeanjnap.domain.entity.error.RequestError
+import com.jeanjnap.domain.entity.success.SuccessRequestType
+import retrofit2.Response
+import java.io.IOException
+
+sealed class ApiResponse {
+ companion object {
+ fun create(response: Response): ApiResponse {
+ return if (response.isSuccessful) {
+ val body = response.body()
+ val emptyBody =
+ (body is List<*> && body.size == 0) || response.code() == SuccessRequestType.CREATED.code
+ if (body == null || emptyBody) {
+ ApiEmptyResponse(
+ ApiError(response.code(), response.message(), response.message())
+ )
+ } else {
+ ApiSuccessResponse(body = body)
+ }
+ } else {
+ val messageClassError = parserStringJsonToApiError(response.errorBody()?.string())
+ val msg = response.errorBody()?.string()
+ val errorData =
+ if (messageClassError?.status != null) messageClassError else ApiError(
+ response.code(),
+ messageClassError?.error,
+ msg
+ )
+ ApiErrorResponse(errorData)
+ }
+ }
+ }
+}
+
+class ApiEmptyResponse(val error: ApiError) : ApiResponse()
+data class ApiSuccessResponse(val body: T) : ApiResponse()
+data class ApiErrorResponse(val error: ApiError) : ApiResponse()
+
+private fun ApiResponse.converter(mapper: Mapper): com.jeanjnap.domain.entity.Response {
+ return when (this) {
+ is ApiEmptyResponse -> EmptyResponse(
+ RequestError(
+ ErrorRequestType.convert(error.status),
+ error.errorMessage ?: error.error
+ )
+ )
+ is ApiSuccessResponse -> {
+ val itemConverted = mapper.transform(body)
+ SuccessResponse(itemConverted)
+ }
+ is ApiErrorResponse -> ErrorResponse(
+ RequestError(
+ ErrorRequestType.convert(error.status),
+ error.errorMessage ?: error.error
+ )
+ )
+ }
+}
+
+fun Response.create(mapper: Mapper): com.jeanjnap.domain.entity.Response {
+ return ApiResponse.create(this).converter(mapper)
+}
+
+fun parserStringJsonToApiError(bodyError: String?): ApiError? {
+ val moshi = InternalMoshiImpl()
+ val adapter = moshi.getMoshi().adapter(ApiError::class.java)
+ return if (bodyError == null) {
+ null
+ } else try {
+ adapter.fromJson(bodyError)
+ } catch (e: IOException) {
+ null
+ }
+}
diff --git a/data/src/main/java/com/jeanjnap/data/client/ApiClient.kt b/data/src/main/java/com/jeanjnap/data/client/ApiClient.kt
new file mode 100644
index 000000000..913447d6c
--- /dev/null
+++ b/data/src/main/java/com/jeanjnap/data/client/ApiClient.kt
@@ -0,0 +1,39 @@
+package com.jeanjnap.data.client
+
+import android.content.Context
+import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory
+import com.jeanjnap.data.R
+import com.jeanjnap.data.util.adapter.BigDecimalAdapter
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import retrofit2.Retrofit
+import retrofit2.converter.moshi.MoshiConverterFactory
+
+object ApiClient {
+
+ inline fun makeService(context: Context): T {
+ return OkHttpClient.Builder().apply {
+ addInterceptor(HttpLoggingInterceptor().apply { level = getLogLevel() })
+
+ }.build().let { retrofitCreate(context.getString(R.string.base_url), it) }
+ }
+
+ inline fun retrofitCreate(baseUrl: String, okHttpClient: OkHttpClient): T {
+ val retrofit = Retrofit.Builder()
+ .baseUrl(baseUrl)
+ .client(okHttpClient)
+ .addCallAdapterFactory(CoroutineCallAdapterFactory())
+ .addConverterFactory(MoshiConverterFactory.create(moshiFactory()).asLenient())
+ .build()
+ return retrofit.create(T::class.java)
+ }
+
+ fun getLogLevel() = HttpLoggingInterceptor.Level.BODY
+
+ fun moshiFactory(): Moshi = Moshi.Builder()
+ .add(KotlinJsonAdapterFactory())
+ .add(BigDecimalAdapter())
+ .build()
+}
\ No newline at end of file
diff --git a/data/src/main/java/com/jeanjnap/data/di/MapperModules.kt b/data/src/main/java/com/jeanjnap/data/di/MapperModules.kt
new file mode 100644
index 000000000..50a4f9134
--- /dev/null
+++ b/data/src/main/java/com/jeanjnap/data/di/MapperModules.kt
@@ -0,0 +1,23 @@
+package com.jeanjnap.data.di
+
+import com.jeanjnap.data.mapper.Mapper
+import com.jeanjnap.data.mapper.StatementSummaryResponseToStatementListMapper
+import com.jeanjnap.data.mapper.UserDataResponseToUserAccountMapper
+import com.jeanjnap.data.model.response.StatementSummaryResponse
+import com.jeanjnap.data.model.response.UserDataResponse
+import com.jeanjnap.domain.entity.Statement
+import com.jeanjnap.domain.entity.UserAccount
+import org.koin.core.qualifier.named
+import org.koin.dsl.module
+
+object MapperModules {
+ val mapperModulesItems = module {
+ single>(
+ named(UserDataResponseToUserAccountMapper::class.java.name)
+ ) { UserDataResponseToUserAccountMapper() }
+
+ single>>(
+ named(StatementSummaryResponseToStatementListMapper::class.java.name)
+ ) { StatementSummaryResponseToStatementListMapper() }
+ }
+}
\ No newline at end of file
diff --git a/data/src/main/java/com/jeanjnap/data/mapper/Mapper.kt b/data/src/main/java/com/jeanjnap/data/mapper/Mapper.kt
new file mode 100644
index 000000000..d01af3a03
--- /dev/null
+++ b/data/src/main/java/com/jeanjnap/data/mapper/Mapper.kt
@@ -0,0 +1,5 @@
+package com.jeanjnap.data.mapper
+
+interface Mapper {
+ fun transform(item: CLASS_IN?): CLASS_OUT
+}
diff --git a/data/src/main/java/com/jeanjnap/data/mapper/StatementSummaryResponseToStatementListMapper.kt b/data/src/main/java/com/jeanjnap/data/mapper/StatementSummaryResponseToStatementListMapper.kt
new file mode 100644
index 000000000..969c252b7
--- /dev/null
+++ b/data/src/main/java/com/jeanjnap/data/mapper/StatementSummaryResponseToStatementListMapper.kt
@@ -0,0 +1,16 @@
+package com.jeanjnap.data.mapper
+
+import com.jeanjnap.data.model.response.StatementSummaryResponse
+import com.jeanjnap.domain.entity.Statement
+import com.jeanjnap.domain.util.extensions.formatStringAsDate
+
+class StatementSummaryResponseToStatementListMapper: Mapper> {
+ override fun transform(item: StatementSummaryResponse?) = item?.statementList?.map {
+ Statement(
+ title = it.title,
+ desc = it.desc,
+ date = it.date?.formatStringAsDate(),
+ value = it.value
+ )
+ } ?: emptyList()
+}
\ No newline at end of file
diff --git a/data/src/main/java/com/jeanjnap/data/mapper/UserDataResponseToUserAccountMapper.kt b/data/src/main/java/com/jeanjnap/data/mapper/UserDataResponseToUserAccountMapper.kt
new file mode 100644
index 000000000..02f1aef0a
--- /dev/null
+++ b/data/src/main/java/com/jeanjnap/data/mapper/UserDataResponseToUserAccountMapper.kt
@@ -0,0 +1,14 @@
+package com.jeanjnap.data.mapper
+
+import com.jeanjnap.data.model.response.UserDataResponse
+import com.jeanjnap.domain.entity.UserAccount
+
+class UserDataResponseToUserAccountMapper: Mapper {
+ override fun transform(item: UserDataResponse?) = UserAccount(
+ userId = item?.userAccount?.userId,
+ name = item?.userAccount?.name,
+ bankAccount = item?.userAccount?.bankAccount,
+ agency = item?.userAccount?.agency,
+ balance = item?.userAccount?.balance
+ )
+}
diff --git a/data/src/main/java/com/jeanjnap/data/model/error/ApiError.kt b/data/src/main/java/com/jeanjnap/data/model/error/ApiError.kt
new file mode 100644
index 000000000..ec5e2be6e
--- /dev/null
+++ b/data/src/main/java/com/jeanjnap/data/model/error/ApiError.kt
@@ -0,0 +1,11 @@
+package com.jeanjnap.data.model.error
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class ApiError(
+ @Json(name = "status") val status: Int? = null,
+ @Json(name = "error") val error: String? = null,
+ @Json(name = "message") val errorMessage: String? = null
+)
diff --git a/data/src/main/java/com/jeanjnap/data/model/response/StatementResponse.kt b/data/src/main/java/com/jeanjnap/data/model/response/StatementResponse.kt
new file mode 100644
index 000000000..5e2f57a9a
--- /dev/null
+++ b/data/src/main/java/com/jeanjnap/data/model/response/StatementResponse.kt
@@ -0,0 +1,13 @@
+package com.jeanjnap.data.model.response
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import java.math.BigDecimal
+
+@JsonClass(generateAdapter = true)
+data class StatementResponse(
+ @Json(name = "title") val title: String?,
+ @Json(name = "desc") val desc: String?,
+ @Json(name = "date") val date: String?,
+ @Json(name = "value") val value: BigDecimal?
+)
diff --git a/data/src/main/java/com/jeanjnap/data/model/response/StatementSummaryResponse.kt b/data/src/main/java/com/jeanjnap/data/model/response/StatementSummaryResponse.kt
new file mode 100644
index 000000000..db4a45b35
--- /dev/null
+++ b/data/src/main/java/com/jeanjnap/data/model/response/StatementSummaryResponse.kt
@@ -0,0 +1,9 @@
+package com.jeanjnap.data.model.response
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class StatementSummaryResponse(
+ @Json(name = "statementList") val statementList: List?
+)
diff --git a/data/src/main/java/com/jeanjnap/data/model/response/UserAccountResponse.kt b/data/src/main/java/com/jeanjnap/data/model/response/UserAccountResponse.kt
new file mode 100644
index 000000000..5b8c59f91
--- /dev/null
+++ b/data/src/main/java/com/jeanjnap/data/model/response/UserAccountResponse.kt
@@ -0,0 +1,14 @@
+package com.jeanjnap.data.model.response
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import java.math.BigDecimal
+
+@JsonClass(generateAdapter = true)
+data class UserAccountResponse(
+ @Json(name = "userId") val userId: Long?,
+ @Json(name = "name") val name: String?,
+ @Json(name = "bankAccount") val bankAccount: String?,
+ @Json(name = "agency") val agency: String?,
+ @Json(name = "balance") val balance: BigDecimal?
+)
diff --git a/data/src/main/java/com/jeanjnap/data/model/response/UserDataResponse.kt b/data/src/main/java/com/jeanjnap/data/model/response/UserDataResponse.kt
new file mode 100644
index 000000000..3427e37be
--- /dev/null
+++ b/data/src/main/java/com/jeanjnap/data/model/response/UserDataResponse.kt
@@ -0,0 +1,9 @@
+package com.jeanjnap.data.model.response
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class UserDataResponse(
+ @Json(name = "userAccount") val userAccount: UserAccountResponse?
+)
\ No newline at end of file
diff --git a/data/src/main/java/com/jeanjnap/data/repository/BankRepositoryImpl.kt b/data/src/main/java/com/jeanjnap/data/repository/BankRepositoryImpl.kt
new file mode 100644
index 000000000..281bc8a06
--- /dev/null
+++ b/data/src/main/java/com/jeanjnap/data/repository/BankRepositoryImpl.kt
@@ -0,0 +1,29 @@
+package com.jeanjnap.data.repository
+
+import com.jeanjnap.data.source.local.BankLocalDataSource
+import com.jeanjnap.data.source.remote.BankRemoteDataSource
+import com.jeanjnap.domain.entity.Response
+import com.jeanjnap.domain.entity.Statement
+import com.jeanjnap.domain.entity.UserAccount
+import com.jeanjnap.domain.repository.BankRepository
+
+class BankRepositoryImpl(
+ private val bankRemoteDataSource: BankRemoteDataSource,
+ private val bankLocalDataSource: BankLocalDataSource
+) : BankRepository {
+ override suspend fun login(username: String, password: String): Response {
+ return bankRemoteDataSource.login(username, password)
+ }
+
+ override suspend fun getStatements(userId: Long?): Response> {
+ return bankRemoteDataSource.getStatements(userId)
+ }
+
+ override fun saveEncryptedUser(user: String) {
+ bankLocalDataSource.saveEncryptedUser(user)
+ }
+
+ override fun getUser(): String? {
+ return bankLocalDataSource.getEncryptedUser()
+ }
+}
\ No newline at end of file
diff --git a/data/src/main/java/com/jeanjnap/data/source/local/BankLocalDataSource.kt b/data/src/main/java/com/jeanjnap/data/source/local/BankLocalDataSource.kt
new file mode 100644
index 000000000..638cd564d
--- /dev/null
+++ b/data/src/main/java/com/jeanjnap/data/source/local/BankLocalDataSource.kt
@@ -0,0 +1,6 @@
+package com.jeanjnap.data.source.local
+
+interface BankLocalDataSource {
+ fun saveEncryptedUser(user: String)
+ fun getEncryptedUser(): String?
+}
\ No newline at end of file
diff --git a/data/src/main/java/com/jeanjnap/data/source/local/BankLocalDataSourceImpl.kt b/data/src/main/java/com/jeanjnap/data/source/local/BankLocalDataSourceImpl.kt
new file mode 100644
index 000000000..9c5a3e6f4
--- /dev/null
+++ b/data/src/main/java/com/jeanjnap/data/source/local/BankLocalDataSourceImpl.kt
@@ -0,0 +1,17 @@
+package com.jeanjnap.data.source.local
+
+class BankLocalDataSourceImpl(
+ private val cache: Cache
+): BankLocalDataSource {
+ override fun saveEncryptedUser(user: String) {
+ return cache.set(USER, user)
+ }
+
+ override fun getEncryptedUser(): String? {
+ return cache.nullableGet(USER, String::class.java)
+ }
+
+ companion object {
+ private const val USER = "USER"
+ }
+}
diff --git a/data/src/main/java/com/jeanjnap/data/source/local/Cache.kt b/data/src/main/java/com/jeanjnap/data/source/local/Cache.kt
new file mode 100644
index 000000000..33f86e8ab
--- /dev/null
+++ b/data/src/main/java/com/jeanjnap/data/source/local/Cache.kt
@@ -0,0 +1,8 @@
+package com.jeanjnap.data.source.local
+
+import java.lang.reflect.Type
+
+interface Cache {
+ fun nullableGet(key: String, type: Type, defaultValue: T? = null): T?
+ fun set(key: String, value: Any?)
+}
\ No newline at end of file
diff --git a/data/src/main/java/com/jeanjnap/data/source/local/CacheImpl.kt b/data/src/main/java/com/jeanjnap/data/source/local/CacheImpl.kt
new file mode 100644
index 000000000..eadd9229a
--- /dev/null
+++ b/data/src/main/java/com/jeanjnap/data/source/local/CacheImpl.kt
@@ -0,0 +1,35 @@
+package com.jeanjnap.data.source.local
+
+import android.content.Context
+import com.jeanjnap.data.util.moshi.InternalMoshi
+import com.squareup.moshi.JsonDataException
+import java.lang.reflect.Type
+
+class CacheImpl(
+ context: Context,
+ private val moshi: InternalMoshi
+): Cache {
+ private val prefs = context.getSharedPreferences(APP_PREFERENCES, Context.MODE_PRIVATE)
+
+ override fun nullableGet(key: String, type: Type, defaultValue: T?): T? {
+ return prefs.getString(key, null)?.let {
+ try {
+ moshi.getMoshi().adapter(type).fromJson(it)
+ } catch (e: JsonDataException) {
+ defaultValue
+ }
+ } ?: defaultValue
+ }
+
+ override fun set(key: String, value: Any?) {
+ with(prefs.edit()) {
+ if (value == null) remove(key)
+ else putString(key, moshi.getMoshi().adapter(Any::class.java).toJson(value))
+ apply()
+ }
+ }
+
+ companion object {
+ private const val APP_PREFERENCES = "APP_PREFERENCES"
+ }
+}
\ No newline at end of file
diff --git a/data/src/main/java/com/jeanjnap/data/source/remote/BankRemoteDataSource.kt b/data/src/main/java/com/jeanjnap/data/source/remote/BankRemoteDataSource.kt
new file mode 100644
index 000000000..014a6b0d4
--- /dev/null
+++ b/data/src/main/java/com/jeanjnap/data/source/remote/BankRemoteDataSource.kt
@@ -0,0 +1,10 @@
+package com.jeanjnap.data.source.remote
+
+import com.jeanjnap.domain.entity.Response
+import com.jeanjnap.domain.entity.Statement
+import com.jeanjnap.domain.entity.UserAccount
+
+interface BankRemoteDataSource {
+ suspend fun login(username: String, password: String): Response
+ suspend fun getStatements(userId: Long?): Response>
+}
\ No newline at end of file
diff --git a/data/src/main/java/com/jeanjnap/data/source/remote/BankRemoteDataSourceImpl.kt b/data/src/main/java/com/jeanjnap/data/source/remote/BankRemoteDataSourceImpl.kt
new file mode 100644
index 000000000..afba040c1
--- /dev/null
+++ b/data/src/main/java/com/jeanjnap/data/source/remote/BankRemoteDataSourceImpl.kt
@@ -0,0 +1,35 @@
+package com.jeanjnap.data.source.remote
+
+import com.jeanjnap.data.api.create
+import com.jeanjnap.data.mapper.StatementSummaryResponseToStatementListMapper
+import com.jeanjnap.data.mapper.UserDataResponseToUserAccountMapper
+import com.jeanjnap.data.source.remote.service.BankService
+import com.jeanjnap.domain.entity.ErrorResponse
+import com.jeanjnap.domain.entity.Response
+import com.jeanjnap.domain.entity.Statement
+import com.jeanjnap.domain.entity.UserAccount
+import com.jeanjnap.domain.entity.error.RequestError
+import java.lang.Exception
+
+class BankRemoteDataSourceImpl(
+ private val bankService: BankService,
+ private val userDataResponseToUserAccountMapper: UserDataResponseToUserAccountMapper,
+ private val statementListMapper: StatementSummaryResponseToStatementListMapper
+) : BankRemoteDataSource {
+ override suspend fun login(username: String, password: String): Response {
+ return try {
+ bankService.loginAsync(username, password).await()
+ .create(userDataResponseToUserAccountMapper)
+ } catch (e: Exception) {
+ ErrorResponse(RequestError(errorMessage = e.message))
+ }
+ }
+
+ override suspend fun getStatements(userId: Long?): Response> {
+ return try {
+ bankService.getStatementsAsync(userId).await().create(statementListMapper)
+ } catch (e: Exception) {
+ ErrorResponse(RequestError(errorMessage = e.message))
+ }
+ }
+}
diff --git a/data/src/main/java/com/jeanjnap/data/source/remote/service/BankService.kt b/data/src/main/java/com/jeanjnap/data/source/remote/service/BankService.kt
new file mode 100644
index 000000000..392f2ec1b
--- /dev/null
+++ b/data/src/main/java/com/jeanjnap/data/source/remote/service/BankService.kt
@@ -0,0 +1,26 @@
+package com.jeanjnap.data.source.remote.service
+
+import com.jeanjnap.data.model.response.StatementSummaryResponse
+import com.jeanjnap.data.model.response.UserDataResponse
+import kotlinx.coroutines.Deferred
+import retrofit2.Response
+import retrofit2.http.Field
+import retrofit2.http.FormUrlEncoded
+import retrofit2.http.GET
+import retrofit2.http.POST
+import retrofit2.http.Path
+
+interface BankService {
+
+ @FormUrlEncoded
+ @POST("login")
+ fun loginAsync(
+ @Field("user") user: String,
+ @Field("password") password: String
+ ): Deferred>
+
+ @GET("statements/{userId}")
+ fun getStatementsAsync(
+ @Path("userId") userId: Long?
+ ): Deferred>
+}
\ No newline at end of file
diff --git a/data/src/main/java/com/jeanjnap/data/util/adapter/BigDecimalAdapter.kt b/data/src/main/java/com/jeanjnap/data/util/adapter/BigDecimalAdapter.kt
new file mode 100644
index 000000000..24b06af75
--- /dev/null
+++ b/data/src/main/java/com/jeanjnap/data/util/adapter/BigDecimalAdapter.kt
@@ -0,0 +1,17 @@
+package com.jeanjnap.data.util.adapter
+
+import com.squareup.moshi.FromJson
+import com.squareup.moshi.ToJson
+import java.math.BigDecimal
+import java.text.ParseException
+
+class BigDecimalAdapter {
+
+ @FromJson
+ @Throws(ParseException::class)
+ fun fromJson(string: String) = BigDecimal(string)
+
+ @ToJson
+ @Throws(ParseException::class)
+ fun toJson(value: BigDecimal) = value.toString()
+}
diff --git a/data/src/main/java/com/jeanjnap/data/util/moshi/InternalMoshi.kt b/data/src/main/java/com/jeanjnap/data/util/moshi/InternalMoshi.kt
new file mode 100644
index 000000000..6bd4afb3d
--- /dev/null
+++ b/data/src/main/java/com/jeanjnap/data/util/moshi/InternalMoshi.kt
@@ -0,0 +1,7 @@
+package com.jeanjnap.data.util.moshi
+
+import com.squareup.moshi.Moshi
+
+interface InternalMoshi {
+ fun getMoshi(): Moshi
+}
diff --git a/data/src/main/java/com/jeanjnap/data/util/moshi/InternalMoshiImpl.kt b/data/src/main/java/com/jeanjnap/data/util/moshi/InternalMoshiImpl.kt
new file mode 100644
index 000000000..0582cc548
--- /dev/null
+++ b/data/src/main/java/com/jeanjnap/data/util/moshi/InternalMoshiImpl.kt
@@ -0,0 +1,10 @@
+package com.jeanjnap.data.util.moshi
+
+import com.squareup.moshi.Moshi
+
+class InternalMoshiImpl : InternalMoshi {
+
+ override fun getMoshi(): Moshi {
+ return Moshi.Builder().build()
+ }
+}
diff --git a/data/src/main/res/values/config.xml b/data/src/main/res/values/config.xml
new file mode 100644
index 000000000..04a3a9790
--- /dev/null
+++ b/data/src/main/res/values/config.xml
@@ -0,0 +1,4 @@
+
+
+ https://bank-app-test.herokuapp.com/api/
+
\ No newline at end of file
diff --git a/data/src/test/java/com/jeanjnap/data/RobolectricBaseTest.kt b/data/src/test/java/com/jeanjnap/data/RobolectricBaseTest.kt
new file mode 100644
index 000000000..2986b0b69
--- /dev/null
+++ b/data/src/test/java/com/jeanjnap/data/RobolectricBaseTest.kt
@@ -0,0 +1,29 @@
+package com.jeanjnap.data
+
+import android.app.Application
+import android.content.Context
+import android.os.Build
+import androidx.test.platform.app.InstrumentationRegistry
+import io.mockk.MockKAnnotations
+import org.junit.Before
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+
+@Suppress("DEPRECATION")
+@RunWith(RobolectricTestRunner::class)
+@Config(application = Application::class, sdk = [Build.VERSION_CODES.P])
+@LooperMode(LooperMode.Mode.LEGACY)
+abstract class RobolectricBaseTest {
+
+ lateinit var context: Context
+ lateinit var application: Application
+
+ @Before
+ fun initializeApplication() {
+ MockKAnnotations.init(this)
+ context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
+ application = InstrumentationRegistry.getInstrumentation().targetContext as Application
+ }
+}
\ No newline at end of file
diff --git a/data/src/test/java/com/jeanjnap/data/mapper/BaseMapperTest.kt b/data/src/test/java/com/jeanjnap/data/mapper/BaseMapperTest.kt
new file mode 100644
index 000000000..5405bd001
--- /dev/null
+++ b/data/src/test/java/com/jeanjnap/data/mapper/BaseMapperTest.kt
@@ -0,0 +1,31 @@
+package com.jeanjnap.data.mapper
+
+import com.jeanjnap.data.di.MapperModules
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.koin.core.context.startKoin
+import org.koin.core.qualifier.named
+import org.koin.test.AutoCloseKoinTest
+import org.koin.test.inject
+
+abstract class BaseMapperTest(name: String) : AutoCloseKoinTest() {
+
+ open val mapper: Mapper by inject(named(name))
+
+ @Before
+ fun setUpKoin() {
+ startKoin {
+ modules(MapperModules.mapperModulesItems)
+ }
+ }
+
+ @Test
+ open fun mapClassInToClassOut() {
+ assertEquals(mapper.transform(mockClassIn()), mockClassOut())
+ }
+
+ abstract fun mockClassIn(): CLASS_IN
+
+ abstract fun mockClassOut(): CLASS_OUT
+}
\ No newline at end of file
diff --git a/data/src/test/java/com/jeanjnap/data/mapper/StatementSummaryResponseToStatementListMapperTest.kt b/data/src/test/java/com/jeanjnap/data/mapper/StatementSummaryResponseToStatementListMapperTest.kt
new file mode 100644
index 000000000..4b2ece057
--- /dev/null
+++ b/data/src/test/java/com/jeanjnap/data/mapper/StatementSummaryResponseToStatementListMapperTest.kt
@@ -0,0 +1,44 @@
+package com.jeanjnap.data.mapper
+
+import com.jeanjnap.data.model.response.StatementResponse
+import com.jeanjnap.data.model.response.StatementSummaryResponse
+import com.jeanjnap.domain.entity.Statement
+import java.math.BigDecimal
+import java.util.Date
+
+class StatementSummaryResponseToStatementListMapperTest :
+ BaseMapperTest>(
+ StatementSummaryResponseToStatementListMapper::class.java.name
+ ) {
+ override fun mockClassIn() = StatementSummaryResponse(
+ statementList = listOf(
+ StatementResponse(
+ title = "Pagamento",
+ desc = "Conta de luz",
+ date = "2021-01-17",
+ value = BigDecimal.TEN
+ ),
+ StatementResponse(
+ title = null,
+ desc = null,
+ date = null,
+ value = null
+ )
+ )
+ )
+
+ override fun mockClassOut(): List = listOf(
+ Statement(
+ title = "Pagamento",
+ desc = "Conta de luz",
+ date = Date(1610852400000),
+ value = BigDecimal.TEN
+ ),
+ Statement(
+ title = null,
+ desc = null,
+ date = null,
+ value = null
+ )
+ )
+}
\ No newline at end of file
diff --git a/data/src/test/java/com/jeanjnap/data/mapper/UserDataResponseToUserAccountMapperTest.kt b/data/src/test/java/com/jeanjnap/data/mapper/UserDataResponseToUserAccountMapperTest.kt
new file mode 100644
index 000000000..598fce474
--- /dev/null
+++ b/data/src/test/java/com/jeanjnap/data/mapper/UserDataResponseToUserAccountMapperTest.kt
@@ -0,0 +1,47 @@
+package com.jeanjnap.data.mapper
+
+import com.jeanjnap.data.model.response.UserAccountResponse
+import com.jeanjnap.data.model.response.UserDataResponse
+import com.jeanjnap.domain.entity.UserAccount
+import org.junit.Assert
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import java.math.BigDecimal
+
+class UserDataResponseToUserAccountMapperTest : BaseMapperTest(
+ UserDataResponseToUserAccountMapper::class.java.name
+) {
+ override fun mockClassIn() = UserDataResponse(
+ userAccount = UserAccountResponse(
+ userId = 1L,
+ name = "Jose da Silva Teste",
+ bankAccount = "1234",
+ agency = "123456",
+ balance = BigDecimal.TEN
+ )
+ )
+
+ override fun mockClassOut() = UserAccount(
+ userId = 1L,
+ name = "Jose da Silva Teste",
+ bankAccount = "1234",
+ agency = "123456",
+ balance = BigDecimal.TEN
+ )
+
+ @Test
+ fun withNullValues() {
+ assertEquals(
+ mapper.transform(
+ UserDataResponse(userAccount = null)
+ ),
+ UserAccount(
+ userId = null,
+ name = null,
+ bankAccount = null,
+ agency = null,
+ balance = null
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/data/src/test/java/com/jeanjnap/data/repository/BankRepositoryTest.kt b/data/src/test/java/com/jeanjnap/data/repository/BankRepositoryTest.kt
new file mode 100644
index 000000000..2328aa4fb
--- /dev/null
+++ b/data/src/test/java/com/jeanjnap/data/repository/BankRepositoryTest.kt
@@ -0,0 +1,82 @@
+package com.jeanjnap.data.repository
+
+import com.jeanjnap.data.source.local.BankLocalDataSource
+import com.jeanjnap.data.source.remote.BankRemoteDataSource
+import com.jeanjnap.domain.entity.SuccessResponse
+import com.jeanjnap.domain.repository.BankRepository
+import io.mockk.MockKAnnotations
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.ArgumentMatchers.anyString
+
+class BankRepositoryTest {
+
+ private lateinit var repository: BankRepository
+
+ @MockK
+ private lateinit var bankRemoteDataSource: BankRemoteDataSource
+
+ @MockK
+ private lateinit var bankLocalDataSource: BankLocalDataSource
+
+ @Before
+ fun setup() {
+ MockKAnnotations.init(this)
+ repository = BankRepositoryImpl(bankRemoteDataSource, bankLocalDataSource)
+ }
+
+ @Test
+ fun login_shouldCallsBankRemoteDataSource() {
+ coEvery { bankRemoteDataSource.login(any(), any()) } returns SuccessResponse(mockk())
+
+ runBlocking { repository.login(anyString(), anyString()) }
+
+ coVerify { bankRemoteDataSource.login(any(), any()) }
+ }
+
+ @Test
+ fun getStatements_shouldCallsRemoteDataSource() {
+ coEvery { bankRemoteDataSource.getStatements(any()) } returns mockk()
+
+ runBlocking { repository.getStatements(anyLong()) }
+
+ coVerify { bankRemoteDataSource.getStatements(any()) }
+ }
+
+ @Test
+ fun saveEncryptedUser_shouldCallsLocalDataSource() {
+ every { bankLocalDataSource.saveEncryptedUser(any()) } returns Unit
+
+ repository.saveEncryptedUser(anyString())
+
+ verify { bankLocalDataSource.saveEncryptedUser(any()) }
+ }
+
+ @Test
+ fun getUser_withSavedUser_shouldCallsReturnsEncryptedUser() {
+ every { bankLocalDataSource.getEncryptedUser() } returns anyString()
+
+ assertEquals(anyString(), repository.getUser())
+
+ verify { bankLocalDataSource.getEncryptedUser() }
+ }
+
+ @Test
+ fun getUser_withNonSavedUser_shouldNotCallsGetEncryptedUserAndDecrypt() {
+ every { bankLocalDataSource.getEncryptedUser() } returns null
+
+ assertNull(repository.getUser())
+
+ verify { bankLocalDataSource.getEncryptedUser() }
+ }
+}
\ No newline at end of file
diff --git a/data/src/test/java/com/jeanjnap/data/source/local/BankLocalDataSourceTest.kt b/data/src/test/java/com/jeanjnap/data/source/local/BankLocalDataSourceTest.kt
new file mode 100644
index 000000000..c1e2dfc39
--- /dev/null
+++ b/data/src/test/java/com/jeanjnap/data/source/local/BankLocalDataSourceTest.kt
@@ -0,0 +1,50 @@
+package com.jeanjnap.data.source.local
+
+import io.mockk.MockKAnnotations
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.verify
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyString
+
+class BankLocalDataSourceTest {
+
+ private lateinit var dataSource: BankLocalDataSource
+
+ @MockK
+ private lateinit var cache: Cache
+
+ @Before
+ fun setup() {
+ MockKAnnotations.init(this)
+ dataSource = BankLocalDataSourceImpl(cache)
+ }
+
+ @Test
+ fun saveEncryptedUser_shouldCallsCash() {
+ every { cache.set(any(), any()) } returns Unit
+
+ dataSource.saveEncryptedUser(anyString())
+
+ verify { cache.set(any(), any()) }
+ }
+
+ @Test
+ fun getEncryptedUser_withUserSaved_shouldReturnsEncryptedUser() {
+ every { cache.nullableGet(any(), any()) } returns anyString()
+
+ assertEquals(anyString(), dataSource.getEncryptedUser())
+ verify { cache.nullableGet(any(), any()) }
+ }
+
+ @Test
+ fun getEncryptedUser_withoutUserSaved_shouldReturnsNull() {
+ every { cache.nullableGet(any(), any()) } returns null
+
+ assertNull(dataSource.getEncryptedUser())
+ verify { cache.nullableGet(any(), any()) }
+ }
+}
\ No newline at end of file
diff --git a/data/src/test/java/com/jeanjnap/data/source/local/CacheTest.kt b/data/src/test/java/com/jeanjnap/data/source/local/CacheTest.kt
new file mode 100644
index 000000000..d4f671e1b
--- /dev/null
+++ b/data/src/test/java/com/jeanjnap/data/source/local/CacheTest.kt
@@ -0,0 +1,54 @@
+package com.jeanjnap.data.source.local
+
+import com.jeanjnap.data.RobolectricBaseTest
+import com.jeanjnap.data.util.moshi.InternalMoshiImpl
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyString
+import java.util.Date
+
+class CacheTest: RobolectricBaseTest() {
+
+ private lateinit var cache: Cache
+
+ @Before
+ fun setup() {
+ cache = CacheImpl(context, InternalMoshiImpl())
+ }
+
+ @Test
+ fun set_shouldSaveValueInSharedPreferencesAndRetrieveWithNullableGet() {
+ cache.set(KEY, VALUE)
+
+ assertEquals(VALUE, cache.nullableGet(KEY, String::class.java))
+ }
+
+ @Test
+ fun nullableGet_withNonSavedKey_shouldReturnsNull() {
+ assertNull(cache.nullableGet(NON_SAVED_KEY, String::class.java))
+ }
+
+ @Test
+ fun nullableGet_withDifferentType_shouldReturnsNull() {
+ cache.set(KEY, VALUE)
+
+ assertNull(cache.nullableGet(NON_SAVED_KEY, Date::class.java))
+ }
+
+ @Test
+ fun nullableGet_withJsonDataException_shouldReturnsNull() {
+ cache.set(KEY, anyString())
+
+ assertNull(cache.nullableGet(KEY, Sample::class.java))
+ }
+
+ data class Sample(val test: String)
+
+ companion object {
+ private const val KEY = "key"
+ private const val NON_SAVED_KEY = "nonSavedKey"
+ private const val VALUE = "value"
+ }
+}
\ No newline at end of file
diff --git a/data/src/test/java/com/jeanjnap/data/source/remote/BankRemoteDataSourceTest.kt b/data/src/test/java/com/jeanjnap/data/source/remote/BankRemoteDataSourceTest.kt
new file mode 100644
index 000000000..e2d774e80
--- /dev/null
+++ b/data/src/test/java/com/jeanjnap/data/source/remote/BankRemoteDataSourceTest.kt
@@ -0,0 +1,127 @@
+package com.jeanjnap.data.source.remote
+
+import com.jeanjnap.data.mapper.StatementSummaryResponseToStatementListMapper
+import com.jeanjnap.data.mapper.UserDataResponseToUserAccountMapper
+import com.jeanjnap.data.model.response.StatementSummaryResponse
+import com.jeanjnap.data.model.response.UserDataResponse
+import com.jeanjnap.data.source.remote.service.BankService
+import com.jeanjnap.domain.entity.ErrorResponse
+import com.jeanjnap.domain.entity.Statement
+import com.jeanjnap.domain.entity.SuccessResponse
+import com.jeanjnap.domain.entity.UserAccount
+import io.mockk.MockKAnnotations
+import io.mockk.coEvery
+import io.mockk.impl.annotations.MockK
+import io.mockk.mockk
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.runBlocking
+import okhttp3.ResponseBody
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.ArgumentMatchers.anyString
+import retrofit2.Response
+import java.io.IOException
+
+class BankRemoteDataSourceTest {
+
+ private lateinit var dataSource: BankRemoteDataSource
+
+ @MockK
+ private lateinit var bankService: BankService
+
+ @Before
+ fun setup() {
+ MockKAnnotations.init(this)
+ dataSource = BankRemoteDataSourceImpl(
+ bankService = bankService,
+ userDataResponseToUserAccountMapper = UserDataResponseToUserAccountMapper(),
+ statementListMapper = StatementSummaryResponseToStatementListMapper()
+ )
+ }
+
+ @Test
+ fun login_withSuccessResponse_shouldReturnSuccessResponseOfUserAccount() {
+ coEvery {
+ bankService.loginAsync(any(), any())
+ } returns CompletableDeferred(mockSuccessLoginResponse())
+
+ runBlocking {
+ assertTrue(dataSource.login(anyString(), anyString()) is SuccessResponse)
+ }
+ }
+
+ @Test
+ fun login_withErrorResponse_shouldReturnErrorResponseOfUserAccount() {
+ coEvery {
+ bankService.loginAsync(any(), any())
+ } returns CompletableDeferred(mockErrorLoginResponse())
+
+ runBlocking {
+ assertTrue(dataSource.login(anyString(), anyString()) is ErrorResponse)
+ }
+ }
+
+ @Test
+ fun login_withIOException_shouldReturnErrorResponseOfUserAccount() {
+ coEvery { bankService.loginAsync(any(), any()) } throws IOException()
+
+ runBlocking {
+ assertTrue(dataSource.login(anyString(), anyString()) is ErrorResponse)
+ }
+ }
+
+ @Test
+ fun getStatements_withSuccessResponse_shouldReturnsSuccessResponseOfStatementList() {
+ coEvery {
+ bankService.getStatementsAsync(any())
+ } returns CompletableDeferred(mockSuccessStatementsResponse())
+
+ runBlocking {
+ assertTrue(dataSource.getStatements(anyLong()) is SuccessResponse>)
+ }
+ }
+
+ @Test
+ fun getStatements_withErrorResponse_shouldReturnErrorResponseOfStatementList() {
+ coEvery {
+ bankService.getStatementsAsync(any())
+ } returns CompletableDeferred(mockErrorStatementsResponse())
+
+ runBlocking {
+ assertTrue(dataSource.getStatements(anyLong()) is ErrorResponse>)
+ }
+ }
+
+ @Test
+ fun getStatements_withIOException_shouldReturnErrorResponseOfStatementList() {
+ coEvery { bankService.getStatementsAsync(any()) } throws IOException()
+
+ runBlocking {
+ assertTrue(dataSource.getStatements(anyLong()) is ErrorResponse>)
+ }
+ }
+
+ private fun mockSuccessLoginResponse(): Response {
+ return Response.success(mockk(relaxed = true))
+ }
+
+ private fun mockErrorLoginResponse(): Response {
+ return Response.error(
+ mockk(),
+ mockk(relaxed = true)
+ )
+ }
+
+ private fun mockSuccessStatementsResponse(): Response {
+ return Response.success(mockk(relaxed = true))
+ }
+
+ private fun mockErrorStatementsResponse(): Response {
+ return Response.error(
+ mockk(),
+ mockk(relaxed = true)
+ )
+ }
+}
\ No newline at end of file
diff --git a/domain/.gitignore b/domain/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/domain/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/domain/build.gradle b/domain/build.gradle
new file mode 100644
index 000000000..84f34ba59
--- /dev/null
+++ b/domain/build.gradle
@@ -0,0 +1,32 @@
+plugins {
+ id 'com.android.library'
+ id 'kotlin-android'
+}
+
+android {
+ compileSdkVersion rootProject.ext.compile_sdk_version
+ buildToolsVersion rootProject.ext.build_tools_module_version
+
+ defaultConfig {
+ minSdkVersion rootProject.ext.min_sdk_version
+ targetSdkVersion rootProject.ext.target_sdk_version
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = rootProject.ext.jvm_target_version
+ }
+}
+
+dependencies {
+ implementation project(':infrastructure')
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+
+ testImplementation "junit:junit:$junit_4_version"
+ testImplementation "io.mockk:mockk:$mockk_version"
+ testImplementation "android.arch.core:core-testing:$lifecycle_version"
+ testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
+}
\ No newline at end of file
diff --git a/domain/consumer-rules.pro b/domain/consumer-rules.pro
new file mode 100644
index 000000000..e69de29bb
diff --git a/domain/proguard-rules.pro b/domain/proguard-rules.pro
new file mode 100644
index 000000000..481bb4348
--- /dev/null
+++ b/domain/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/domain/src/main/AndroidManifest.xml b/domain/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..87fea4ba0
--- /dev/null
+++ b/domain/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/domain/src/main/java/com/jeanjnap/domain/boundary/ResourcesString.kt b/domain/src/main/java/com/jeanjnap/domain/boundary/ResourcesString.kt
new file mode 100644
index 000000000..a2db4774f
--- /dev/null
+++ b/domain/src/main/java/com/jeanjnap/domain/boundary/ResourcesString.kt
@@ -0,0 +1,6 @@
+package com.jeanjnap.domain.boundary
+
+interface ResourcesString {
+ val genericError: String
+ val noConnectionError: String
+}
diff --git a/domain/src/main/java/com/jeanjnap/domain/entity/Response.kt b/domain/src/main/java/com/jeanjnap/domain/entity/Response.kt
new file mode 100644
index 000000000..6f8ccad50
--- /dev/null
+++ b/domain/src/main/java/com/jeanjnap/domain/entity/Response.kt
@@ -0,0 +1,8 @@
+package com.jeanjnap.domain.entity
+
+import com.jeanjnap.domain.entity.error.RequestError
+
+sealed class Response
+class EmptyResponse(val error: RequestError) : Response()
+data class SuccessResponse(val body: T) : Response()
+data class ErrorResponse(val error: RequestError) : Response()
diff --git a/domain/src/main/java/com/jeanjnap/domain/entity/Statement.kt b/domain/src/main/java/com/jeanjnap/domain/entity/Statement.kt
new file mode 100644
index 000000000..f2423feda
--- /dev/null
+++ b/domain/src/main/java/com/jeanjnap/domain/entity/Statement.kt
@@ -0,0 +1,11 @@
+package com.jeanjnap.domain.entity
+
+import java.math.BigDecimal
+import java.util.Date
+
+data class Statement(
+ val title: String?,
+ val desc: String?,
+ val date: Date?,
+ val value: BigDecimal?
+)
diff --git a/domain/src/main/java/com/jeanjnap/domain/entity/UserAccount.kt b/domain/src/main/java/com/jeanjnap/domain/entity/UserAccount.kt
new file mode 100644
index 000000000..982257c88
--- /dev/null
+++ b/domain/src/main/java/com/jeanjnap/domain/entity/UserAccount.kt
@@ -0,0 +1,12 @@
+package com.jeanjnap.domain.entity
+
+import java.io.Serializable
+import java.math.BigDecimal
+
+data class UserAccount(
+ val userId: Long?,
+ val name: String?,
+ val bankAccount: String?,
+ val agency: String?,
+ val balance: BigDecimal?
+) : Serializable
diff --git a/domain/src/main/java/com/jeanjnap/domain/entity/error/ErrorRequestType.kt b/domain/src/main/java/com/jeanjnap/domain/entity/error/ErrorRequestType.kt
new file mode 100644
index 000000000..f6c3c64ac
--- /dev/null
+++ b/domain/src/main/java/com/jeanjnap/domain/entity/error/ErrorRequestType.kt
@@ -0,0 +1,18 @@
+package com.jeanjnap.domain.entity.error
+
+enum class ErrorRequestType(val code: Int) {
+ BAD_REQUEST(400),
+ UNAUTHORIZED(401),
+ FORBIDDEN(403),
+ NOT_FOUND(404),
+ TOKEN_EXPIRED(409),
+ INTERNAL_SERVER_ERROR(500),
+ SERVICE_UNAVAILABLE(503);
+
+ companion object {
+ fun convert(code: Int?): ErrorRequestType {
+ return enumValues().firstOrNull { it.code == code }
+ ?: INTERNAL_SERVER_ERROR
+ }
+ }
+}
diff --git a/domain/src/main/java/com/jeanjnap/domain/entity/error/RequestError.kt b/domain/src/main/java/com/jeanjnap/domain/entity/error/RequestError.kt
new file mode 100644
index 000000000..4fda8afec
--- /dev/null
+++ b/domain/src/main/java/com/jeanjnap/domain/entity/error/RequestError.kt
@@ -0,0 +1,6 @@
+package com.jeanjnap.domain.entity.error
+
+data class RequestError(
+ val status: ErrorRequestType? = null,
+ val errorMessage: String? = null
+)
diff --git a/domain/src/main/java/com/jeanjnap/domain/entity/success/SuccessRequestType.kt b/domain/src/main/java/com/jeanjnap/domain/entity/success/SuccessRequestType.kt
new file mode 100644
index 000000000..49cab2daa
--- /dev/null
+++ b/domain/src/main/java/com/jeanjnap/domain/entity/success/SuccessRequestType.kt
@@ -0,0 +1,5 @@
+package com.jeanjnap.domain.entity.success
+
+enum class SuccessRequestType(val code: Int) {
+ CREATED(204)
+}
diff --git a/domain/src/main/java/com/jeanjnap/domain/repository/BankRepository.kt b/domain/src/main/java/com/jeanjnap/domain/repository/BankRepository.kt
new file mode 100644
index 000000000..be0cd1c7f
--- /dev/null
+++ b/domain/src/main/java/com/jeanjnap/domain/repository/BankRepository.kt
@@ -0,0 +1,12 @@
+package com.jeanjnap.domain.repository
+
+import com.jeanjnap.domain.entity.Response
+import com.jeanjnap.domain.entity.Statement
+import com.jeanjnap.domain.entity.UserAccount
+
+interface BankRepository {
+ suspend fun login(username: String, password: String): Response
+ suspend fun getStatements(userId: Long?): Response>
+ fun saveEncryptedUser(user: String)
+ fun getUser(): String?
+}
diff --git a/domain/src/main/java/com/jeanjnap/domain/usecase/BankUseCase.kt b/domain/src/main/java/com/jeanjnap/domain/usecase/BankUseCase.kt
new file mode 100644
index 000000000..035ff3056
--- /dev/null
+++ b/domain/src/main/java/com/jeanjnap/domain/usecase/BankUseCase.kt
@@ -0,0 +1,11 @@
+package com.jeanjnap.domain.usecase
+
+import com.jeanjnap.domain.entity.Response
+import com.jeanjnap.domain.entity.Statement
+import com.jeanjnap.domain.entity.UserAccount
+
+interface BankUseCase {
+ suspend fun login(username: String, password: String): Response
+ suspend fun getStatements(userId: Long?): Response>
+ fun getUser(): String?
+}
\ No newline at end of file
diff --git a/domain/src/main/java/com/jeanjnap/domain/usecase/BankUseCaseImpl.kt b/domain/src/main/java/com/jeanjnap/domain/usecase/BankUseCaseImpl.kt
new file mode 100644
index 000000000..37a53ca0e
--- /dev/null
+++ b/domain/src/main/java/com/jeanjnap/domain/usecase/BankUseCaseImpl.kt
@@ -0,0 +1,31 @@
+package com.jeanjnap.domain.usecase
+
+import com.jeanjnap.domain.entity.Response
+import com.jeanjnap.domain.entity.Statement
+import com.jeanjnap.domain.entity.SuccessResponse
+import com.jeanjnap.domain.entity.UserAccount
+import com.jeanjnap.domain.repository.BankRepository
+import com.jeanjnap.infrastructure.crypto.RSACrypto
+
+class BankUseCaseImpl(
+ private val bankRepository: BankRepository,
+ private val rsaCrypto: RSACrypto
+): BankUseCase {
+ override suspend fun login(username: String, password: String): Response {
+ return bankRepository.login(username, password).also {
+ if (it is SuccessResponse) {
+ bankRepository.saveEncryptedUser(rsaCrypto.encrypt(username))
+ }
+ }
+ }
+
+ override suspend fun getStatements(userId: Long?): Response> {
+ return bankRepository.getStatements(userId)
+ }
+
+ override fun getUser(): String? {
+ return bankRepository.getUser()?.let {
+ rsaCrypto.decrypt(it)
+ }
+ }
+}
diff --git a/domain/src/main/java/com/jeanjnap/domain/util/extensions/BigDecimalExtensions.kt b/domain/src/main/java/com/jeanjnap/domain/util/extensions/BigDecimalExtensions.kt
new file mode 100644
index 000000000..d7e44af86
--- /dev/null
+++ b/domain/src/main/java/com/jeanjnap/domain/util/extensions/BigDecimalExtensions.kt
@@ -0,0 +1,22 @@
+package com.jeanjnap.domain.util.extensions
+
+import java.math.BigDecimal
+import java.text.DecimalFormat
+import java.text.DecimalFormatSymbols
+import java.text.NumberFormat
+import java.util.Locale
+
+private const val MONETARY_FORMAT = "#,##0"
+private const val DOT = "."
+private const val SEMI_COLON = ";"
+private const val ZERO_VALUE = 0
+private const val TWO_VALUE = 2
+
+fun BigDecimal.formatCurrency(useSymbol: Boolean = true, decimalPlaces: Int = TWO_VALUE): String {
+ val locale = Locale("pt", "BR")
+ val format = MONETARY_FORMAT + DOT + ZERO_VALUE.toString().repeat(decimalPlaces) + SEMI_COLON
+ val symbol = NumberFormat.getCurrencyInstance(locale).currency?.getSymbol(locale)
+ val pattern: String = if (useSymbol) "$symbol $format" else format
+ val df = DecimalFormat(pattern, DecimalFormatSymbols.getInstance(locale))
+ return df.format(this)
+}
\ No newline at end of file
diff --git a/domain/src/main/java/com/jeanjnap/domain/util/extensions/DateExtensions.kt b/domain/src/main/java/com/jeanjnap/domain/util/extensions/DateExtensions.kt
new file mode 100644
index 000000000..7a4b8f220
--- /dev/null
+++ b/domain/src/main/java/com/jeanjnap/domain/util/extensions/DateExtensions.kt
@@ -0,0 +1,12 @@
+package com.jeanjnap.domain.util.extensions
+
+import android.annotation.SuppressLint
+import java.text.SimpleDateFormat
+import java.util.Date
+
+private const val DATE_FORMAT = "dd/MM/yyyy"
+
+@SuppressLint("SimpleDateFormat")
+fun Date.formatToString(format: String = DATE_FORMAT): String? {
+ return SimpleDateFormat(format).format(this)
+}
diff --git a/domain/src/main/java/com/jeanjnap/domain/util/extensions/StringExtensions.kt b/domain/src/main/java/com/jeanjnap/domain/util/extensions/StringExtensions.kt
new file mode 100644
index 000000000..56bb5c520
--- /dev/null
+++ b/domain/src/main/java/com/jeanjnap/domain/util/extensions/StringExtensions.kt
@@ -0,0 +1,74 @@
+package com.jeanjnap.domain.util.extensions
+
+import android.annotation.SuppressLint
+import java.text.ParseException
+import java.text.SimpleDateFormat
+import java.util.Date
+
+private const val DEFAULT_DATE_FORMAT = "yyyy-MM-dd"
+private const val AGENCY_MASK = "##.######-#"
+private const val NOT_REPLACEABLE_CHAR = '#'
+private const val EMPTY_TEXT = ""
+private const val ZERO_VALUE = 0
+private const val ONE_VALUE = 1
+private const val EIGHT_VALUE = 8
+private const val NINE_VALUE = 9
+private const val TEN_VALUE = 10
+private const val ELEVEN_VALUE = 11
+
+@SuppressLint("SimpleDateFormat")
+fun String.formatStringAsDate(format: String = DEFAULT_DATE_FORMAT): Date? {
+ return try {
+ SimpleDateFormat(format).parse(this)
+ } catch (e: ParseException) {
+ null
+ }
+}
+
+fun String.formatAgency(): String {
+ var res = EMPTY_TEXT
+ var interator = ZERO_VALUE
+ AGENCY_MASK.forEachIndexed { _, currentChar ->
+ if (currentChar == NOT_REPLACEABLE_CHAR) {
+ try {
+ res += this[interator]
+ interator++
+ } catch (e: StringIndexOutOfBoundsException) {
+ return res
+ }
+ } else {
+ res += currentChar
+ }
+ }
+ return res
+}
+
+fun String.isCPF(): Boolean {
+ if (this.isEmpty()) return false
+
+ val numbers = arrayListOf()
+
+ this.filter { it.isDigit() }.forEach {
+ numbers.add(it.toString().toInt())
+ }
+
+ if (numbers.size != ELEVEN_VALUE) return false
+
+ (ZERO_VALUE..NINE_VALUE).forEach { n ->
+ val digits = arrayListOf()
+ (ZERO_VALUE..TEN_VALUE).forEach { _ -> digits.add(n) }
+ if (numbers == digits) return false
+ }
+
+ val dv1 =
+ ((ZERO_VALUE..EIGHT_VALUE).sumBy { (it + ONE_VALUE) * numbers[it] }).rem(ELEVEN_VALUE).let {
+ if (it >= TEN_VALUE) ZERO_VALUE else it
+ }
+
+ val dv2 = ((ZERO_VALUE..EIGHT_VALUE).sumBy { it * numbers[it] }
+ .let { (it + (dv1 * NINE_VALUE)).rem(ELEVEN_VALUE) }).let {
+ if (it >= TEN_VALUE) ZERO_VALUE else it
+ }
+
+ return numbers[NINE_VALUE] == dv1 && numbers[TEN_VALUE] == dv2
+}
diff --git a/domain/src/test/java/com/jeanjnap/domain/ExampleUnitTest.kt b/domain/src/test/java/com/jeanjnap/domain/ExampleUnitTest.kt
new file mode 100644
index 000000000..733324f23
--- /dev/null
+++ b/domain/src/test/java/com/jeanjnap/domain/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.jeanjnap.domain
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/domain/src/test/java/com/jeanjnap/domain/usecase/BankUseCaseTest.kt b/domain/src/test/java/com/jeanjnap/domain/usecase/BankUseCaseTest.kt
new file mode 100644
index 000000000..e5e8ffdd9
--- /dev/null
+++ b/domain/src/test/java/com/jeanjnap/domain/usecase/BankUseCaseTest.kt
@@ -0,0 +1,91 @@
+package com.jeanjnap.domain.usecase
+
+import com.jeanjnap.domain.entity.ErrorResponse
+import com.jeanjnap.domain.entity.SuccessResponse
+import com.jeanjnap.domain.repository.BankRepository
+import com.jeanjnap.infrastructure.crypto.RSACrypto
+import io.mockk.MockKAnnotations
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.ArgumentMatchers.anyString
+
+class BankUseCaseTest {
+
+ private lateinit var useCase: BankUseCase
+
+ @MockK
+ private lateinit var bankRepository: BankRepository
+
+ @MockK
+ private lateinit var rsaCrypto: RSACrypto
+
+ @Before
+ fun setup() {
+ MockKAnnotations.init(this)
+ useCase = BankUseCaseImpl(bankRepository, rsaCrypto)
+ }
+
+ @Test
+ fun login_withSuccessResponse_shouldCallsBankRepositoryAndSaveEncryptedUser() {
+ coEvery { bankRepository.login(any(), any()) } returns SuccessResponse(mockk())
+ every { rsaCrypto.encrypt(any()) } returns anyString()
+ every { bankRepository.saveEncryptedUser(any()) } returns Unit
+
+ runBlocking { useCase.login(anyString(), anyString()) }
+
+ coVerify { bankRepository.login(any(), any()) }
+ verify { bankRepository.saveEncryptedUser(any()) }
+ verify { rsaCrypto.encrypt(any()) }
+ }
+
+ @Test
+ fun login_withErrorResponse_shouldCallsBankRepositoryButNotSaveEncryptedUser() {
+ coEvery { bankRepository.login(any(), any()) } returns ErrorResponse(mockk())
+
+ runBlocking { useCase.login(anyString(), anyString()) }
+
+ coVerify { bankRepository.login(any(), any()) }
+ verify(inverse = true) { bankRepository.saveEncryptedUser(any()) }
+ verify(inverse = true) { rsaCrypto.encrypt(any()) }
+ }
+
+ @Test
+ fun getStatements_shouldCallsBankRepository() {
+ coEvery { bankRepository.getStatements(any()) } returns mockk()
+
+ runBlocking { useCase.getStatements(anyLong()) }
+
+ coVerify { useCase.getStatements(any()) }
+ }
+
+ @Test
+ fun getUser_withSavedUser_shouldDecryptUser() {
+ every { bankRepository.getUser() } returns anyString()
+ every { rsaCrypto.decrypt(any()) } returns anyString()
+
+ assertEquals(anyString(), useCase.getUser())
+
+ verify { bankRepository.getUser() }
+ verify { rsaCrypto.decrypt(any()) }
+ }
+
+ @Test
+ fun getUser_withoutSavedUser_shouldCallsRepositoryButNotDecryptUser() {
+ every { bankRepository.getUser() } returns null
+
+ assertNull(useCase.getUser())
+
+ verify { bankRepository.getUser() }
+ verify(inverse = true) { rsaCrypto.decrypt(any()) }
+ }
+}
\ No newline at end of file
diff --git a/domain/src/test/java/com/jeanjnap/domain/util/extensions/BigDecimalExtensionsTest.kt b/domain/src/test/java/com/jeanjnap/domain/util/extensions/BigDecimalExtensionsTest.kt
new file mode 100644
index 000000000..7ebd38f5d
--- /dev/null
+++ b/domain/src/test/java/com/jeanjnap/domain/util/extensions/BigDecimalExtensionsTest.kt
@@ -0,0 +1,15 @@
+package com.jeanjnap.domain.util.extensions
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import java.math.BigDecimal
+
+class BigDecimalExtensionsTest {
+
+ @Test
+ fun formatCurrency() {
+ assertEquals("R$ 10,00", BigDecimal.TEN.formatCurrency())
+ assertEquals("R$ 1.000,00", "1000".toBigDecimal().formatCurrency())
+ assertEquals("R$ 0,99", "0.99".toBigDecimal().formatCurrency())
+ }
+}
\ No newline at end of file
diff --git a/domain/src/test/java/com/jeanjnap/domain/util/extensions/DateExtensionsTest.kt b/domain/src/test/java/com/jeanjnap/domain/util/extensions/DateExtensionsTest.kt
new file mode 100644
index 000000000..2e34292a7
--- /dev/null
+++ b/domain/src/test/java/com/jeanjnap/domain/util/extensions/DateExtensionsTest.kt
@@ -0,0 +1,15 @@
+package com.jeanjnap.domain.util.extensions
+
+import org.junit.Assert
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyString
+import java.util.Date
+
+class DateExtensionsTest {
+
+ @Test
+ fun formatToString() {
+ Assert.assertEquals("17/01/2021", Date(1610852400000).formatToString())
+ Assert.assertNull(anyString().formatStringAsDate())
+ }
+}
\ No newline at end of file
diff --git a/domain/src/test/java/com/jeanjnap/domain/util/extensions/StringExtensionsTest.kt b/domain/src/test/java/com/jeanjnap/domain/util/extensions/StringExtensionsTest.kt
new file mode 100644
index 000000000..4a2011350
--- /dev/null
+++ b/domain/src/test/java/com/jeanjnap/domain/util/extensions/StringExtensionsTest.kt
@@ -0,0 +1,30 @@
+package com.jeanjnap.domain.util.extensions
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import java.util.Date
+
+class StringExtensionsTest {
+
+ @Test
+ fun formatStringAsDate() {
+ assertEquals(Date(1610852400000), "2021-01-17".formatStringAsDate())
+ assertNull("2021/01/17".formatStringAsDate())
+ }
+
+ @Test
+ fun formatAgency() {
+ assertEquals("01.111222-4", "011112224".formatAgency())
+ assertEquals("01.111222-", "01111222".formatAgency())
+ }
+
+ @Test
+ fun isCPF() {
+ assertTrue("123.456.789-09".isCPF())
+ assertTrue("12345678909".isCPF())
+ assertFalse("123".isCPF())
+ }
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 000000000..7a516bc55
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+org.gradle.parallel=true
+kapt.incremental.apt=true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..f6b961fd5
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..492dacf76
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sat Jan 16 10:42:38 BRT 2021
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
diff --git a/gradlew b/gradlew
new file mode 100644
index 000000000..cccdd3d51
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 000000000..f9553162f
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/infrastructure/.gitignore b/infrastructure/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/infrastructure/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/infrastructure/build.gradle b/infrastructure/build.gradle
new file mode 100644
index 000000000..f2ea7b9bf
--- /dev/null
+++ b/infrastructure/build.gradle
@@ -0,0 +1,30 @@
+plugins {
+ id 'com.android.library'
+ id 'kotlin-android'
+}
+
+android {
+ compileSdkVersion rootProject.ext.compile_sdk_version
+ buildToolsVersion rootProject.ext.build_tools_module_version
+
+ defaultConfig {
+ minSdkVersion rootProject.ext.min_sdk_version
+ targetSdkVersion rootProject.ext.target_sdk_version
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = rootProject.ext.jvm_target_version
+ }
+}
+
+dependencies {
+
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+ implementation "androidx.core:core-ktx:$core_ktx_version"
+
+ testImplementation "junit:junit:$junit_4_version"
+}
\ No newline at end of file
diff --git a/infrastructure/consumer-rules.pro b/infrastructure/consumer-rules.pro
new file mode 100644
index 000000000..e69de29bb
diff --git a/infrastructure/proguard-rules.pro b/infrastructure/proguard-rules.pro
new file mode 100644
index 000000000..481bb4348
--- /dev/null
+++ b/infrastructure/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/infrastructure/src/main/AndroidManifest.xml b/infrastructure/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..b2ddb8bb2
--- /dev/null
+++ b/infrastructure/src/main/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/infrastructure/src/main/java/com/jeanjnap/infrastructure/crypto/RSACrypto.kt b/infrastructure/src/main/java/com/jeanjnap/infrastructure/crypto/RSACrypto.kt
new file mode 100644
index 000000000..8b02892e5
--- /dev/null
+++ b/infrastructure/src/main/java/com/jeanjnap/infrastructure/crypto/RSACrypto.kt
@@ -0,0 +1,6 @@
+package com.jeanjnap.infrastructure.crypto
+
+interface RSACrypto {
+ fun encrypt(value: String): String
+ fun decrypt(value: String): String
+}
diff --git a/infrastructure/src/main/java/com/jeanjnap/infrastructure/crypto/RSACryptoImpl.kt b/infrastructure/src/main/java/com/jeanjnap/infrastructure/crypto/RSACryptoImpl.kt
new file mode 100644
index 000000000..682611218
--- /dev/null
+++ b/infrastructure/src/main/java/com/jeanjnap/infrastructure/crypto/RSACryptoImpl.kt
@@ -0,0 +1,86 @@
+package com.jeanjnap.infrastructure.crypto
+
+import android.content.Context
+import android.util.Base64
+import com.jeanjnap.infrastructure.crypto.key.RSAKey
+import java.io.ByteArrayOutputStream
+import javax.crypto.Cipher
+
+class RSACryptoImpl(
+ context: Context
+): RSACrypto {
+
+ private val rsaKey = RSAKey(context).apply {
+ initAndGenerateKeyPair()
+ }
+
+ private val encryptCipher: Cipher by lazy {
+ Cipher.getInstance(CIPHER_RSA_ENCRYPT_MODE).apply {
+ init(Cipher.ENCRYPT_MODE, rsaKey.getPrivateKey())
+ }
+ }
+
+ private val decryptCipher: Cipher by lazy {
+ Cipher.getInstance(CIPHER_RSA_ENCRYPT_MODE).apply {
+ init(Cipher.DECRYPT_MODE, rsaKey.getPublicKey())
+ }
+ }
+ override fun encrypt(value: String): String {
+ val originalMessage = value.toByteArray()
+ return if (originalMessage.size > CIPHER_BLOCK_SIZE) {
+ encryptLargeText(encryptCipher, originalMessage)
+ } else {
+ Base64.encodeToString(encryptCipher.doFinal(value.toByteArray()), Base64.DEFAULT)
+ }
+ }
+
+ override fun decrypt(value: String): String {
+ val encrypted = Base64.decode(value, Base64.DEFAULT)
+ return if (encrypted.size > CIPHER_BLOCK_SIZE)
+ decryptLargeText(decryptCipher, encrypted)
+ else {
+ String(decryptCipher.doFinal(Base64.decode(value, Base64.DEFAULT)))
+ }
+ }
+
+ private fun encryptLargeText(cipher: Cipher, message: ByteArray): String {
+ // k - 11 octets (k is the octet length of the RSA modulus) k -> KeySize/8
+ var limit: Int = (RSAKey.RSA_KEY_SIZE / EIGHT_VALUE) - ELEVEN_VALUE
+ var position = ZERO_VALUE
+
+ val byteArrayOutputStream = ByteArrayOutputStream()
+ while (position < message.size) {
+ if (message.size - position < limit) limit = message.size - position
+ val res = cipher.doFinal(message, position, limit)
+ byteArrayOutputStream.write(res)
+ position += limit
+ }
+
+ return Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.DEFAULT)
+ }
+
+ private fun decryptLargeText(cipher: Cipher, encryptedMessage: ByteArray): String {
+ var limit = RSAKey.RSA_KEY_SIZE / EIGHT_VALUE
+ var position = ZERO_VALUE
+
+ val byteArrayOutputStream = ByteArrayOutputStream()
+ while (position < encryptedMessage.size) {
+ if (encryptedMessage.size - position < limit) {
+ limit = encryptedMessage.size - position
+ }
+ val result = cipher.doFinal(encryptedMessage, position, limit)
+ byteArrayOutputStream.write(result)
+ position += limit
+ }
+
+ return String(byteArrayOutputStream.toByteArray())
+ }
+
+ companion object {
+ private const val ZERO_VALUE = 0
+ private const val EIGHT_VALUE = 8
+ private const val ELEVEN_VALUE = 11
+ const val CIPHER_RSA_ENCRYPT_MODE = "RSA/ECB/PKCS1Padding"
+ const val CIPHER_BLOCK_SIZE = 256
+ }
+}
\ No newline at end of file
diff --git a/infrastructure/src/main/java/com/jeanjnap/infrastructure/crypto/key/RSAKey.kt b/infrastructure/src/main/java/com/jeanjnap/infrastructure/crypto/key/RSAKey.kt
new file mode 100644
index 000000000..be84e16d9
--- /dev/null
+++ b/infrastructure/src/main/java/com/jeanjnap/infrastructure/crypto/key/RSAKey.kt
@@ -0,0 +1,113 @@
+@file:Suppress("DEPRECATION")
+
+package com.jeanjnap.infrastructure.crypto.key
+
+import android.content.Context
+import android.os.Build
+import android.security.KeyPairGeneratorSpec
+import android.security.keystore.KeyGenParameterSpec
+import android.security.keystore.KeyProperties
+import androidx.annotation.RequiresApi
+import java.math.BigInteger
+import java.security.KeyPairGenerator
+import java.security.KeyStore
+import java.security.KeyStore.PrivateKeyEntry
+import java.security.PrivateKey
+import java.security.PublicKey
+import java.util.Calendar
+import javax.security.auth.x500.X500Principal
+
+class RSAKey(
+ private val context: Context
+) {
+ private val keyStore: KeyStore? by lazy {
+ KeyStore.getInstance(ANDROID_KEY_STORE).apply {
+ load(null)
+ }
+ }
+
+ fun initAndGenerateKeyPair() {
+ if (keyStore?.containsAlias(ALIAS) == true) {
+ // We will not ask to generate every single time
+ return
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ generatePairKeyAfterAndroidM()
+ } else {
+ generatePairKeyBeforeAndroidM()
+ }
+ }
+
+ fun getPrivateKey(): PrivateKey? {
+ return if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) {
+ keyStore?.getKey(ALIAS, null) as PrivateKey
+ } else {
+ (keyStore?.getEntry(ALIAS, null) as? PrivateKeyEntry)?.privateKey
+ }
+ }
+
+ fun getPublicKey(): PublicKey? {
+ val privateKeyEntry = (keyStore?.getEntry(ALIAS, null) as? PrivateKeyEntry)
+ return privateKeyEntry?.certificate?.publicKey
+ }
+
+ @Suppress("DEPRECATION")
+ private fun generatePairKeyBeforeAndroidM() {
+ val specBuilder = KeyPairGeneratorSpec.Builder(context).run {
+ setKeySize(RSA_KEY_SIZE)
+ setSerialNumber(BigInteger.TEN)
+ setSubject(X500Principal("CN=$ALIAS"))
+ setAlias(ALIAS)
+ setStartDate(Calendar.getInstance().apply { add(Calendar.DAY_OF_YEAR, -1) }.time)
+ setEndDate(Calendar.getInstance().apply { add(Calendar.YEAR, 2) }.time)
+ build()
+ }
+
+ KeyPairGenerator.getInstance(
+ RSA_ENCRYPT_MODE,
+ ANDROID_KEY_STORE
+ ).apply {
+ initialize(specBuilder)
+ generateKeyPair()
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun generatePairKeyAfterAndroidM() {
+ val kpg: KeyPairGenerator = KeyPairGenerator.getInstance(
+ KeyProperties.KEY_ALGORITHM_RSA,
+ ANDROID_KEY_STORE
+ )
+ val parameterSpec: KeyGenParameterSpec = KeyGenParameterSpec.Builder(
+ ALIAS,
+ KeyProperties.PURPOSE_ENCRYPT or
+ KeyProperties.PURPOSE_DECRYPT or
+ KeyProperties.PURPOSE_SIGN or
+ KeyProperties.PURPOSE_VERIFY
+ ).run {
+ setDigests(KeyProperties.DIGEST_MD5, KeyProperties.DIGEST_NONE, KeyProperties.DIGEST_SHA1, KeyProperties.DIGEST_SHA224, KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA384, KeyProperties.DIGEST_SHA512)
+ setKeySize(RSA_KEY_SIZE)
+ setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
+ setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
+ setCertificateSerialNumber(BigInteger.TEN)
+ setCertificateSubject(X500Principal("CN=$ALIAS"))
+ setKeyValidityStart(Calendar.getInstance().apply { add(Calendar.DAY_OF_YEAR, -1) }.time)
+ setKeyValidityEnd(Calendar.getInstance().apply { add(Calendar.YEAR, 2) }.time)
+ build()
+ }
+
+ kpg.apply {
+ initialize(parameterSpec)
+ generateKeyPair()
+ }
+ }
+
+ companion object {
+ private const val RSA_ENCRYPT_MODE = "RSA"
+ private const val ANDROID_KEY_STORE = "AndroidKeyStore"
+ private const val ALIAS = "alias"
+
+ const val RSA_KEY_SIZE = 2048
+ }
+}
\ No newline at end of file
diff --git a/infrastructure/src/main/java/com/jeanjnap/infrastructure/network/Network.kt b/infrastructure/src/main/java/com/jeanjnap/infrastructure/network/Network.kt
new file mode 100644
index 000000000..cd5b73620
--- /dev/null
+++ b/infrastructure/src/main/java/com/jeanjnap/infrastructure/network/Network.kt
@@ -0,0 +1,5 @@
+package com.jeanjnap.infrastructure.network
+
+interface Network {
+ fun hasActiveInternetConnection(): Boolean
+}
diff --git a/infrastructure/src/main/java/com/jeanjnap/infrastructure/network/NetworkImpl.kt b/infrastructure/src/main/java/com/jeanjnap/infrastructure/network/NetworkImpl.kt
new file mode 100644
index 000000000..4f113adf0
--- /dev/null
+++ b/infrastructure/src/main/java/com/jeanjnap/infrastructure/network/NetworkImpl.kt
@@ -0,0 +1,21 @@
+package com.jeanjnap.infrastructure.network
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.os.Build
+
+class NetworkImpl(
+ private val context: Context
+) : Network {
+
+ override fun hasActiveInternetConnection(): Boolean {
+ return (context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager)?.let {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ it.activeNetwork != null
+ } else {
+ @Suppress("DEPRECATION")
+ it.activeNetworkInfo != null
+ }
+ } ?: false
+ }
+}
diff --git a/infrastructure/src/test/java/com/jeanjnap/infrastructure/ExampleUnitTest.kt b/infrastructure/src/test/java/com/jeanjnap/infrastructure/ExampleUnitTest.kt
new file mode 100644
index 000000000..2e7f5d2fc
--- /dev/null
+++ b/infrastructure/src/test/java/com/jeanjnap/infrastructure/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.jeanjnap.infrastructure
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 000000000..dbe3283e5
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,5 @@
+include ':infrastructure'
+include ':domain'
+include ':data'
+include ':app'
+rootProject.name = "Bank App"
\ No newline at end of file
diff --git a/telas.png b/telas.png
deleted file mode 100644
index 783374440..000000000
Binary files a/telas.png and /dev/null differ