From 5b412a21fc55aef98495cecae8a941698fd2dc4c Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 11 Feb 2024 20:50:45 +0530 Subject: [PATCH] refactor: migrate login from xml to compose and multi module with clean arch --- core/common/build.gradle.kts | 6 +- core/common/src/main/AndroidManifest.xml | 2 + .../com/mifos/core/common/utils/BaseUrl.kt | 16 ++ .../com/mifos/core/common/utils/Network.kt | 33 +++ .../com/mifos/core/common/utils/Resource.kt | 14 ++ core/data/.gitignore | 1 + core/data/build.gradle.kts | 43 ++++ core/data/consumer-rules.pro | 0 core/data/proguard-rules.pro | 21 ++ .../core/data/ExampleInstrumentedTest.kt | 24 ++ core/data/src/main/AndroidManifest.xml | 4 + .../java/com/mifos/core/data/model/Role.kt | 16 ++ .../java/com/mifos/core/data/model/User.kt | 23 ++ .../com/mifos/core/data/ExampleUnitTest.kt | 17 ++ core/datastore/build.gradle.kts | 16 ++ .../com/mifos/core/datastore/PrefManager.kt | 54 +++++ .../component/MifosAndroidClientIcon.kt | 6 +- .../component/MifosEditTextField.kt | 19 +- core/network/.gitignore | 1 + core/network/build.gradle.kts | 61 +++++ core/network/consumer-rules.pro | 0 core/network/proguard-rules.pro | 21 ++ .../core/network/ExampleInstrumentedTest.kt | 24 ++ core/network/src/main/AndroidManifest.xml | 4 + .../network/datamanger/DataManagerAuth.kt | 28 +++ .../network/di/BaseApiManagerQualifier.kt | 7 + .../mifos/core/network/di/NetworkModule.kt | 31 +++ .../com/mifos/core/network/ExampleUnitTest.kt | 17 ++ feature/auth/build.gradle.kts | 19 +- .../data/repository_imp/LoginRepositoryImp.kt | 19 ++ .../auth/login/di/ApplicationModule.kt | 19 ++ .../feature/auth/login/di/UseCaseModule.kt | 28 +++ .../login/domain/model/ValidationResult.kt | 12 + .../domain/repository/LoginRepository.kt | 14 ++ .../login/domain/use_case/LoginUseCase.kt | 54 +++++ .../use_case/PasswordValidationUseCase.kt | 28 +++ .../use_case/UsernameValidationUseCase.kt | 27 +++ .../login/{ => presentation}/LoginScreen.kt | 111 +++++++-- .../auth/login/presentation/LoginUiState.kt | 22 ++ .../auth/login/presentation/LoginViewModel.kt | 133 +++++++++++ feature/auth/src/main/res/values/strings.xml | 5 + mifosng-android/build.gradle.kts | 4 + .../activity/login/LoginActivity.kt | 217 ++---------------- settings.gradle.kts | 2 + 44 files changed, 984 insertions(+), 239 deletions(-) create mode 100644 core/common/src/main/java/com/mifos/core/common/utils/BaseUrl.kt create mode 100644 core/common/src/main/java/com/mifos/core/common/utils/Network.kt create mode 100644 core/common/src/main/java/com/mifos/core/common/utils/Resource.kt create mode 100644 core/data/.gitignore create mode 100644 core/data/build.gradle.kts create mode 100644 core/data/consumer-rules.pro create mode 100644 core/data/proguard-rules.pro create mode 100644 core/data/src/androidTest/java/com/mifos/core/data/ExampleInstrumentedTest.kt create mode 100644 core/data/src/main/AndroidManifest.xml create mode 100644 core/data/src/main/java/com/mifos/core/data/model/Role.kt create mode 100644 core/data/src/main/java/com/mifos/core/data/model/User.kt create mode 100644 core/data/src/test/java/com/mifos/core/data/ExampleUnitTest.kt create mode 100644 core/datastore/src/main/java/com/mifos/core/datastore/PrefManager.kt create mode 100644 core/network/.gitignore create mode 100644 core/network/build.gradle.kts create mode 100644 core/network/consumer-rules.pro create mode 100644 core/network/proguard-rules.pro create mode 100644 core/network/src/androidTest/java/com/mifos/core/network/ExampleInstrumentedTest.kt create mode 100644 core/network/src/main/AndroidManifest.xml create mode 100644 core/network/src/main/java/com/mifos/core/network/datamanger/DataManagerAuth.kt create mode 100644 core/network/src/main/java/com/mifos/core/network/di/BaseApiManagerQualifier.kt create mode 100644 core/network/src/main/java/com/mifos/core/network/di/NetworkModule.kt create mode 100644 core/network/src/test/java/com/mifos/core/network/ExampleUnitTest.kt create mode 100644 feature/auth/src/main/java/com/mifos/feature/auth/login/data/repository_imp/LoginRepositoryImp.kt create mode 100644 feature/auth/src/main/java/com/mifos/feature/auth/login/di/ApplicationModule.kt create mode 100644 feature/auth/src/main/java/com/mifos/feature/auth/login/di/UseCaseModule.kt create mode 100644 feature/auth/src/main/java/com/mifos/feature/auth/login/domain/model/ValidationResult.kt create mode 100644 feature/auth/src/main/java/com/mifos/feature/auth/login/domain/repository/LoginRepository.kt create mode 100644 feature/auth/src/main/java/com/mifos/feature/auth/login/domain/use_case/LoginUseCase.kt create mode 100644 feature/auth/src/main/java/com/mifos/feature/auth/login/domain/use_case/PasswordValidationUseCase.kt create mode 100644 feature/auth/src/main/java/com/mifos/feature/auth/login/domain/use_case/UsernameValidationUseCase.kt rename feature/auth/src/main/java/com/mifos/feature/auth/login/{ => presentation}/LoginScreen.kt (60%) create mode 100644 feature/auth/src/main/java/com/mifos/feature/auth/login/presentation/LoginUiState.kt create mode 100644 feature/auth/src/main/java/com/mifos/feature/auth/login/presentation/LoginViewModel.kt diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 12ad40ba1a9..69f2a6e84a5 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -24,11 +24,11 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "17" } } diff --git a/core/common/src/main/AndroidManifest.xml b/core/common/src/main/AndroidManifest.xml index a5918e68abc..cbb96c15613 100644 --- a/core/common/src/main/AndroidManifest.xml +++ b/core/common/src/main/AndroidManifest.xml @@ -1,4 +1,6 @@ + + \ No newline at end of file diff --git a/core/common/src/main/java/com/mifos/core/common/utils/BaseUrl.kt b/core/common/src/main/java/com/mifos/core/common/utils/BaseUrl.kt new file mode 100644 index 00000000000..669263eb3d5 --- /dev/null +++ b/core/common/src/main/java/com/mifos/core/common/utils/BaseUrl.kt @@ -0,0 +1,16 @@ +package com.mifos.core.common.utils + +object BaseUrl { + + // "/" in the last of the base url always + + const val PROTOCOL_HTTPS = "https://" + + const val API_ENDPOINT = "gsoc.mifos.community" + + const val API_PATH = "/fineract-provider/api/v1/" + + const val PORT = "80" + + const val TENANT = "default" +} \ No newline at end of file diff --git a/core/common/src/main/java/com/mifos/core/common/utils/Network.kt b/core/common/src/main/java/com/mifos/core/common/utils/Network.kt new file mode 100644 index 00000000000..e88dd2cb796 --- /dev/null +++ b/core/common/src/main/java/com/mifos/core/common/utils/Network.kt @@ -0,0 +1,33 @@ +/* + * This project is licensed under the open source MPL V2. + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.common.utils + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities + +/** + * Created by Aditya Gupta on 11/02/24. + */ + +object Network { + + fun isOnline(context: Context): Boolean { + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val capabilities = + connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + if (capabilities != null) { + if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + return true + } else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + return true + } else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) { + return true + } + } + return false + } +} \ No newline at end of file diff --git a/core/common/src/main/java/com/mifos/core/common/utils/Resource.kt b/core/common/src/main/java/com/mifos/core/common/utils/Resource.kt new file mode 100644 index 00000000000..0e63c6d1111 --- /dev/null +++ b/core/common/src/main/java/com/mifos/core/common/utils/Resource.kt @@ -0,0 +1,14 @@ +package com.mifos.core.common.utils + +/** + * Created by Aditya Gupta on 11/02/24. + */ + +sealed class Resource(val data: T? = null, val message: String? = null) { + + class Success(data: T?) : Resource(data) + + class Error(message: String, data: T? = null) : Resource(data, message) + + class Loading(data: T? = null) : Resource(data) +} \ No newline at end of file diff --git a/core/data/.gitignore b/core/data/.gitignore new file mode 100644 index 00000000000..42afabfd2ab --- /dev/null +++ b/core/data/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts new file mode 100644 index 00000000000..01c85df162b --- /dev/null +++ b/core/data/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "com.mifos.core.data" + compileSdk = 34 + + defaultConfig { + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } +} + +dependencies { + + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.11.0") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") +} \ No newline at end of file diff --git a/core/data/consumer-rules.pro b/core/data/consumer-rules.pro new file mode 100644 index 00000000000..e69de29bb2d diff --git a/core/data/proguard-rules.pro b/core/data/proguard-rules.pro new file mode 100644 index 00000000000..481bb434814 --- /dev/null +++ b/core/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/core/data/src/androidTest/java/com/mifos/core/data/ExampleInstrumentedTest.kt b/core/data/src/androidTest/java/com/mifos/core/data/ExampleInstrumentedTest.kt new file mode 100644 index 00000000000..fe519f7e834 --- /dev/null +++ b/core/data/src/androidTest/java/com/mifos/core/data/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.mifos.core.data + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.mifos.core.data.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/core/data/src/main/AndroidManifest.xml b/core/data/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..a5918e68abc --- /dev/null +++ b/core/data/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/core/data/src/main/java/com/mifos/core/data/model/Role.kt b/core/data/src/main/java/com/mifos/core/data/model/Role.kt new file mode 100644 index 00000000000..20e21a38432 --- /dev/null +++ b/core/data/src/main/java/com/mifos/core/data/model/Role.kt @@ -0,0 +1,16 @@ +/* + * This project is licensed under the open source MPL V2. + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.data.model + +/** + * Created by ishankhanna on 09/02/14. + */ +data class Role( + var id: Int = 0, + + var name: String? = null, + + var description: String? = null +) \ No newline at end of file diff --git a/core/data/src/main/java/com/mifos/core/data/model/User.kt b/core/data/src/main/java/com/mifos/core/data/model/User.kt new file mode 100644 index 00000000000..d1568b880c5 --- /dev/null +++ b/core/data/src/main/java/com/mifos/core/data/model/User.kt @@ -0,0 +1,23 @@ +/* + * This project is licensed under the open source MPL V2. + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.data.model + +class User { + var username: String? = null + + var userId = 0 + + var base64EncodedAuthenticationKey: String? = null + + var isAuthenticated = false + + var officeId = 0 + + var officeName: String? = null + + var roles: List = ArrayList() + + var permissions: List = ArrayList() +} \ No newline at end of file diff --git a/core/data/src/test/java/com/mifos/core/data/ExampleUnitTest.kt b/core/data/src/test/java/com/mifos/core/data/ExampleUnitTest.kt new file mode 100644 index 00000000000..bd60383b85b --- /dev/null +++ b/core/data/src/test/java/com/mifos/core/data/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.mifos.core.data + +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/core/datastore/build.gradle.kts b/core/datastore/build.gradle.kts index 663e7ba3d47..810535c77ee 100644 --- a/core/datastore/build.gradle.kts +++ b/core/datastore/build.gradle.kts @@ -1,6 +1,8 @@ plugins { id("com.android.library") id("org.jetbrains.kotlin.android") + id("kotlin-kapt") + id("com.google.dagger.hilt.android") } android { @@ -34,10 +36,24 @@ android { dependencies { + implementation(project(":core:data")) + implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.appcompat:appcompat:1.6.1") implementation("com.google.android.material:material:1.11.0") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") + + // Hilt dependency + implementation("com.google.dagger:hilt-android:2.50") + kapt("com.google.dagger:hilt-android-compiler:2.50") + + + // fineract sdk dependencies + implementation("com.github.openMF:mifos-android-sdk-arch:1.06") + + // sdk client + implementation("com.github.openMF:fineract-client:2.0.3") } \ No newline at end of file diff --git a/core/datastore/src/main/java/com/mifos/core/datastore/PrefManager.kt b/core/datastore/src/main/java/com/mifos/core/datastore/PrefManager.kt new file mode 100644 index 00000000000..59219b6463e --- /dev/null +++ b/core/datastore/src/main/java/com/mifos/core/datastore/PrefManager.kt @@ -0,0 +1,54 @@ +package com.mifos.core.datastore + +import android.content.Context +import android.content.SharedPreferences +import android.preference.PreferenceManager +import com.mifos.core.data.model.User +import dagger.hilt.android.qualifiers.ApplicationContext +import org.apache.fineract.client.models.PostAuthenticationResponse +import org.mifos.core.sharedpreference.Key +import org.mifos.core.sharedpreference.UserPreferences +import javax.inject.Inject + +/** + * Created by Aditya Gupta on 19/08/23. + */ + +class PrefManager @Inject constructor(@ApplicationContext context: Context) : + UserPreferences() { + + private val USER_DETAILS = "user_details" + private val AUTH_USERNAME = "auth_username" + private val AUTH_PASSWORD = "auth_password" + + override val preference: SharedPreferences = + PreferenceManager.getDefaultSharedPreferences(context) + + override fun getUser(): User { + return get(Key.Custom(USER_DETAILS)) + } + + override fun saveUser(user: User) { + put(Key.Custom(USER_DETAILS), user) + } + + // Created this to store userDetails + fun savePostAuthenticationResponse(user: PostAuthenticationResponse) { + put(Key.Custom(USER_DETAILS), user) + } + + fun setPermissionDeniedStatus(permissionDeniedStatus: String, status: Boolean) { + put(Key.Custom(permissionDeniedStatus), status) + } + + fun getPermissionDeniedStatus(permissionDeniedStatus: String): Boolean { + return get(Key.Custom(permissionDeniedStatus), true) + } + + var usernamePassword: Pair + get() = Pair(get(Key.Custom(AUTH_USERNAME)), get(Key.Custom(AUTH_PASSWORD))) + set(value) { + put(Key.Custom(AUTH_USERNAME), value.first) + put(Key.Custom(AUTH_PASSWORD), value.second) + } +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosAndroidClientIcon.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosAndroidClientIcon.kt index 548d36170e6..96889d9ee25 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosAndroidClientIcon.kt +++ b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosAndroidClientIcon.kt @@ -1,12 +1,8 @@ package com.mifos.core.designsystem.component import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp @@ -18,6 +14,6 @@ fun MifosAndroidClientIcon(id: Int) { painter = painterResource(id = id), contentDescription = null, modifier = Modifier - .size(200.dp,100.dp) + .size(200.dp, 100.dp) ) } \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosEditTextField.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosEditTextField.kt index af5ef0633a7..74c2ca1aa06 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosEditTextField.kt +++ b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosEditTextField.kt @@ -35,8 +35,7 @@ fun MifosOutlinedTextField( label: Int, visualTransformation: VisualTransformation = VisualTransformation.None, trailingIcon: @Composable (() -> Unit)? = null, - error: Boolean = false, - supportingText: String? + error: Int? ) { OutlinedTextField( @@ -66,16 +65,14 @@ fun MifosOutlinedTextField( }, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), visualTransformation = visualTransformation, - isError = error, + isError = error != null, supportingText = { - if (error) { - if (supportingText != null) { - Text( - modifier = Modifier.fillMaxWidth(), - text = supportingText, - color = MaterialTheme.colorScheme.error - ) - } + if (error != null) { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = error), + color = MaterialTheme.colorScheme.error + ) } } ) diff --git a/core/network/.gitignore b/core/network/.gitignore new file mode 100644 index 00000000000..42afabfd2ab --- /dev/null +++ b/core/network/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts new file mode 100644 index 00000000000..758f01c526b --- /dev/null +++ b/core/network/build.gradle.kts @@ -0,0 +1,61 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("kotlin-kapt") + id("com.google.dagger.hilt.android") +} + +android { + namespace = "com.mifos.core.network" + compileSdk = 34 + + defaultConfig { + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } +} + +dependencies { + + implementation(project(":core:datastore")) + + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.11.0") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + + //rxjava dependencies + implementation("io.reactivex:rxandroid:1.1.0") + implementation("io.reactivex:rxjava:1.3.8") + + // Hilt dependency + implementation("com.google.dagger:hilt-android:2.50") + kapt("com.google.dagger:hilt-android-compiler:2.50") + + // fineract sdk dependencies + implementation("com.github.openMF:mifos-android-sdk-arch:1.06") + + // sdk client + implementation("com.github.openMF:fineract-client:2.0.3") +} \ No newline at end of file diff --git a/core/network/consumer-rules.pro b/core/network/consumer-rules.pro new file mode 100644 index 00000000000..e69de29bb2d diff --git a/core/network/proguard-rules.pro b/core/network/proguard-rules.pro new file mode 100644 index 00000000000..481bb434814 --- /dev/null +++ b/core/network/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/core/network/src/androidTest/java/com/mifos/core/network/ExampleInstrumentedTest.kt b/core/network/src/androidTest/java/com/mifos/core/network/ExampleInstrumentedTest.kt new file mode 100644 index 00000000000..4740686abba --- /dev/null +++ b/core/network/src/androidTest/java/com/mifos/core/network/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.mifos.core.network + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.mifos.core.network.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/core/network/src/main/AndroidManifest.xml b/core/network/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..a5918e68abc --- /dev/null +++ b/core/network/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/core/network/src/main/java/com/mifos/core/network/datamanger/DataManagerAuth.kt b/core/network/src/main/java/com/mifos/core/network/datamanger/DataManagerAuth.kt new file mode 100644 index 00000000000..0a1ca8a8696 --- /dev/null +++ b/core/network/src/main/java/com/mifos/core/network/datamanger/DataManagerAuth.kt @@ -0,0 +1,28 @@ +package com.mifos.core.network.datamanger + +import com.mifos.core.network.di.BaseApiManagerQualifier +import org.apache.fineract.client.models.PostAuthenticationRequest +import org.apache.fineract.client.models.PostAuthenticationResponse +import org.mifos.core.apimanager.BaseApiManager +import rx.Observable +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Created by Rajan Maurya on 19/02/17. + */ +@Singleton +class DataManagerAuth @Inject constructor(@BaseApiManagerQualifier private val baseApiManager: BaseApiManager) { + /** + * @param username Username + * @param password Password + * @return Basic OAuth + */ + fun login(username: String, password: String): Observable { + val body = PostAuthenticationRequest().apply { + this.username = username + this.password = password + } + return baseApiManager.getAuthApi().authenticate(body, true) + } +} \ No newline at end of file diff --git a/core/network/src/main/java/com/mifos/core/network/di/BaseApiManagerQualifier.kt b/core/network/src/main/java/com/mifos/core/network/di/BaseApiManagerQualifier.kt new file mode 100644 index 00000000000..2326c0bf2b7 --- /dev/null +++ b/core/network/src/main/java/com/mifos/core/network/di/BaseApiManagerQualifier.kt @@ -0,0 +1,7 @@ +package com.mifos.core.network.di + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class BaseApiManagerQualifier \ No newline at end of file diff --git a/core/network/src/main/java/com/mifos/core/network/di/NetworkModule.kt b/core/network/src/main/java/com/mifos/core/network/di/NetworkModule.kt new file mode 100644 index 00000000000..1e6d27168f0 --- /dev/null +++ b/core/network/src/main/java/com/mifos/core/network/di/NetworkModule.kt @@ -0,0 +1,31 @@ +package com.mifos.core.network.di + +import com.mifos.core.datastore.PrefManager +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.mifos.core.apimanager.BaseApiManager +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + + @Provides + @Singleton + @BaseApiManagerQualifier + fun provideSdkBaseApiManager(prefManager: PrefManager): BaseApiManager { + val usernamePassword: Pair = prefManager.usernamePassword + val baseManager = BaseApiManager.getInstance() + baseManager.createService( + usernamePassword.first, + usernamePassword.second, + prefManager.getInstanceUrl(), + prefManager.getTenant(), + false + ) + return baseManager + } + +} \ No newline at end of file diff --git a/core/network/src/test/java/com/mifos/core/network/ExampleUnitTest.kt b/core/network/src/test/java/com/mifos/core/network/ExampleUnitTest.kt new file mode 100644 index 00000000000..dc3064e936e --- /dev/null +++ b/core/network/src/test/java/com/mifos/core/network/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.mifos.core.network + +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/feature/auth/build.gradle.kts b/feature/auth/build.gradle.kts index f523933e8a4..473f90ae36c 100644 --- a/feature/auth/build.gradle.kts +++ b/feature/auth/build.gradle.kts @@ -47,8 +47,11 @@ android { dependencies { -// implementation(project(":mifosng-android")) implementation(project(":core:designsystem")) + implementation(project(":core:network")) + implementation(project(":core:datastore")) + implementation(project(":core:common")) + implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.appcompat:appcompat:1.6.1") @@ -61,6 +64,10 @@ dependencies { implementation("com.google.dagger:hilt-android:2.50") kapt("com.google.dagger:hilt-android-compiler:2.50") + //rxjava dependencies + implementation("io.reactivex:rxandroid:1.1.0") + implementation("io.reactivex:rxjava:1.3.8") + // Jetpack Compose implementation("androidx.compose.material:material:1.6.0") implementation("androidx.compose.compiler:compiler:1.5.8") @@ -70,4 +77,14 @@ dependencies { implementation("androidx.compose.material3:material3:1.1.2") implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") implementation("androidx.compose.material:material-icons-extended:1.6.1") + + // ViewModel utilities for Compose + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") + implementation("androidx.hilt:hilt-navigation-compose:1.1.0") + + // fineract sdk dependencies + implementation("com.github.openMF:mifos-android-sdk-arch:1.06") + + // sdk client + implementation("com.github.openMF:fineract-client:2.0.3") } \ No newline at end of file diff --git a/feature/auth/src/main/java/com/mifos/feature/auth/login/data/repository_imp/LoginRepositoryImp.kt b/feature/auth/src/main/java/com/mifos/feature/auth/login/data/repository_imp/LoginRepositoryImp.kt new file mode 100644 index 00000000000..d864d13e39e --- /dev/null +++ b/feature/auth/src/main/java/com/mifos/feature/auth/login/data/repository_imp/LoginRepositoryImp.kt @@ -0,0 +1,19 @@ +package com.mifos.feature.auth.login.data.repository_imp + +import com.mifos.core.network.datamanger.DataManagerAuth +import com.mifos.feature.auth.login.domain.repository.LoginRepository +import org.apache.fineract.client.models.PostAuthenticationResponse +import rx.Observable +import javax.inject.Inject + +/** + * Created by Aditya Gupta on 06/08/23. + */ + +class LoginRepositoryImp @Inject constructor(private val dataManagerAuth: DataManagerAuth) : + LoginRepository { + + override fun login(username: String, password: String): Observable { + return dataManagerAuth.login(username, password) + } +} \ No newline at end of file diff --git a/feature/auth/src/main/java/com/mifos/feature/auth/login/di/ApplicationModule.kt b/feature/auth/src/main/java/com/mifos/feature/auth/login/di/ApplicationModule.kt new file mode 100644 index 00000000000..68ae7f551b8 --- /dev/null +++ b/feature/auth/src/main/java/com/mifos/feature/auth/login/di/ApplicationModule.kt @@ -0,0 +1,19 @@ +package com.mifos.feature.auth.login.di + +import com.mifos.core.network.datamanger.DataManagerAuth +import com.mifos.feature.auth.login.data.repository_imp.LoginRepositoryImp +import com.mifos.feature.auth.login.domain.repository.LoginRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@InstallIn(SingletonComponent::class) +@Module +object ApplicationModule { + + @Provides + fun providesLoginRepository(dataManagerAuth: DataManagerAuth): LoginRepository = + LoginRepositoryImp(dataManagerAuth) + +} \ No newline at end of file diff --git a/feature/auth/src/main/java/com/mifos/feature/auth/login/di/UseCaseModule.kt b/feature/auth/src/main/java/com/mifos/feature/auth/login/di/UseCaseModule.kt new file mode 100644 index 00000000000..223f629992e --- /dev/null +++ b/feature/auth/src/main/java/com/mifos/feature/auth/login/di/UseCaseModule.kt @@ -0,0 +1,28 @@ +package com.mifos.feature.auth.login.di + +import com.mifos.feature.auth.login.domain.repository.LoginRepository +import com.mifos.feature.auth.login.domain.use_case.LoginUseCase +import com.mifos.feature.auth.login.domain.use_case.PasswordValidationUseCase +import com.mifos.feature.auth.login.domain.use_case.UsernameValidationUseCase +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +object UseCaseModule { + + @Provides + fun provideUsernameValidationUseCase(): UsernameValidationUseCase = + UsernameValidationUseCase() + + @Provides + fun providePasswordValidationUseCase(): PasswordValidationUseCase = + PasswordValidationUseCase() + + @Provides + fun provideLoginUseCase(loginRepository: LoginRepository): LoginUseCase = + LoginUseCase(loginRepository) + +} \ No newline at end of file diff --git a/feature/auth/src/main/java/com/mifos/feature/auth/login/domain/model/ValidationResult.kt b/feature/auth/src/main/java/com/mifos/feature/auth/login/domain/model/ValidationResult.kt new file mode 100644 index 00000000000..933cd1801be --- /dev/null +++ b/feature/auth/src/main/java/com/mifos/feature/auth/login/domain/model/ValidationResult.kt @@ -0,0 +1,12 @@ +package com.mifos.feature.auth.login.domain.model + +/** + * Created by Aditya Gupta on 11/02/24. + */ + +data class ValidationResult( + + val success: Boolean, + + val message: Int? = null +) \ No newline at end of file diff --git a/feature/auth/src/main/java/com/mifos/feature/auth/login/domain/repository/LoginRepository.kt b/feature/auth/src/main/java/com/mifos/feature/auth/login/domain/repository/LoginRepository.kt new file mode 100644 index 00000000000..d870d114a5d --- /dev/null +++ b/feature/auth/src/main/java/com/mifos/feature/auth/login/domain/repository/LoginRepository.kt @@ -0,0 +1,14 @@ +package com.mifos.feature.auth.login.domain.repository + +import org.apache.fineract.client.models.PostAuthenticationResponse +import rx.Observable + +/** + * Created by Aditya Gupta on 06/08/23. + */ + +interface LoginRepository { + + fun login(username: String, password: String): Observable + +} \ No newline at end of file diff --git a/feature/auth/src/main/java/com/mifos/feature/auth/login/domain/use_case/LoginUseCase.kt b/feature/auth/src/main/java/com/mifos/feature/auth/login/domain/use_case/LoginUseCase.kt new file mode 100644 index 00000000000..4d2b7414a46 --- /dev/null +++ b/feature/auth/src/main/java/com/mifos/feature/auth/login/domain/use_case/LoginUseCase.kt @@ -0,0 +1,54 @@ +package com.mifos.feature.auth.login.domain.use_case + +import com.mifos.core.common.utils.Resource +import com.mifos.feature.auth.login.domain.repository.LoginRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.suspendCancellableCoroutine +import org.apache.fineract.client.models.PostAuthenticationResponse +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +/** + * Created by Aditya Gupta on 11/02/24. + */ + +class LoginUseCase(private val loginRepository: LoginRepository) { + + operator fun invoke( + username: String, + password: String + ): Flow> = flow { + try { + emit(Resource.Loading()) + val result = login(username, password) + emit(result) + } catch (e: Exception) { + emit(Resource.Error(e.message.toString())) + } + } + + suspend fun login(username: String, password: String): Resource { + return suspendCancellableCoroutine { continuation -> + loginRepository.login(username, password) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe(object : Subscriber() { + override fun onNext(user: PostAuthenticationResponse) { + continuation.resume(Resource.Success(user)) + } + + override fun onCompleted() { + // No operation needed + } + + override fun onError(e: Throwable) { + continuation.resumeWithException(e) + } + }) + } + } +} diff --git a/feature/auth/src/main/java/com/mifos/feature/auth/login/domain/use_case/PasswordValidationUseCase.kt b/feature/auth/src/main/java/com/mifos/feature/auth/login/domain/use_case/PasswordValidationUseCase.kt new file mode 100644 index 00000000000..a3815c300f5 --- /dev/null +++ b/feature/auth/src/main/java/com/mifos/feature/auth/login/domain/use_case/PasswordValidationUseCase.kt @@ -0,0 +1,28 @@ +package com.mifos.feature.auth.login.domain.use_case + +import com.mifos.feature.auth.R +import com.mifos.feature.auth.login.domain.model.ValidationResult + +/** + * Created by Aditya Gupta on 11/02/24. + */ + +class PasswordValidationUseCase { + + operator fun invoke(password: String): ValidationResult { + + if (password.isEmpty()) { + return ValidationResult( + success = false, + R.string.feature_auth_enter_credentials + ) + } else if (password.length < 6) { + return ValidationResult( + success = false, + R.string.feature_error_password_length + ) + } + return ValidationResult(success = true) + } + +} \ No newline at end of file diff --git a/feature/auth/src/main/java/com/mifos/feature/auth/login/domain/use_case/UsernameValidationUseCase.kt b/feature/auth/src/main/java/com/mifos/feature/auth/login/domain/use_case/UsernameValidationUseCase.kt new file mode 100644 index 00000000000..3f846bac05c --- /dev/null +++ b/feature/auth/src/main/java/com/mifos/feature/auth/login/domain/use_case/UsernameValidationUseCase.kt @@ -0,0 +1,27 @@ +package com.mifos.feature.auth.login.domain.use_case + +import com.mifos.feature.auth.R +import com.mifos.feature.auth.login.domain.model.ValidationResult + +/** + * Created by Aditya Gupta on 11/02/24. + */ + +class UsernameValidationUseCase { + + operator fun invoke(username: String): ValidationResult { + if (username.isEmpty()) { + return ValidationResult( + success = false, + R.string.feature_auth_enter_credentials + ) + } else if (username.length < 5) { + return ValidationResult( + success = false, + R.string.feature_error_username_length + ) + } + return ValidationResult(success = true) + } + +} \ No newline at end of file diff --git a/feature/auth/src/main/java/com/mifos/feature/auth/login/LoginScreen.kt b/feature/auth/src/main/java/com/mifos/feature/auth/login/presentation/LoginScreen.kt similarity index 60% rename from feature/auth/src/main/java/com/mifos/feature/auth/login/LoginScreen.kt rename to feature/auth/src/main/java/com/mifos/feature/auth/login/presentation/LoginScreen.kt index 21c79160ceb..585277d26bb 100644 --- a/feature/auth/src/main/java/com/mifos/feature/auth/login/LoginScreen.kt +++ b/feature/auth/src/main/java/com/mifos/feature/auth/login/presentation/LoginScreen.kt @@ -1,4 +1,4 @@ -package com.mifos.feature.auth.login +package com.mifos.feature.auth.login.presentation import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Column @@ -12,17 +12,24 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Error import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -31,6 +38,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontStyle @@ -42,15 +50,34 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.hilt.navigation.compose.hiltViewModel import com.mifos.core.designsystem.component.MifosAndroidClientIcon import com.mifos.core.designsystem.component.MifosOutlinedTextField import com.mifos.core.designsystem.theme.BluePrimary import com.mifos.core.designsystem.theme.BluePrimaryDark import com.mifos.core.designsystem.theme.DarkGrey +import com.mifos.core.designsystem.theme.White import com.mifos.feature.auth.R +/** + * Created by Aditya Gupta on 11/02/24. + */ + @Composable -fun LoginScreen() { +fun LoginScreen( + homeIntent: () -> Unit, + passcodeIntent: () -> Unit +) { + + val loginViewModel: LoginViewModel = hiltViewModel() + val state = loginViewModel.loginUiState.collectAsState().value + val context = LocalContext.current + + val snackbarHostState = remember { SnackbarHostState() } + + val showDialog = rememberSaveable { mutableStateOf(false) } var userName by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf( @@ -64,12 +91,46 @@ fun LoginScreen() { } var passwordVisibility: Boolean by remember { mutableStateOf(false) } + val usernameError: MutableState = remember { mutableStateOf(null) } + val passwordError: MutableState = remember { mutableStateOf(null) } + + when (state) { + is LoginUiState.Empty -> {} + + is LoginUiState.ShowError -> { + showDialog.value = false + LaunchedEffect(key1 = state.message) { + snackbarHostState.showSnackbar(message = context.getString(state.message)) + } + } + + is LoginUiState.ShowProgress -> { + showDialog.value = true + } + + is LoginUiState.ShowValidationError -> { + usernameError.value = state.usernameError + passwordError.value = state.passwordError + } + + LoginUiState.HomeActivityIntent -> { + showDialog.value = false + homeIntent() + } + + LoginUiState.PassCodeActivityIntent -> { + showDialog.value = false + passcodeIntent() + } + } + Scaffold( modifier = Modifier .fillMaxSize() .padding(16.dp), - containerColor = Color.White + containerColor = Color.White, + snackbarHost = { SnackbarHost(snackbarHostState) } ) { Column( modifier = Modifier @@ -106,7 +167,12 @@ fun LoginScreen() { }, icon = Icons.Filled.Person, label = R.string.feature_auth_username, - supportingText = null + error = usernameError.value, + trailingIcon = { + if (usernameError.value != null) { + Icon(imageVector = Icons.Filled.Error, contentDescription = null) + } + } ) Spacer(modifier = Modifier.height(6.dp)) @@ -117,23 +183,27 @@ fun LoginScreen() { password = value }, visualTransformation = if (passwordVisibility) VisualTransformation.None else PasswordVisualTransformation(), - trailingIcon = { - val image = if (passwordVisibility) - Icons.Filled.Visibility - else Icons.Filled.VisibilityOff - IconButton(onClick = { passwordVisibility = !passwordVisibility }) { - Icon(imageVector = image, null) - } - }, icon = Icons.Filled.Lock, label = R.string.feature_auth_password, - supportingText = null + error = passwordError.value, + trailingIcon = { + if (passwordError.value == null) { + val image = if (passwordVisibility) + Icons.Filled.Visibility + else Icons.Filled.VisibilityOff + IconButton(onClick = { passwordVisibility = !passwordVisibility }) { + Icon(imageVector = image, null) + } + } else { + Icon(imageVector = Icons.Filled.Error, contentDescription = null) + } + } ) Spacer(modifier = Modifier.height(8.dp)) Button( - onClick = { }, + onClick = { loginViewModel.validateUserInputs(userName.text, password.text) }, modifier = Modifier .fillMaxWidth() .heightIn(44.dp) @@ -146,11 +216,22 @@ fun LoginScreen() { Text(text = "Login", fontSize = 16.sp) } } + if (showDialog.value) { + Dialog( + onDismissRequest = { showDialog.value }, + properties = DialogProperties( + dismissOnBackPress = false, + dismissOnClickOutside = false + ) + ) { + CircularProgressIndicator(color = White) + } + } } } @Preview(showSystemUi = true, device = "id:pixel_7") @Composable fun LoginScreenPreview() { - LoginScreen() + LoginScreen({}, {}) } \ No newline at end of file diff --git a/feature/auth/src/main/java/com/mifos/feature/auth/login/presentation/LoginUiState.kt b/feature/auth/src/main/java/com/mifos/feature/auth/login/presentation/LoginUiState.kt new file mode 100644 index 00000000000..fdb409b0c37 --- /dev/null +++ b/feature/auth/src/main/java/com/mifos/feature/auth/login/presentation/LoginUiState.kt @@ -0,0 +1,22 @@ +package com.mifos.feature.auth.login.presentation + +/** + * Created by Aditya Gupta on 06/08/23. + */ + +sealed class LoginUiState { + + data object Empty : LoginUiState() + + data object ShowProgress : LoginUiState() + + data class ShowError(val message: Int) : LoginUiState() + + data class ShowValidationError(val usernameError: Int? = null, val passwordError: Int? = null) : + LoginUiState() + + data object HomeActivityIntent : LoginUiState() + + data object PassCodeActivityIntent : LoginUiState() + +} \ No newline at end of file diff --git a/feature/auth/src/main/java/com/mifos/feature/auth/login/presentation/LoginViewModel.kt b/feature/auth/src/main/java/com/mifos/feature/auth/login/presentation/LoginViewModel.kt new file mode 100644 index 00000000000..6a84a078f67 --- /dev/null +++ b/feature/auth/src/main/java/com/mifos/feature/auth/login/presentation/LoginViewModel.kt @@ -0,0 +1,133 @@ +package com.mifos.feature.auth.login.presentation + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.mifos.core.common.utils.BaseUrl +import com.mifos.core.common.utils.Network +import com.mifos.core.common.utils.Resource +import com.mifos.core.datastore.PrefManager +import com.mifos.core.network.di.BaseApiManagerQualifier +import com.mifos.feature.auth.R +import com.mifos.feature.auth.login.domain.use_case.LoginUseCase +import com.mifos.feature.auth.login.domain.use_case.PasswordValidationUseCase +import com.mifos.feature.auth.login.domain.use_case.UsernameValidationUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import org.apache.fineract.client.models.PostAuthenticationResponse +import org.mifos.core.apimanager.BaseApiManager +import javax.inject.Inject + +/** + * Created by Aditya Gupta on 06/08/23. + */ + +@HiltViewModel +class LoginViewModel @Inject constructor( + @ApplicationContext private val context: Context, + private val prefManager: PrefManager, + private val usernameValidationUseCase: UsernameValidationUseCase, + private val passwordValidationUseCase: PasswordValidationUseCase, + @BaseApiManagerQualifier private val baseApiManager: BaseApiManager, + private val loginUseCase: LoginUseCase +) : + ViewModel() { + + private val _loginUiState = MutableStateFlow(LoginUiState.Empty) + val loginUiState = _loginUiState.asStateFlow() + + + fun validateUserInputs(username: String, password: String) { + + val usernameValidationResult = usernameValidationUseCase(username) + val passwordValidationResult = passwordValidationUseCase(password) + + val hasError = + listOf(usernameValidationResult, passwordValidationResult).any { !it.success } + + if (hasError) { + _loginUiState.value = LoginUiState.ShowValidationError( + usernameValidationResult.message, + passwordValidationResult.message + ) + return + } + viewModelScope.launch { + setupPrefManger(username, password) + } + } + + private fun setupPrefManger(username: String, password: String) { + + prefManager.setTenant(BaseUrl.TENANT) + // Saving InstanceURL for next usages + prefManager.setInstanceUrl(BaseUrl.PROTOCOL_HTTPS + BaseUrl.API_ENDPOINT + BaseUrl.API_PATH) + // Saving domain name + prefManager.setInstanceDomain(BaseUrl.API_ENDPOINT) + // Saving port + prefManager.setPort(BaseUrl.PORT) + // Updating Services + baseApiManager.createService( + username, + password, + prefManager.getInstanceUrl(), + prefManager.getTenant(), + true + ) + if (Network.isOnline(context)) { + login(username, password) + } else { + _loginUiState.value = + LoginUiState.ShowError(R.string.feature_error_not_connected_internet) + } + } + + + fun login(username: String, password: String) { + viewModelScope.launch(Dispatchers.IO) { + loginUseCase(username, password).collect { result -> + when (result) { + is Resource.Error -> { + _loginUiState.value = + LoginUiState.ShowError(R.string.feature_error_login_failed) + } + + is Resource.Loading -> { + _loginUiState.value = LoginUiState.ShowProgress + } + + is Resource.Success -> { + result.data?.let { onLoginSuccessful(it, username, password) } + } + } + } + } + } + + + private fun onLoginSuccessful( + user: PostAuthenticationResponse, + username: String, + password: String + ) { + // Saving username password + prefManager.usernamePassword = Pair(username, password) + // Saving userID + prefManager.setUserId(user.userId!!.toInt()) + // Saving user's token + prefManager.saveToken("Basic " + user.base64EncodedAuthenticationKey) + // Saving user + prefManager.savePostAuthenticationResponse(user) + + if (prefManager.getPassCodeStatus()) { + _loginUiState.value = LoginUiState.HomeActivityIntent + } else { + _loginUiState.value = LoginUiState.PassCodeActivityIntent + } + } + +} \ No newline at end of file diff --git a/feature/auth/src/main/res/values/strings.xml b/feature/auth/src/main/res/values/strings.xml index 456306a9a50..510eeb5230f 100644 --- a/feature/auth/src/main/res/values/strings.xml +++ b/feature/auth/src/main/res/values/strings.xml @@ -4,4 +4,9 @@ Please enter your credentials Password Username + Please Enter the Credentials + Invalid username length + Invalid password length + No Internet Connection + Login Failed, Please Try Again Later. \ No newline at end of file diff --git a/mifosng-android/build.gradle.kts b/mifosng-android/build.gradle.kts index 5d149042bc2..9e5378b9a44 100644 --- a/mifosng-android/build.gradle.kts +++ b/mifosng-android/build.gradle.kts @@ -246,4 +246,8 @@ dependencies { implementation("androidx.compose.material3:material3:1.1.2") implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") implementation("androidx.compose.material:material-icons-extended:1.6.1") + + // ViewModel utilities for Compose + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") + implementation("androidx.hilt:hilt-navigation-compose:1.1.0") } \ No newline at end of file diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/activity/login/LoginActivity.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/activity/login/LoginActivity.kt index abf277afa39..eca155bbb71 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/activity/login/LoginActivity.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/activity/login/LoginActivity.kt @@ -6,224 +6,33 @@ package com.mifos.mifosxdroid.activity.login import android.content.Intent import android.os.Bundle -import android.text.Editable -import android.text.InputType -import android.text.TextWatcher -import android.view.KeyEvent -import android.view.Menu -import android.view.MenuItem -import android.view.View -import android.view.inputmethod.EditorInfo -import android.widget.Toast -import androidx.core.content.ContextCompat -import androidx.lifecycle.ViewModelProvider -import com.mifos.api.BaseApiManager +import androidx.activity.compose.setContent +import com.mifos.feature.auth.login.presentation.LoginScreen import com.mifos.mifosxdroid.activity.home.HomeActivity -import com.mifos.mifosxdroid.R import com.mifos.mifosxdroid.core.MifosBaseActivity -import com.mifos.mifosxdroid.core.util.Toaster -import com.mifos.mifosxdroid.databinding.ActivityLoginBinding import com.mifos.mifosxdroid.passcode.PassCodeActivity import com.mifos.utils.Constants -import com.mifos.utils.Network -import com.mifos.utils.PrefManager -import com.mifos.utils.PrefManager.savePostAuthenticationResponse -import com.mifos.utils.PrefManager.saveToken -import com.mifos.utils.ValidationUtil import dagger.hilt.android.AndroidEntryPoint -import org.apache.fineract.client.models.PostAuthenticationResponse -import javax.inject.Inject /** * Created by ishankhanna on 08/02/14. */ @AndroidEntryPoint -class LoginActivity : MifosBaseActivity(){ - - private lateinit var binding: ActivityLoginBinding - - private lateinit var viewModel: LoginViewModel - - @Inject - lateinit var baseApiManager: org.mifos.core.apimanager.BaseApiManager - - private lateinit var username: String - private lateinit var instanceURL: String - private lateinit var password: String - private lateinit var domain: String - private var isValidUrl = false - private val urlWatcher: TextWatcher = object : TextWatcher { - override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} - override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} - override fun afterTextChanged(editable: Editable) { - val port = if (binding.etInstancePort.editableText.toString() - .isEmpty() - ) null else Integer.valueOf(binding.etInstancePort.editableText.toString()) - instanceURL = ValidationUtil.getInstanceUrl(binding.etInstanceURL.text.toString(), port) - isValidUrl = ValidationUtil.isValidUrl(instanceURL) - binding.tvConstructedInstanceUrl.text = instanceURL - domain = binding.etInstanceURL.editableText.toString() - if (domain.isEmpty() || domain.contains(" ")) { - isValidUrl = false - } - binding.tvConstructedInstanceUrl.setTextColor( - if (isValidUrl) ContextCompat.getColor( - applicationContext, R.color.green_light - ) else ContextCompat.getColor( - applicationContext, R.color.red_light - ) - ) - } - } +class LoginActivity : MifosBaseActivity() { public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = ActivityLoginBinding.inflate(layoutInflater) - title = null - setContentView(binding.root) - binding.etInstancePort.inputType = InputType.TYPE_CLASS_NUMBER - if (PrefManager.getPort() != "80") binding.etInstancePort.setText(PrefManager.getPort()) - binding.etInstanceURL.setText(PrefManager.getInstanceDomain()) - binding.etInstanceURL.addTextChangedListener(urlWatcher) - binding.etInstancePort.addTextChangedListener(urlWatcher) - urlWatcher.afterTextChanged(Editable.Factory.getInstance().newEditable("")) - - viewModel = ViewModelProvider(this)[LoginViewModel::class.java] - - binding.btLogin.setOnClickListener { - hideKeyboard(binding.btLogin) - login() - } - - binding.etPassword.setOnEditorActionListener { _, actionId, keyEvent -> - if (actionId == EditorInfo.IME_ACTION_DONE || (keyEvent != null && keyEvent.keyCode == KeyEvent.KEYCODE_ENTER && keyEvent.action == KeyEvent.ACTION_DOWN)) { - login() - return@setOnEditorActionListener true - } - return@setOnEditorActionListener false - } - viewModel.loginUiState.observe(this){ - when(it) { - is LoginUiState.ShowProgress -> showProgressbar(it.state) - is LoginUiState.ShowLoginSuccessful -> { - hideProgress() - onLoginSuccessful(it.user) - } - is LoginUiState.ShowError -> { - hideProgress() - onLoginError(it.message) - } - } - } - } - - private fun validateUserInputs(): Boolean { - domain = binding.etInstanceURL.editableText.toString() - if (domain.isEmpty() || domain.contains(" ")) { - showToastMessage(getString(R.string.error_invalid_url)) - return false - } - if (!isValidUrl) { - showToastMessage(getString(R.string.error_invalid_connection)) - return false + setContent { + LoginScreen(homeIntent = { + startActivity(Intent(this, HomeActivity::class.java)) + finish() + }, passcodeIntent = { + val intent = Intent(this, PassCodeActivity::class.java) + intent.putExtra(Constants.INTIAL_LOGIN, true) + startActivity(intent) + finish() + }) } - username = binding.etUsername.editableText.toString() - password = binding.etPassword.editableText.toString() - if (username.isEmpty() || password.isEmpty()) { - showToastMessage(getString(R.string.error_enter_credentials)) - return false - } else { - if (username.length < 5) { - showToastMessage(getString(R.string.error_username_length)) - return false - } - if (password.length < 6) { - showToastMessage(getString(R.string.error_password_length)) - return false - } - } - return true - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - val inflater = menuInflater - inflater.inflate(R.menu.menu_login, menu) - return true } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - // Handle item selection - return when (item.itemId) { - R.id.mItem_connection_settings -> { - binding.llConnectionSettings.visibility = - if (binding.llConnectionSettings.visibility == View.VISIBLE) View.GONE else View.VISIBLE - true - } - - else -> super.onOptionsItemSelected(item) - } - } - - private fun showToastMessage(message: String) { - Toaster.show(findViewById(android.R.id.content), message, Toaster.LONG) - } - - private fun onLoginSuccessful(user: PostAuthenticationResponse) { - - PrefManager.usernamePassword = Pair(username,password) - // Saving userID - PrefManager.setUserId(user.userId!!.toInt()) - // Saving user's token - saveToken("Basic " + user.base64EncodedAuthenticationKey) - // Saving user - savePostAuthenticationResponse(user) - Toast.makeText( - this, getString(R.string.toast_welcome) + " " + user.username, Toast.LENGTH_SHORT - ).show() - if (PrefManager.getPassCodeStatus()) { - startActivity(Intent(this, HomeActivity::class.java)) - } else { - val intent = Intent(this, PassCodeActivity::class.java) - intent.putExtra(Constants.INTIAL_LOGIN, true) - startActivity(intent) - } - finish() - } - - private fun onLoginError(errorMessage: String) { - showToastMessage(errorMessage) - } - - private fun showProgressbar(show: Boolean) { - if (show) { - showProgress(getString(R.string.logging_in)) - } else { - hideProgress() - } - } - - - private fun login() { - if (!validateUserInputs()) { - return - } - // Saving tenant - PrefManager.setTenant(binding.etTenantIdentifier.editableText.toString()) - // Saving InstanceURL for next usages - PrefManager.setInstanceUrl(instanceURL) - // Saving domain name - PrefManager.setInstanceDomain(binding.etInstanceURL.editableText.toString()) - // Saving port - PrefManager.setPort(binding.etInstancePort.editableText.toString()) - // Updating Services - BaseApiManager.createService() - baseApiManager.createService(username,password,PrefManager.getInstanceUrl(),PrefManager.getTenant(),true) - if (Network.isOnline(this)) { - viewModel.login(username, password) - } else { - showToastMessage(getString(R.string.error_not_connected_internet)) - } - } - } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 0e23a5076a1..e69a31e6f03 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,3 +23,5 @@ include(":core:designsystem") include(":core:datastore") include(":core:common") include(":feature:auth") +include(":core:network") +include(":core:data")