From b3ab1d134a0d3ebf2fc36f342983c3bb91ce4c8e Mon Sep 17 00:00:00 2001 From: Sk Niyaj Ali Date: Fri, 25 Oct 2024 16:27:58 +0530 Subject: [PATCH] Feat: Migrated KYC Module to KMP --- README.md | 2 +- .../org/mifospay/core/common/DateHelper.kt | 6 +- .../data/repository/DocumentRepository.kt | 8 + .../data/repository/KycLevelRepository.kt | 13 +- .../repositoryImp/DocumentRepositoryImpl.kt | 50 ++- .../repositoryImp/KycLevelRepositoryImpl.kt | 46 ++- .../core/designsystem/icon/MifosIcons.kt | 10 + .../core/model}/kyc/KYCLevel1Details.kt | 21 +- .../core/network/services/DocumentService.kt | 9 + .../core/network/services/KYCLevel1Service.kt | 13 +- .../kotlin/org/mifospay/core/ui/AvatarBox.kt | 1 + feature/kyc/build.gradle.kts | 27 +- feature/kyc/consumer-rules.pro | 0 feature/kyc/proguard-rules.pro | 21 - .../{main => androidMain}/AndroidManifest.xml | 0 .../drawable/feature_kyc_ic_error_state.xml | 2 +- .../composeResources}/values/strings.xml | 2 +- .../feature/kyc/KYCDescriptionScreen.kt | 202 +++++++++ .../feature/kyc/KYCDescriptionViewModel.kt | 120 ++++++ .../mifospay/feature/kyc/KYCLevel1Screen.kt | 269 ++++++++++++ .../feature/kyc/KYCLevel1ViewModel.kt | 314 ++++++++++++++ .../mifospay/feature/kyc/KYCLevel2Screen.kt | 315 ++++++++++++++ .../feature/kyc/KYCLevel2ViewModel.kt | 238 +++++++++++ .../mifospay/feature/kyc/KYCLevel3Screen.kt | 51 +++ .../feature/kyc/KYCLevel3ViewModel.kt | 14 + .../org/mifospay/feature/kyc/di/KYCModule.kt | 24 ++ .../kyc/navigation/KYCLevel1Navigation.kt | 6 +- .../kyc/navigation/KYCLevel2Navigation.kt | 15 +- .../kyc/navigation/KYCLevel3Navigation.kt | 17 +- .../feature/kyc/navigation/KYCNavigation.kt | 0 .../feature/kyc/KYCDescriptionScreen.kt | 354 ---------------- .../feature/kyc/KYCDescriptionViewModel.kt | 80 ---- .../mifospay/feature/kyc/KYCLevel1Screen.kt | 286 ------------- .../feature/kyc/KYCLevel1ViewModel.kt | 85 ---- .../mifospay/feature/kyc/KYCLevel2Screen.kt | 389 ------------------ .../feature/kyc/KYCLevel2ViewModel.kt | 76 ---- .../mifospay/feature/kyc/KYCLevel3Screen.kt | 111 ----- .../feature/kyc/KYCLevel3ViewModel.kt | 34 -- .../org/mifospay/feature/kyc/di/KYCModule.kt | 50 --- feature/kyc/src/main/res/values/colors.xml | 18 - feature/kyc/src/main/res/values/dimens.xml | 25 -- gradle/libs.versions.toml | 6 +- .../prodReleaseRuntimeClasspath.tree.txt | 33 ++ .../prodReleaseRuntimeClasspath.txt | 1 + mifospay-shared/build.gradle.kts | 1 + .../org/mifospay/shared/di/KoinModules.kt | 2 + .../shared/navigation/MifosNavHost.kt | 45 +- 47 files changed, 1808 insertions(+), 1604 deletions(-) rename core/{network/src/commonMain/kotlin/org/mifospay/core/network/model/entity => model/src/commonMain/kotlin/org/mifospay/core/model}/kyc/KYCLevel1Details.kt (54%) delete mode 100644 feature/kyc/consumer-rules.pro delete mode 100644 feature/kyc/proguard-rules.pro rename feature/kyc/src/{main => androidMain}/AndroidManifest.xml (100%) rename feature/kyc/src/{main/res => commonMain/composeResources}/drawable/feature_kyc_ic_error_state.xml (92%) rename feature/kyc/src/{main/res => commonMain/composeResources}/values/strings.xml (97%) create mode 100644 feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCDescriptionScreen.kt create mode 100644 feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCDescriptionViewModel.kt create mode 100644 feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel1Screen.kt create mode 100644 feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel1ViewModel.kt create mode 100644 feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel2Screen.kt create mode 100644 feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel2ViewModel.kt create mode 100644 feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel3Screen.kt create mode 100644 feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel3ViewModel.kt create mode 100644 feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/di/KYCModule.kt rename feature/kyc/src/{main => commonMain}/kotlin/org/mifospay/feature/kyc/navigation/KYCLevel1Navigation.kt (80%) rename feature/kyc/src/{main => commonMain}/kotlin/org/mifospay/feature/kyc/navigation/KYCLevel2Navigation.kt (59%) rename feature/kyc/src/{main => commonMain}/kotlin/org/mifospay/feature/kyc/navigation/KYCLevel3Navigation.kt (56%) rename feature/kyc/src/{main => commonMain}/kotlin/org/mifospay/feature/kyc/navigation/KYCNavigation.kt (100%) delete mode 100644 feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCDescriptionScreen.kt delete mode 100644 feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCDescriptionViewModel.kt delete mode 100644 feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel1Screen.kt delete mode 100644 feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel1ViewModel.kt delete mode 100644 feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel2Screen.kt delete mode 100644 feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel2ViewModel.kt delete mode 100644 feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel3Screen.kt delete mode 100644 feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel3ViewModel.kt delete mode 100644 feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/di/KYCModule.kt delete mode 100644 feature/kyc/src/main/res/values/colors.xml delete mode 100644 feature/kyc/src/main/res/values/dimens.xml diff --git a/README.md b/README.md index cd204e53f..35d31872e 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ that can be used as a dependency in any other wallet based project. It is develo | :feature:finance | Done | ✅ | ✅ | ❔ | ✅ | ❔ | | :feature:account | Done | ✅ | ✅ | ❔ | ✅ | ❔ | | :feature:invoices | Done | ✅ | ✅ | ❔ | ✅ | ❔ | -| :feature:kyc | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | +| :feature:kyc | Done | ✅ | ✅ | ❔ | ✅ | ❔ | | :feature:make-transfer | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | | :feature:merchants | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | | :feature:notification | Not started | ❌ | ❌ | ❌ | ❌ | ❌ | diff --git a/core/common/src/commonMain/kotlin/org/mifospay/core/common/DateHelper.kt b/core/common/src/commonMain/kotlin/org/mifospay/core/common/DateHelper.kt index 6b4fee1be..666cb2ad7 100644 --- a/core/common/src/commonMain/kotlin/org/mifospay/core/common/DateHelper.kt +++ b/core/common/src/commonMain/kotlin/org/mifospay/core/common/DateHelper.kt @@ -11,6 +11,7 @@ package org.mifospay.core.common import kotlinx.datetime.Clock import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime import kotlinx.datetime.Month @@ -174,7 +175,10 @@ object DateHelper { } fun getDateAsStringFromLong(timeInMillis: Long): String { - return fullMonthFormat.parse(timeInMillis.toString()).toString() + val instant = Instant.fromEpochMilliseconds(timeInMillis) + .toLocalDateTime(TimeZone.currentSystemDefault()) + + return instant.format(shortMonthFormat) } val currentDate = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/DocumentRepository.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/DocumentRepository.kt index d19b8c874..5c8b52f4e 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/DocumentRepository.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/DocumentRepository.kt @@ -25,6 +25,14 @@ interface DocumentRepository { fileName: PartData.FileItem, ): Flow> + suspend fun createDocument( + entityType: String, + entityId: Long, + name: String, + description: String, + file: ByteArray, + ): DataState + suspend fun downloadDocument(entityType: String, entityId: Int, documentId: Int): Flow> suspend fun deleteDocument(entityType: String, entityId: Int, documentId: Int): Flow> diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/KycLevelRepository.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/KycLevelRepository.kt index 537f8498a..510e14371 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/KycLevelRepository.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/KycLevelRepository.kt @@ -11,19 +11,18 @@ package org.mifospay.core.data.repository import kotlinx.coroutines.flow.Flow import org.mifospay.core.common.DataState -import org.mifospay.core.network.model.GenericResponse -import org.mifospay.core.network.model.entity.kyc.KYCLevel1Details +import org.mifospay.core.model.kyc.KYCLevel1Details interface KycLevelRepository { - suspend fun fetchKYCLevel1Details(clientId: Int): Flow>> + fun fetchKYCLevel1Details(clientId: Long): Flow> suspend fun addKYCLevel1Details( - clientId: Int, + clientId: Long, kycLevel1Details: KYCLevel1Details, - ): Flow> + ): DataState suspend fun updateKYCLevel1Details( - clientId: Int, + clientId: Long, kycLevel1Details: KYCLevel1Details, - ): Flow> + ): DataState } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/DocumentRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/DocumentRepositoryImpl.kt index 2e8532176..adda6c6e7 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/DocumentRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/DocumentRepositoryImpl.kt @@ -9,10 +9,15 @@ */ package org.mifospay.core.data.repositoryImp +import io.ktor.client.request.forms.MultiPartFormDataContent +import io.ktor.client.request.forms.formData +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders import io.ktor.http.content.PartData import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.withContext import org.mifospay.core.common.DataState import org.mifospay.core.common.asDataStateFlow import org.mifospay.core.data.repository.DocumentRepository @@ -23,7 +28,10 @@ class DocumentRepositoryImpl( private val apiManager: FineractApiManager, private val ioDispatcher: CoroutineDispatcher, ) : DocumentRepository { - override suspend fun getDocuments(entityType: String, entityId: Int): Flow>> { + override suspend fun getDocuments( + entityType: String, + entityId: Int, + ): Flow>> { return apiManager.documentApi .getDocuments(entityType, entityId) .asDataStateFlow().flowOn(ioDispatcher) @@ -41,6 +49,46 @@ class DocumentRepositoryImpl( .asDataStateFlow().flowOn(ioDispatcher) } + override suspend fun createDocument( + entityType: String, + entityId: Long, + name: String, + description: String, + file: ByteArray, + ): DataState { + return try { + val formData = MultiPartFormDataContent( + formData { + // File part + append( + "file", + file, + Headers.build { + append(HttpHeaders.ContentType, "multipart/form-data") + append(HttpHeaders.ContentDisposition, "filename=\"$name\"") + }, + ) + + // Name and description fields + append("name", name) + append("description", description) + }, + ) + + withContext(ioDispatcher) { + apiManager.documentApi.createDocumentFile( + entityType = entityType, + entityId = entityId, + file = formData, + ) + } + + DataState.Success("Document Uploaded Successfully") + } catch (e: Exception) { + DataState.Error(e) + } + } + override suspend fun downloadDocument( entityType: String, entityId: Int, diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/KycLevelRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/KycLevelRepositoryImpl.kt index 4d4ad45f4..020d572a2 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/KycLevelRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/KycLevelRepositoryImpl.kt @@ -11,41 +11,57 @@ package org.mifospay.core.data.repositoryImp import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext import org.mifospay.core.common.DataState import org.mifospay.core.common.asDataStateFlow import org.mifospay.core.data.repository.KycLevelRepository +import org.mifospay.core.model.kyc.KYCLevel1Details import org.mifospay.core.network.FineractApiManager -import org.mifospay.core.network.model.GenericResponse -import org.mifospay.core.network.model.entity.kyc.KYCLevel1Details class KycLevelRepositoryImpl( private val apiManager: FineractApiManager, private val ioDispatcher: CoroutineDispatcher, ) : KycLevelRepository { - override suspend fun fetchKYCLevel1Details( - clientId: Int, - ): Flow>> { + override fun fetchKYCLevel1Details( + clientId: Long, + ): Flow> { return apiManager.kycLevel1Api .fetchKYCLevel1Details(clientId) + .catch { DataState.Error(it, null) } + .map { it.firstOrNull() } .asDataStateFlow().flowOn(ioDispatcher) } override suspend fun addKYCLevel1Details( - clientId: Int, + clientId: Long, kycLevel1Details: KYCLevel1Details, - ): Flow> { - return apiManager.kycLevel1Api - .addKYCLevel1Details(clientId, kycLevel1Details) - .asDataStateFlow().flowOn(ioDispatcher) + ): DataState { + return try { + withContext(ioDispatcher) { + apiManager.kycLevel1Api.addKYCLevel1Details(clientId, kycLevel1Details) + } + + DataState.Success("KYC Level One details added successfully") + } catch (e: Exception) { + DataState.Error(e) + } } override suspend fun updateKYCLevel1Details( - clientId: Int, + clientId: Long, kycLevel1Details: KYCLevel1Details, - ): Flow> { - return apiManager.kycLevel1Api - .updateKYCLevel1Details(clientId, kycLevel1Details) - .asDataStateFlow().flowOn(ioDispatcher) + ): DataState { + return try { + withContext(ioDispatcher) { + apiManager.kycLevel1Api.updateKYCLevel1Details(clientId, kycLevel1Details) + } + + DataState.Success("KYC Level One details added successfully") + } catch (e: Exception) { + DataState.Error(e) + } } } diff --git a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/icon/MifosIcons.kt b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/icon/MifosIcons.kt index af2304740..d5ce089a9 100644 --- a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/icon/MifosIcons.kt +++ b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/icon/MifosIcons.kt @@ -14,6 +14,8 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.filled.ArrowOutward import androidx.compose.material.icons.filled.AttachMoney +import androidx.compose.material.icons.filled.Badge +import androidx.compose.material.icons.filled.CalendarMonth import androidx.compose.material.icons.filled.Camera import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.ChevronLeft @@ -21,11 +23,13 @@ import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Description import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.FlashOff import androidx.compose.material.icons.filled.FlashOn import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Photo import androidx.compose.material.icons.filled.PhotoLibrary import androidx.compose.material.icons.filled.QrCode @@ -36,6 +40,7 @@ import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material.icons.outlined.AccountCircle import androidx.compose.material.icons.outlined.Cancel import androidx.compose.material.icons.outlined.DeleteOutline +import androidx.compose.material.icons.outlined.DoneAll import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.Home import androidx.compose.material.icons.outlined.Info @@ -113,4 +118,9 @@ object MifosIcons { val QrCode2 = Icons.Filled.QrCode2 val Edit = Icons.Filled.Edit val Edit2 = Icons.Outlined.Edit + val CalenderMonth = Icons.Filled.CalendarMonth + val OutlinedDoneAll = Icons.Outlined.DoneAll + val Person = Icons.Filled.Person + val Badge = Icons.Filled.Badge + val DataInfo = Icons.Filled.Description } diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/kyc/KYCLevel1Details.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/kyc/KYCLevel1Details.kt similarity index 54% rename from core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/kyc/KYCLevel1Details.kt rename to core/model/src/commonMain/kotlin/org/mifospay/core/model/kyc/KYCLevel1Details.kt index 411111bdf..af9233c20 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/kyc/KYCLevel1Details.kt +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/kyc/KYCLevel1Details.kt @@ -7,17 +7,20 @@ * * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ -package org.mifospay.core.network.model.entity.kyc +package org.mifospay.core.model.kyc import kotlinx.serialization.Serializable +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize @Serializable +@Parcelize data class KYCLevel1Details( - val firstName: String? = null, - val lastName: String? = null, - val addressLine1: String? = null, - val addressLine2: String? = null, - val mobileNo: String? = null, - val dob: String? = null, - val currentLevel: String = "", -) + val firstName: String, + val lastName: String, + val addressLine1: String, + val addressLine2: String, + val mobileNo: String, + val dob: String, + val currentLevel: String, +) : Parcelable diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/DocumentService.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/DocumentService.kt index 62f47b915..f650a22e3 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/DocumentService.kt +++ b/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/DocumentService.kt @@ -9,6 +9,7 @@ */ package org.mifospay.core.network.services +import de.jensklingenberg.ktorfit.http.Body import de.jensklingenberg.ktorfit.http.DELETE import de.jensklingenberg.ktorfit.http.GET import de.jensklingenberg.ktorfit.http.Multipart @@ -16,6 +17,7 @@ import de.jensklingenberg.ktorfit.http.POST import de.jensklingenberg.ktorfit.http.PUT import de.jensklingenberg.ktorfit.http.Part import de.jensklingenberg.ktorfit.http.Path +import io.ktor.client.request.forms.MultiPartFormDataContent import io.ktor.http.content.PartData import kotlinx.coroutines.flow.Flow import org.mifospay.core.network.model.entity.noncore.Document @@ -46,6 +48,13 @@ interface DocumentService { @Part typedFile: PartData, ): Flow + @POST("{entityType}/{entityId}/" + ApiEndPoints.DOCUMENTS) + suspend fun createDocumentFile( + @Path("entityType") entityType: String, + @Path("entityId") entityId: Long, + @Body file: MultiPartFormDataContent, + ): Unit + /** * This Service is for downloading the Document with EntityType and EntityId and Document Id * Rest End Point : diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/KYCLevel1Service.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/KYCLevel1Service.kt index bda3168ed..6afd8ea35 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/KYCLevel1Service.kt +++ b/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/KYCLevel1Service.kt @@ -15,24 +15,23 @@ import de.jensklingenberg.ktorfit.http.POST import de.jensklingenberg.ktorfit.http.PUT import de.jensklingenberg.ktorfit.http.Path import kotlinx.coroutines.flow.Flow -import org.mifospay.core.network.model.GenericResponse -import org.mifospay.core.network.model.entity.kyc.KYCLevel1Details +import org.mifospay.core.model.kyc.KYCLevel1Details import org.mifospay.core.network.utils.ApiEndPoints interface KYCLevel1Service { @GET(ApiEndPoints.DATATABLES + "/kyc_level1_details/{clientId}") - suspend fun fetchKYCLevel1Details(@Path("clientId") clientId: Int): Flow> + fun fetchKYCLevel1Details(@Path("clientId") clientId: Long): Flow> @POST(ApiEndPoints.DATATABLES + "/kyc_level1_details/{clientId}") suspend fun addKYCLevel1Details( - @Path("clientId") clientId: Int, + @Path("clientId") clientId: Long, @Body kycLevel1Details: KYCLevel1Details, - ): Flow + ): Unit @PUT(ApiEndPoints.DATATABLES + "/kyc_level1_details/{clientId}/") suspend fun updateKYCLevel1Details( - @Path("clientId") clientId: Int, + @Path("clientId") clientId: Long, @Body kycLevel1Details: KYCLevel1Details, - ): Flow + ): Unit } diff --git a/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/AvatarBox.kt b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/AvatarBox.kt index c4bc8bc5b..9a9591b3f 100644 --- a/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/AvatarBox.kt +++ b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/AvatarBox.kt @@ -73,6 +73,7 @@ fun AvatarBox( imageVector = icon, contentDescription = "Avatar", tint = contentColor, + modifier = Modifier.size((size / 2).dp), ) } } diff --git a/feature/kyc/build.gradle.kts b/feature/kyc/build.gradle.kts index d855b9d50..67ca42fc7 100644 --- a/feature/kyc/build.gradle.kts +++ b/feature/kyc/build.gradle.kts @@ -8,21 +8,24 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.android.feature) - alias(libs.plugins.mifospay.android.library.compose) + alias(libs.plugins.mifospay.cmp.feature) + alias(libs.plugins.kotlin.parcelize) } android { - namespace = "org.mifospay.kyc" + namespace = "org.mifospay.feature.kyc" } -dependencies { - implementation(projects.libs.countryCodePicker) - implementation(projects.libs.pullrefresh) - - implementation(libs.sheets.compose.dialogs.core) - implementation(libs.sheets.compose.dialogs.calender) - - // TODO:: this should be removed - implementation(libs.squareup.okhttp) +kotlin { + sourceSets { + commonMain.dependencies { + implementation(compose.ui) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.components.resources) + implementation(compose.components.uiToolingPreview) + implementation(libs.filekit.compose) + implementation(libs.coil.kt.compose) + } + } } \ No newline at end of file diff --git a/feature/kyc/consumer-rules.pro b/feature/kyc/consumer-rules.pro deleted file mode 100644 index e69de29bb..000000000 diff --git a/feature/kyc/proguard-rules.pro b/feature/kyc/proguard-rules.pro deleted file mode 100644 index 481bb4348..000000000 --- a/feature/kyc/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# 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/feature/kyc/src/main/AndroidManifest.xml b/feature/kyc/src/androidMain/AndroidManifest.xml similarity index 100% rename from feature/kyc/src/main/AndroidManifest.xml rename to feature/kyc/src/androidMain/AndroidManifest.xml diff --git a/feature/kyc/src/main/res/drawable/feature_kyc_ic_error_state.xml b/feature/kyc/src/commonMain/composeResources/drawable/feature_kyc_ic_error_state.xml similarity index 92% rename from feature/kyc/src/main/res/drawable/feature_kyc_ic_error_state.xml rename to feature/kyc/src/commonMain/composeResources/drawable/feature_kyc_ic_error_state.xml index bedfa5a2b..aaba57433 100644 --- a/feature/kyc/src/main/res/drawable/feature_kyc_ic_error_state.xml +++ b/feature/kyc/src/commonMain/composeResources/drawable/feature_kyc_ic_error_state.xml @@ -14,6 +14,6 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/feature/kyc/src/main/res/values/strings.xml b/feature/kyc/src/commonMain/composeResources/values/strings.xml similarity index 97% rename from feature/kyc/src/main/res/values/strings.xml rename to feature/kyc/src/commonMain/composeResources/values/strings.xml index b92a9afb1..f87a0f7dd 100644 --- a/feature/kyc/src/main/res/values/strings.xml +++ b/feature/kyc/src/commonMain/composeResources/values/strings.xml @@ -32,7 +32,7 @@ Address Line 1 Address Line 2 Phone Number - Select Date of Birth + Date of Birth Submit Please approve storage permissions Browse diff --git a/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCDescriptionScreen.kt b/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCDescriptionScreen.kt new file mode 100644 index 000000000..fe5bb505a --- /dev/null +++ b/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCDescriptionScreen.kt @@ -0,0 +1,202 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.kyc + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import mobile_wallet.feature.kyc.generated.resources.Res +import mobile_wallet.feature.kyc.generated.resources.feature_kyc_check +import mobile_wallet.feature.kyc.generated.resources.feature_kyc_complete_kyc +import mobile_wallet.feature.kyc.generated.resources.feature_kyc_error_oops +import mobile_wallet.feature.kyc.generated.resources.feature_kyc_loading +import mobile_wallet.feature.kyc.generated.resources.feature_kyc_unexpected_error_subtitle +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel +import org.mifospay.core.designsystem.component.MifosOverlayLoadingWheel +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.ui.AvatarBox +import org.mifospay.core.ui.EmptyContentScreen +import org.mifospay.core.ui.utils.EventsEffect + +@Composable +fun KYCScreen( + onLevel1Clicked: () -> Unit, + onLevel2Clicked: () -> Unit, + onLevel3Clicked: () -> Unit, + modifier: Modifier = Modifier, + viewModel: KYCDescriptionViewModel = koinViewModel(), +) { + val state by viewModel.kycState.collectAsStateWithLifecycle() + + EventsEffect(viewModel) { event -> + when (event) { + KycEvent.OnLevel1Clicked -> onLevel1Clicked.invoke() + KycEvent.OnLevel2Clicked -> onLevel2Clicked.invoke() + KycEvent.OnLevel3Clicked -> onLevel3Clicked.invoke() + } + } + + KYCDescriptionScreen( + kUiState = state, + onAction = remember(viewModel) { + { viewModel.trySendAction(it) } + }, + modifier = modifier, + ) +} + +@Composable +private fun KYCDescriptionScreen( + kUiState: KYCDescriptionUiState, + onAction: (KycAction) -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + when (kUiState) { + KYCDescriptionUiState.Loading -> { + MifosOverlayLoadingWheel(contentDesc = stringResource(Res.string.feature_kyc_loading)) + } + + is KYCDescriptionUiState.Error -> { + EmptyContentScreen( + title = stringResource(Res.string.feature_kyc_error_oops), + subTitle = stringResource(Res.string.feature_kyc_unexpected_error_subtitle), + modifier = Modifier, + iconTint = MaterialTheme.colorScheme.primary, + iconImageVector = MifosIcons.Info, + ) + } + + is KYCDescriptionUiState.Content -> { + KYCDescriptionScreen( + currentLevel = kUiState.currentLevel, + onLevel1Clicked = { onAction(KycAction.Level1Clicked) }, + onLevel2Clicked = { onAction(KycAction.Level2Clicked) }, + onLevel3Clicked = { onAction(KycAction.Level3Clicked) }, + ) + } + } + } +} + +@Composable +private fun KYCDescriptionScreen( + currentLevel: KycLevel?, + onLevel1Clicked: () -> Unit, + onLevel2Clicked: () -> Unit, + onLevel3Clicked: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + text = stringResource(Res.string.feature_kyc_complete_kyc), + modifier = Modifier.padding(vertical = 20.dp), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleMedium, + ) + + KycLevel.entries.forEach { kycLevel -> + KYCLevelCard( + title = kycLevel.title, + icon = kycLevel.icon, + completed = (currentLevel?.level ?: -1) >= kycLevel.level, + enabled = (currentLevel?.level ?: 0) >= kycLevel.level - 1, + onClick = { + when (kycLevel.level) { + 1 -> onLevel1Clicked.invoke() + 2 -> onLevel2Clicked.invoke() + 3 -> onLevel3Clicked.invoke() + } + }, + ) + } + } +} + +@Composable +private fun KYCLevelCard( + title: String, + icon: ImageVector, + completed: Boolean, + enabled: Boolean, + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + Row( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + OutlinedCard( + enabled = enabled, + onClick = onClick, + shape = RoundedCornerShape(4.dp), + colors = CardDefaults.outlinedCardColors( + containerColor = Color.Transparent, + disabledContainerColor = Color.Transparent, + ), + border = CardDefaults.outlinedCardBorder(true), + modifier = Modifier.weight(2.5f, false), + ) { + ListItem( + headlineContent = { + Text(text = title) + }, + leadingContent = { + AvatarBox(icon = icon) + }, + trailingContent = { + if (completed) { + Icon( + imageVector = MifosIcons.OutlinedDoneAll, + contentDescription = stringResource(Res.string.feature_kyc_check), + tint = MaterialTheme.colorScheme.primary, + ) + } + }, + colors = ListItemDefaults.colors( + containerColor = Color.Transparent, + ), + ) + } + } +} diff --git a/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCDescriptionViewModel.kt b/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCDescriptionViewModel.kt new file mode 100644 index 000000000..19658b811 --- /dev/null +++ b/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCDescriptionViewModel.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.kyc + +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import org.mifospay.core.common.DataState +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize +import org.mifospay.core.data.repository.KycLevelRepository +import org.mifospay.core.datastore.UserPreferencesRepository +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.ui.utils.BaseViewModel + +private const val KEY_STATE = "kyc_state" + +class KYCDescriptionViewModel( + private val repository: UserPreferencesRepository, + kycLevelRepository: KycLevelRepository, + savedStateHandle: SavedStateHandle, +) : BaseViewModel( + initialState = savedStateHandle[KEY_STATE] ?: run { + val clientId = requireNotNull(repository.clientId.value) + KycState(clientId = clientId) + }, +) { + @OptIn(ExperimentalCoroutinesApi::class) + val kycState = kycLevelRepository.fetchKYCLevel1Details(state.clientId).mapLatest { result -> + when (result) { + is DataState.Loading -> { + KYCDescriptionUiState.Loading + } + + is DataState.Error -> { + KYCDescriptionUiState.Error + } + + is DataState.Success -> { + val currentLevel = result.data?.let { + KycLevel.valueOf(it.currentLevel) + } + + KYCDescriptionUiState.Content(currentLevel) + } + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = KYCDescriptionUiState.Loading, + ) + + init { + stateFlow + .onEach { savedStateHandle[KEY_STATE] = it } + .launchIn(viewModelScope) + } + + override fun handleAction(action: KycAction) { + when (action) { + KycAction.Level1Clicked -> { + sendEvent(KycEvent.OnLevel1Clicked) + } + + KycAction.Level2Clicked -> { + sendEvent(KycEvent.OnLevel2Clicked) + } + + KycAction.Level3Clicked -> { + sendEvent(KycEvent.OnLevel3Clicked) + } + } + } +} + +@Parcelize +data class KycState( + val clientId: Long, +) : Parcelable + +sealed interface KYCDescriptionUiState { + data class Content(val currentLevel: KycLevel?) : KYCDescriptionUiState + data object Error : KYCDescriptionUiState + data object Loading : KYCDescriptionUiState +} + +enum class KycLevel( + val level: Int, + val title: String, + val icon: ImageVector, +) { + KYC_LEVEL_1(level = 1, title = "Basic Details", icon = MifosIcons.Person), + KYC_LEVEL_2(level = 2, title = "Upload Documents", icon = MifosIcons.Badge), + KYC_LEVEL_3(level = 3, title = "Review & Submit", icon = MifosIcons.DataInfo), +} + +sealed interface KycEvent { + data object OnLevel1Clicked : KycEvent + data object OnLevel2Clicked : KycEvent + data object OnLevel3Clicked : KycEvent +} + +sealed interface KycAction { + data object Level1Clicked : KycAction + data object Level2Clicked : KycAction + data object Level3Clicked : KycAction +} diff --git a/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel1Screen.kt b/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel1Screen.kt new file mode 100644 index 000000000..0655f574a --- /dev/null +++ b/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel1Screen.kt @@ -0,0 +1,269 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.kyc + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.DatePicker +import androidx.compose.material3.DatePickerDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberDatePickerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.launch +import mobile_wallet.feature.kyc.generated.resources.Res +import mobile_wallet.feature.kyc.generated.resources.feature_kyc_address_line_1 +import mobile_wallet.feature.kyc.generated.resources.feature_kyc_address_line_2 +import mobile_wallet.feature.kyc.generated.resources.feature_kyc_first_name +import mobile_wallet.feature.kyc.generated.resources.feature_kyc_last_name +import mobile_wallet.feature.kyc.generated.resources.feature_kyc_phone_number +import mobile_wallet.feature.kyc.generated.resources.feature_kyc_select_dob +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel +import org.mifospay.core.designsystem.component.BasicDialogState +import org.mifospay.core.designsystem.component.LoadingDialogState +import org.mifospay.core.designsystem.component.MifosBasicDialog +import org.mifospay.core.designsystem.component.MifosButton +import org.mifospay.core.designsystem.component.MifosLoadingDialog +import org.mifospay.core.designsystem.component.MifosScaffold +import org.mifospay.core.designsystem.component.MifosTextField +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.ui.utils.EventsEffect + +@Composable +internal fun KYCLevel1Screen( + navigateBack: () -> Unit, + navigateToKycLevel2: () -> Unit, + modifier: Modifier = Modifier, + viewModel: KYCLevel1ViewModel = koinViewModel(), +) { + val scope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + + EventsEffect(viewModel) { event -> + when (event) { + is KycLevel1Event.NavigateToKycLevel2 -> navigateToKycLevel2.invoke() + is KycLevel1Event.ShowToast -> { + scope.launch { + snackbarHostState.showSnackbar(event.message) + } + } + + is KycLevel1Event.OnNavigateBack -> navigateBack.invoke() + } + } + + KycLevel1Dialogs( + dialogState = state.dialogState, + onDismissRequest = remember(viewModel) { + { viewModel.trySendAction(KycLevel1Action.DismissDialog) } + }, + ) + + KYCLevel1ScreenContent( + state = state, + onAction = remember(viewModel) { + { action -> viewModel.trySendAction(action) } + }, + snackbarHostState = snackbarHostState, + modifier = modifier, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun KYCLevel1ScreenContent( + state: KycLevel1State, + onAction: (KycLevel1Action) -> Unit, + snackbarHostState: SnackbarHostState, + modifier: Modifier = Modifier, +) { + MifosScaffold( + modifier = modifier, + topBarTitle = state.title, + backPress = { + onAction(KycLevel1Action.NavigateBack) + }, + snackbarHost = { SnackbarHost(snackbarHostState) }, + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + item { + MifosTextField( + label = stringResource(Res.string.feature_kyc_first_name), + value = state.firstNameInput, + onValueChange = { + onAction(KycLevel1Action.FirstNameChanged(it)) + }, + ) + } + + item { + MifosTextField( + label = stringResource(Res.string.feature_kyc_last_name), + value = state.lastNameInput, + onValueChange = { + onAction(KycLevel1Action.LastNameChanged(it)) + }, + ) + } + + item { + MifosTextField( + label = stringResource(Res.string.feature_kyc_phone_number), + value = state.mobileNoInput, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + onValueChange = { + onAction(KycLevel1Action.MobileNoChanged(it)) + }, + ) + } + + item { + MifosTextField( + label = stringResource(Res.string.feature_kyc_address_line_1), + value = state.addressLine1Input, + onValueChange = { + onAction(KycLevel1Action.AddressLine1Changed(it)) + }, + ) + } + + item { + MifosTextField( + label = stringResource(Res.string.feature_kyc_address_line_2), + value = state.addressLine2Input, + onValueChange = { + onAction(KycLevel1Action.AddressLine2Changed(it)) + }, + ) + } + + item { + var showDialog by remember { mutableStateOf(false) } + + val dateState = rememberDatePickerState( + initialSelectedDateMillis = state.initialDate, + ) + + val confirmEnabled = remember { + derivedStateOf { dateState.selectedDateMillis != null } + } + + AnimatedVisibility(showDialog) { + DatePickerDialog( + onDismissRequest = { showDialog = false }, + confirmButton = { + TextButton( + onClick = { + showDialog = false + onAction(KycLevel1Action.DobChanged(dateState.selectedDateMillis!!)) + }, + enabled = confirmEnabled.value, + ) { + Text(text = "Ok") + } + }, + dismissButton = { + TextButton( + onClick = { + showDialog = false + }, + ) { Text("Cancel") } + }, + content = { + DatePicker(state = dateState) + }, + ) + } + + MifosTextField( + label = stringResource(Res.string.feature_kyc_select_dob), + value = state.dobInput, + readOnly = true, + showClearIcon = false, + trailingIcon = { + IconButton( + onClick = { + showDialog = true + }, + ) { + Icon( + imageVector = MifosIcons.CalenderMonth, + contentDescription = "Choose Date", + ) + } + }, + onValueChange = {}, + ) + } + + item { + MifosButton( + onClick = { + onAction(KycLevel1Action.SubmitClicked) + }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(text = state.submitButtonText) + } + } + } + } +} + +@Composable +private fun KycLevel1Dialogs( + dialogState: KycLevel1State.DialogState?, + onDismissRequest: () -> Unit, +) { + when (dialogState) { + is KycLevel1State.DialogState.Error -> MifosBasicDialog( + visibilityState = BasicDialogState.Shown( + message = dialogState.message, + ), + onDismissRequest = onDismissRequest, + ) + + is KycLevel1State.DialogState.Loading -> MifosLoadingDialog( + visibilityState = LoadingDialogState.Shown, + ) + + null -> Unit + } +} diff --git a/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel1ViewModel.kt b/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel1ViewModel.kt new file mode 100644 index 000000000..ec6885466 --- /dev/null +++ b/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel1ViewModel.kt @@ -0,0 +1,314 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.kyc + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.datetime.Clock +import org.mifospay.core.common.DataState +import org.mifospay.core.common.DateHelper +import org.mifospay.core.common.IgnoredOnParcel +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize +import org.mifospay.core.common.takeUntilResultSuccess +import org.mifospay.core.data.repository.KycLevelRepository +import org.mifospay.core.datastore.UserPreferencesRepository +import org.mifospay.core.model.kyc.KYCLevel1Details +import org.mifospay.core.ui.utils.BaseViewModel +import org.mifospay.feature.kyc.KycLevel1Action.Internal.HandleLevel1Result +import org.mifospay.feature.kyc.KycLevel1Action.Internal.KycLevel1DetailsResult +import org.mifospay.feature.kyc.KycLevel1State.DialogState.Error + +private const val KEY_STATE = "kyc_level_1_state" + +internal class KYCLevel1ViewModel( + private val kycLevelRepository: KycLevelRepository, + private val repository: UserPreferencesRepository, + savedStateHandle: SavedStateHandle, +) : BaseViewModel( + initialState = savedStateHandle[KEY_STATE] ?: run { + val clientId = requireNotNull(repository.clientId.value) + + KycLevel1State(clientId = clientId) + }, +) { + + init { + stateFlow + .onEach { savedStateHandle[KEY_STATE] = it } + .launchIn(viewModelScope) + + kycLevelRepository.fetchKYCLevel1Details(state.clientId) + .takeUntilResultSuccess() + .onEach { + sendAction(HandleLevel1Result(it)) + }.launchIn(viewModelScope) + } + + override fun handleAction(action: KycLevel1Action) { + when (action) { + is KycLevel1Action.FirstNameChanged -> { + mutableStateFlow.update { + it.copy(firstNameInput = action.firstName) + } + } + + is KycLevel1Action.LastNameChanged -> { + mutableStateFlow.update { + it.copy(lastNameInput = action.lastName) + } + } + + is KycLevel1Action.MobileNoChanged -> { + mutableStateFlow.update { + it.copy(mobileNoInput = action.mobileNo) + } + } + + is KycLevel1Action.AddressLine1Changed -> { + mutableStateFlow.update { + it.copy(addressLine1Input = action.addressLine1) + } + } + + is KycLevel1Action.AddressLine2Changed -> { + mutableStateFlow.update { + it.copy(addressLine2Input = action.addressLine2) + } + } + + is KycLevel1Action.DobChanged -> { + val formattedDate = DateHelper.getDateAsStringFromLong(action.dob) + + mutableStateFlow.update { + it.copy(dobInput = formattedDate) + } + } + + KycLevel1Action.DismissDialog -> { + mutableStateFlow.update { + it.copy(dialogState = null) + } + } + + KycLevel1Action.NavigateToKycLevel2 -> { + sendEvent(KycLevel1Event.NavigateToKycLevel2) + } + + is KycLevel1Action.NavigateBack -> { + sendEvent(KycLevel1Event.OnNavigateBack) + } + + KycLevel1Action.SubmitClicked -> initiateKycLevel1Submission() + + is KycLevel1DetailsResult -> handleKycLevel1DetailsResult(action) + + is HandleLevel1Result -> handleLevel1Result(action) + } + } + + private fun initiateKycLevel1Submission() = when { + state.firstNameInput.isEmpty() -> { + mutableStateFlow.update { + it.copy(dialogState = Error("First name is required")) + } + } + + state.lastNameInput.isEmpty() -> { + mutableStateFlow.update { + it.copy(dialogState = Error("Last name is required")) + } + } + + state.mobileNoInput.isEmpty() -> { + mutableStateFlow.update { + it.copy(dialogState = Error("Mobile number is required")) + } + } + + state.mobileNoInput.length != 10 -> { + mutableStateFlow.update { + it.copy(dialogState = Error("Mobile number should be 10 digits")) + } + } + + state.addressLine1Input.isEmpty() -> { + mutableStateFlow.update { + it.copy(dialogState = Error("Address line 1 is required")) + } + } + + state.addressLine2Input.isEmpty() -> { + mutableStateFlow.update { + it.copy(dialogState = Error("Address line 2 is required")) + } + } + + state.dobInput.isEmpty() -> { + mutableStateFlow.update { + it.copy(dialogState = Error("Date of birth is required")) + } + } + + else -> submitKycLevel1Details() + } + + private fun submitKycLevel1Details() { + mutableStateFlow.update { + it.copy(dialogState = KycLevel1State.DialogState.Loading) + } + + viewModelScope.launch { + val result = if (state.doesExist) { + kycLevelRepository.updateKYCLevel1Details(state.clientId, state.details) + } else { + kycLevelRepository.addKYCLevel1Details(state.clientId, state.details) + } + + sendAction(KycLevel1DetailsResult(result)) + } + } + + private fun handleKycLevel1DetailsResult(action: KycLevel1DetailsResult) { + when (action.result) { + is DataState.Success -> { + mutableStateFlow.update { + it.copy(dialogState = null) + } + sendEvent(KycLevel1Event.ShowToast(action.result.data)) + sendEvent(KycLevel1Event.NavigateToKycLevel2) + } + + is DataState.Error -> { + val message = action.result.exception.message.toString() + mutableStateFlow.update { + it.copy(dialogState = Error(message)) + } + } + + is DataState.Loading -> { + mutableStateFlow.update { + it.copy(dialogState = KycLevel1State.DialogState.Loading) + } + } + } + } + + private fun handleLevel1Result(action: HandleLevel1Result) { + when (action.result) { + is DataState.Success -> { + action.result.data?.let { data -> + mutableStateFlow.update { + it.copy( + firstNameInput = data.firstName, + lastNameInput = data.lastName, + addressLine1Input = data.addressLine1, + addressLine2Input = data.addressLine2, + mobileNoInput = data.mobileNo, + dobInput = data.dob, + currentLevelInput = data.currentLevel, + doesExist = true, + dialogState = null, + ) + } + } ?: run { + mutableStateFlow.update { + it.copy(dialogState = null) + } + } + } + + is DataState.Loading -> { + mutableStateFlow.update { + it.copy(dialogState = KycLevel1State.DialogState.Loading) + } + } + + else -> { + mutableStateFlow.update { + it.copy(dialogState = null) + } + } + } + } +} + +@Parcelize +internal data class KycLevel1State( + val clientId: Long, + val firstNameInput: String = "", + val lastNameInput: String = "", + val addressLine1Input: String = "", + val addressLine2Input: String = "", + val mobileNoInput: String = "", + val dobInput: String = "", + val currentLevelInput: String = KycLevel.KYC_LEVEL_1.name, + val doesExist: Boolean = false, + val dialogState: DialogState? = null, +) : Parcelable { + @IgnoredOnParcel + val title: String + get() = if (doesExist) "Update Basic Details" else "Enter Basic Details" + + @IgnoredOnParcel + val submitButtonText: String + get() = if (doesExist) "Update" else "Submit" + + @IgnoredOnParcel + val initialDate = Clock.System.now().toEpochMilliseconds() + + @IgnoredOnParcel + val details = KYCLevel1Details( + firstName = firstNameInput, + lastName = lastNameInput, + addressLine1 = addressLine1Input, + addressLine2 = addressLine2Input, + mobileNo = mobileNoInput, + dob = dobInput, + currentLevel = currentLevelInput, + ) + + sealed interface DialogState : Parcelable { + @Parcelize + data object Loading : DialogState + + @Parcelize + data class Error(val message: String) : DialogState + } +} + +internal sealed interface KycLevel1Event { + data object NavigateToKycLevel2 : KycLevel1Event + data object OnNavigateBack : KycLevel1Event + data class ShowToast(val message: String) : KycLevel1Event +} + +internal sealed interface KycLevel1Action { + data class FirstNameChanged(val firstName: String) : KycLevel1Action + data class LastNameChanged(val lastName: String) : KycLevel1Action + data class AddressLine1Changed(val addressLine1: String) : KycLevel1Action + data class AddressLine2Changed(val addressLine2: String) : KycLevel1Action + data class MobileNoChanged(val mobileNo: String) : KycLevel1Action + data class DobChanged(val dob: Long) : KycLevel1Action + + data object SubmitClicked : KycLevel1Action + data object DismissDialog : KycLevel1Action + data object NavigateBack : KycLevel1Action + data object NavigateToKycLevel2 : KycLevel1Action + + sealed interface Internal : KycLevel1Action { + data class HandleLevel1Result(val result: DataState) : Internal + data class KycLevel1DetailsResult(val result: DataState) : Internal + } +} diff --git a/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel2Screen.kt b/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel2Screen.kt new file mode 100644 index 000000000..477ac1511 --- /dev/null +++ b/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel2Screen.kt @@ -0,0 +1,315 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.kyc + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +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.layout.ContentScale +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil3.ImageLoader +import coil3.compose.AsyncImagePainter +import coil3.compose.LocalPlatformContext +import coil3.compose.SubcomposeAsyncImage +import coil3.compose.SubcomposeAsyncImageContent +import coil3.compose.rememberAsyncImagePainter +import io.github.vinceglb.filekit.compose.rememberFilePickerLauncher +import io.github.vinceglb.filekit.core.PickerMode +import io.github.vinceglb.filekit.core.PlatformFile +import kotlinx.coroutines.launch +import mobile_wallet.feature.kyc.generated.resources.Res +import mobile_wallet.feature.kyc.generated.resources.feature_kyc_file_name +import mobile_wallet.feature.kyc.generated.resources.feature_kyc_submit +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel +import org.mifospay.core.designsystem.component.BasicDialogState +import org.mifospay.core.designsystem.component.LoadingDialogState +import org.mifospay.core.designsystem.component.MifosBasicDialog +import org.mifospay.core.designsystem.component.MifosButton +import org.mifospay.core.designsystem.component.MifosLoadingDialog +import org.mifospay.core.designsystem.component.MifosLoadingWheel +import org.mifospay.core.designsystem.component.MifosScaffold +import org.mifospay.core.designsystem.component.MifosTextField +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.ui.AvatarBox +import org.mifospay.core.ui.utils.EventsEffect + +@Composable +internal fun KYCLevel2Screen( + navigateBack: () -> Unit, + navigateToLevel3: () -> Unit, + modifier: Modifier = Modifier, + viewModel: KYCLevel2ViewModel = koinViewModel(), +) { + val scope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + + EventsEffect(viewModel) { event -> + when (event) { + is KycLevel2Event.OnNavigateBack -> navigateBack.invoke() + KycLevel2Event.OnNavigateToLevel3 -> navigateToLevel3.invoke() + is KycLevel2Event.ShowToast -> { + scope.launch { + snackbarHostState.showSnackbar(event.message) + } + } + } + } + + KycLevel2Dialogs( + dialogState = state.dialogState, + onDismissRequest = remember(viewModel) { + { viewModel.trySendAction(KycLevel2Action.DismissDialog) } + }, + ) + + KYCLevel2ScreenContent( + state = state, + snackbarHostState = snackbarHostState, + modifier = modifier.fillMaxSize(), + onAction = remember(viewModel) { + { viewModel.trySendAction(it) } + }, + ) +} + +@Composable +internal fun KYCLevel2ScreenContent( + state: KycLevel2State, + snackbarHostState: SnackbarHostState, + modifier: Modifier = Modifier, + onAction: (KycLevel2Action) -> Unit, +) { + MifosScaffold( + topBarTitle = "Upload Documents", + snackbarHost = { SnackbarHost(snackbarHostState) }, + modifier = modifier, + backPress = { + onAction(KycLevel2Action.NavigateBack) + }, + ) { paddingValues -> + KYCLevel2ScreenContent( + state = state, + onAction = onAction, + modifier = Modifier.padding(paddingValues), + ) + } +} + +@Composable +private fun KYCLevel2ScreenContent( + state: KycLevel2State, + modifier: Modifier = Modifier, + onAction: (KycLevel2Action) -> Unit, +) { + Column( + modifier = modifier + .verticalScroll(rememberScrollState()) + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + DocumentPicker( + onChooseDocument = { + onAction(KycLevel2Action.FileChanged(it)) + }, + ) + + MifosTextField( + value = state.name, + label = stringResource(Res.string.feature_kyc_file_name), + onValueChange = { + onAction(KycLevel2Action.NameChanged(it)) + }, + onClickClearIcon = { + onAction(KycLevel2Action.NameChanged("")) + }, + ) + + MifosTextField( + value = state.description, + label = "Description", + onValueChange = { + onAction(KycLevel2Action.DescriptionChanged(it)) + }, + onClickClearIcon = { + onAction(KycLevel2Action.DescriptionChanged("")) + }, + ) + + MifosButton( + onClick = { + onAction(KycLevel2Action.SubmitClicked) + }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(text = stringResource(Res.string.feature_kyc_submit)) + } + } +} + +@Composable +private fun DocumentPicker( + modifier: Modifier = Modifier, + onChooseDocument: (PlatformFile) -> Unit, +) { + val scope = rememberCoroutineScope() + val context = LocalPlatformContext.current + + var uploadedImage by remember { mutableStateOf(null) } + + val painter = rememberAsyncImagePainter( + model = uploadedImage, + imageLoader = ImageLoader(context), + ) + + val filePicker = rememberFilePickerLauncher( + mode = PickerMode.Single, + ) { + scope.launch { + it?.let { file -> + onChooseDocument(file) + + uploadedImage = if (file.supportsStreams()) { + val size = file.getSize() + if (size != null && size > 0L) { + val buffer = ByteArray(size.toInt()) + val tmpBuffer = ByteArray(1000) + var totalBytesRead = 0 + file.getStream().use { + while (it.hasBytesAvailable()) { + val numRead = it.readInto(tmpBuffer, 1000) + tmpBuffer.copyInto( + buffer, + destinationOffset = totalBytesRead, + endIndex = numRead, + ) + totalBytesRead += numRead + } + } + buffer + } else { + file.readBytes() + } + } else { + file.readBytes() + } + } + } + } + + OutlinedCard( + modifier = modifier + .fillMaxWidth() + .height(200.dp), + shape = RoundedCornerShape(8.dp), + colors = CardDefaults.outlinedCardColors( + containerColor = Color.Transparent, + ), + onClick = filePicker::launch, + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + SubcomposeAsyncImage( + model = uploadedImage, + imageLoader = ImageLoader(context), + contentScale = ContentScale.None, + contentDescription = "Uploaded Image", + modifier = Modifier.align(Alignment.Center), + ) { + val painterState by painter.state.collectAsStateWithLifecycle() + + when (painterState) { + is AsyncImagePainter.State.Empty -> { + AvatarBox( + icon = MifosIcons.Add, + size = 120, + ) + } + + is AsyncImagePainter.State.Error -> { + if (uploadedImage == null) { + AvatarBox( + icon = MifosIcons.Add, + size = 120, + ) + } else { + Text( + text = "Unsupported Media Type for Preview", + modifier = Modifier.align(Alignment.Center), + ) + } + } + + is AsyncImagePainter.State.Loading -> { + MifosLoadingWheel( + contentDesc = "Loading Image", + modifier = Modifier.align(Alignment.Center), + ) + } + + is AsyncImagePainter.State.Success -> { + SubcomposeAsyncImageContent( + contentScale = ContentScale.Fit, + ) + } + } + } + } + } +} + +@Composable +private fun KycLevel2Dialogs( + dialogState: KycLevel2State.DialogState?, + onDismissRequest: () -> Unit, +) { + when (dialogState) { + is KycLevel2State.DialogState.Error -> MifosBasicDialog( + visibilityState = BasicDialogState.Shown( + message = dialogState.message, + ), + onDismissRequest = onDismissRequest, + ) + + is KycLevel2State.DialogState.Loading -> MifosLoadingDialog( + visibilityState = LoadingDialogState.Shown, + ) + + null -> Unit + } +} diff --git a/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel2ViewModel.kt b/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel2ViewModel.kt new file mode 100644 index 000000000..3835790ea --- /dev/null +++ b/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel2ViewModel.kt @@ -0,0 +1,238 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.kyc + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import io.github.vinceglb.filekit.core.PlatformFile +import io.github.vinceglb.filekit.core.baseName +import io.github.vinceglb.filekit.core.extension +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.mifospay.core.common.DataState +import org.mifospay.core.common.IgnoredOnParcel +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize +import org.mifospay.core.data.repository.DocumentRepository +import org.mifospay.core.data.util.Constants +import org.mifospay.core.datastore.UserPreferencesRepository +import org.mifospay.core.ui.utils.BaseViewModel +import org.mifospay.feature.kyc.KycLevel2Action.Internal.HandleDocumentUploadResult +import org.mifospay.feature.kyc.KycLevel2State.DialogState.Error + +private const val KEY_STATE = "kyc_level_2_state" + +internal class KYCLevel2ViewModel( + private val repository: DocumentRepository, + private val userRepository: UserPreferencesRepository, + savedStateHandle: SavedStateHandle, +) : BaseViewModel( + initialState = savedStateHandle[KEY_STATE] ?: run { + val clientId = requireNotNull(userRepository.clientId.value) + + KycLevel2State(entityId = clientId) + }, +) { + + init { + stateFlow + .onEach { savedStateHandle[KEY_STATE] = it } + .launchIn(viewModelScope) + } + + override fun handleAction(action: KycLevel2Action) { + when (action) { + is KycLevel2Action.FileChanged -> { + mutableStateFlow.update { + it.copy( + file = action.file, + name = action.file.baseName, + extension = action.file.extension, + ) + } + } + + is KycLevel2Action.NameChanged -> { + mutableStateFlow.update { + it.copy(name = action.name) + } + } + + is KycLevel2Action.DescriptionChanged -> { + mutableStateFlow.update { + it.copy(description = action.desc) + } + } + + KycLevel2Action.NavigateBack -> { + sendEvent(KycLevel2Event.OnNavigateBack) + } + + KycLevel2Action.DismissDialog -> { + mutableStateFlow.update { + it.copy(dialogState = null) + } + } + + KycLevel2Action.SubmitClicked -> initiateUploadDocument() + + is HandleDocumentUploadResult -> handleDocumentUploadResult(action) + } + } + + private fun initiateUploadDocument() = when { + state.file == null -> { + mutableStateFlow.update { + it.copy(dialogState = Error("Upload an image or pdf")) + } + } + + state.name.isEmpty() -> { + mutableStateFlow.update { + it.copy(dialogState = Error("Name is required")) + } + } + + state.description.isEmpty() -> { + mutableStateFlow.update { + it.copy(dialogState = Error("Description is required")) + } + } + + else -> uploadDocument() + } + + private fun uploadDocument() { + mutableStateFlow.update { + it.copy(dialogState = KycLevel2State.DialogState.Loading) + } + + viewModelScope.launch { + val file = state.file?.let { file -> + if (file.supportsStreams()) { + val size = file.getSize() + if (size != null && size > 0L) { + val buffer = ByteArray(size.toInt()) + val tmpBuffer = ByteArray(1000) + var totalBytesRead = 0 + file.getStream().use { + while (it.hasBytesAvailable()) { + val numRead = it.readInto(tmpBuffer, 1000) + tmpBuffer.copyInto( + buffer, + destinationOffset = totalBytesRead, + endIndex = numRead, + ) + totalBytesRead += numRead + } + } + buffer + } else { + file.readBytes() + } + } else { + file.readBytes() + } + } + + file?.let { + val result = repository.createDocument( + entityType = state.entityType, + entityId = state.entityId, + name = state.fileName, + description = state.description, + file = it, + ) + + sendAction(HandleDocumentUploadResult(result)) + } + } + } + + // region HandleDocumentUploadResult + /** + * API call to upload document fails with the following error: + * Unable to create parent directories + * of /.fineract/VENUS/documents/clients/2/iwqyn/abc.png + * This is a server side error, the client side code is correct. + */ + private fun handleDocumentUploadResult(action: HandleDocumentUploadResult) { + when (action.result) { + is DataState.Loading -> { + mutableStateFlow.update { + it.copy(dialogState = KycLevel2State.DialogState.Loading) + } + } + + is DataState.Error -> { + val message = action.result.exception.message.toString() + mutableStateFlow.update { + it.copy(dialogState = Error(message)) + } + } + + is DataState.Success -> { + mutableStateFlow.update { + it.copy(dialogState = null) + } + + sendEvent(KycLevel2Event.ShowToast(action.result.data)) + sendEvent(KycLevel2Event.OnNavigateToLevel3) + } + } + } + // endregion +} + +@Parcelize +internal data class KycLevel2State( + val entityId: Long, + val description: String = "", + @IgnoredOnParcel + val file: PlatformFile? = null, + val name: String = "", + val extension: String = "", + val entityType: String = Constants.ENTITY_TYPE_CLIENTS, + val dialogState: DialogState? = null, +) : Parcelable { + @IgnoredOnParcel + val fileName = "$name.$extension" + + sealed interface DialogState : Parcelable { + @Parcelize + data object Loading : DialogState + + @Parcelize + data class Error(val message: String) : DialogState + } +} + +internal sealed interface KycLevel2Event { + data object OnNavigateBack : KycLevel2Event + data object OnNavigateToLevel3 : KycLevel2Event + data class ShowToast(val message: String) : KycLevel2Event +} + +internal sealed interface KycLevel2Action { + data class DescriptionChanged(val desc: String) : KycLevel2Action + data class FileChanged(val file: PlatformFile) : KycLevel2Action + data class NameChanged(val name: String) : KycLevel2Action + + data object SubmitClicked : KycLevel2Action + + data object NavigateBack : KycLevel2Action + data object DismissDialog : KycLevel2Action + + sealed interface Internal : KycLevel2Action { + data class HandleDocumentUploadResult(val result: DataState) : Internal + } +} diff --git a/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel3Screen.kt b/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel3Screen.kt new file mode 100644 index 000000000..ebf6275a8 --- /dev/null +++ b/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel3Screen.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.kyc + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import org.koin.compose.viewmodel.koinViewModel +import org.mifospay.core.designsystem.component.MifosScaffold + +// TODO:: Implement KYC Level 3 screen +@Composable +internal fun KYCLevel3Screen( + navigateBack: () -> Unit, + modifier: Modifier = Modifier, + viewModel: KYCLevel3ViewModel = koinViewModel(), +) { + KYCLevel3ScreenContent( + modifier = modifier, + navigateBack = navigateBack, + ) +} + +@Composable +fun KYCLevel3ScreenContent( + modifier: Modifier = Modifier, + navigateBack: () -> Unit, +) { + MifosScaffold( + topBarTitle = "Review & Submit", + backPress = navigateBack, + modifier = modifier, + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text(text = "KYC Level 3") + } + } +} diff --git a/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel3ViewModel.kt b/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel3ViewModel.kt new file mode 100644 index 000000000..09585d4e4 --- /dev/null +++ b/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel3ViewModel.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.kyc + +import androidx.lifecycle.ViewModel + +class KYCLevel3ViewModel : ViewModel() diff --git a/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/di/KYCModule.kt b/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/di/KYCModule.kt new file mode 100644 index 000000000..edee1ffd4 --- /dev/null +++ b/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/di/KYCModule.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.kyc.di + +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module +import org.mifospay.feature.kyc.KYCDescriptionViewModel +import org.mifospay.feature.kyc.KYCLevel1ViewModel +import org.mifospay.feature.kyc.KYCLevel2ViewModel +import org.mifospay.feature.kyc.KYCLevel3ViewModel + +val KYCModule = module { + viewModelOf(::KYCDescriptionViewModel) + viewModelOf(::KYCLevel1ViewModel) + viewModelOf(::KYCLevel2ViewModel) + viewModelOf(::KYCLevel3ViewModel) +} diff --git a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/navigation/KYCLevel1Navigation.kt b/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/navigation/KYCLevel1Navigation.kt similarity index 80% rename from feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/navigation/KYCLevel1Navigation.kt rename to feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/navigation/KYCLevel1Navigation.kt index bfce6e3f9..15b02c3a5 100644 --- a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/navigation/KYCLevel1Navigation.kt +++ b/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/navigation/KYCLevel1Navigation.kt @@ -11,7 +11,7 @@ package org.mifospay.feature.kyc.navigation import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable +import org.mifospay.core.ui.composableWithPushTransitions import org.mifospay.feature.kyc.KYCLevel1Screen const val KYC_LEVEL_1_ROUTE = "kyc_level_1_route" @@ -21,10 +21,12 @@ fun NavController.navigateToKYCLevel1() { } fun NavGraphBuilder.kycLevel1Screen( + navigateBack: () -> Unit, navigateToKycLevel2: () -> Unit, ) { - composable(route = KYC_LEVEL_1_ROUTE) { + composableWithPushTransitions(route = KYC_LEVEL_1_ROUTE) { KYCLevel1Screen( + navigateBack = navigateBack, navigateToKycLevel2 = navigateToKycLevel2, ) } diff --git a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/navigation/KYCLevel2Navigation.kt b/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/navigation/KYCLevel2Navigation.kt similarity index 59% rename from feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/navigation/KYCLevel2Navigation.kt rename to feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/navigation/KYCLevel2Navigation.kt index 3f5c6da28..3c7096ec5 100644 --- a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/navigation/KYCLevel2Navigation.kt +++ b/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/navigation/KYCLevel2Navigation.kt @@ -11,21 +11,24 @@ package org.mifospay.feature.kyc.navigation import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable +import androidx.navigation.NavOptions +import org.mifospay.core.ui.composableWithPushTransitions import org.mifospay.feature.kyc.KYCLevel2Screen const val KYC_LEVEL_2_ROUTE = "kyc_level_2_route" -fun NavController.navigateToKYCLevel2() { - this.navigate(KYC_LEVEL_2_ROUTE) +fun NavController.navigateToKYCLevel2(navOptions: NavOptions? = null) { + this.navigate(KYC_LEVEL_2_ROUTE, navOptions) } fun NavGraphBuilder.kycLevel2Screen( - onSuccessKyc2: () -> Unit, + navigateBack: () -> Unit, + navigateToLevel3: () -> Unit, ) { - composable(route = KYC_LEVEL_2_ROUTE) { + composableWithPushTransitions(route = KYC_LEVEL_2_ROUTE) { KYCLevel2Screen( - onSuccessKyc2 = onSuccessKyc2, + navigateBack = navigateBack, + navigateToLevel3 = navigateToLevel3, ) } } diff --git a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/navigation/KYCLevel3Navigation.kt b/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/navigation/KYCLevel3Navigation.kt similarity index 56% rename from feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/navigation/KYCLevel3Navigation.kt rename to feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/navigation/KYCLevel3Navigation.kt index c29db1d4d..c7159517a 100644 --- a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/navigation/KYCLevel3Navigation.kt +++ b/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/navigation/KYCLevel3Navigation.kt @@ -11,17 +11,22 @@ package org.mifospay.feature.kyc.navigation import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable +import androidx.navigation.NavOptions +import org.mifospay.core.ui.composableWithPushTransitions import org.mifospay.feature.kyc.KYCLevel3Screen const val KYC_LEVEL_3_ROUTE = "kyc_level_3_route" -fun NavController.navigateToKYCLevel3() { - this.navigate(KYC_LEVEL_3_ROUTE) +fun NavController.navigateToKYCLevel3(navOptions: NavOptions? = null) { + this.navigate(KYC_LEVEL_3_ROUTE, navOptions) } -fun NavGraphBuilder.kycLevel3Screen() { - composable(route = KYC_LEVEL_3_ROUTE) { - KYCLevel3Screen() +fun NavGraphBuilder.kycLevel3Screen( + navigateBack: () -> Unit, +) { + composableWithPushTransitions(route = KYC_LEVEL_3_ROUTE) { + KYCLevel3Screen( + navigateBack = navigateBack, + ) } } diff --git a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/navigation/KYCNavigation.kt b/feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/navigation/KYCNavigation.kt similarity index 100% rename from feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/navigation/KYCNavigation.kt rename to feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/navigation/KYCNavigation.kt diff --git a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCDescriptionScreen.kt b/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCDescriptionScreen.kt deleted file mode 100644 index 860a28cfd..000000000 --- a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCDescriptionScreen.kt +++ /dev/null @@ -1,354 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md - */ -package org.mifospay.feature.kyc - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.mifos.library.pullrefresh.PullRefreshIndicator -import com.mifos.library.pullrefresh.pullRefresh -import com.mifos.library.pullrefresh.rememberPullRefreshState -import org.koin.androidx.compose.koinViewModel -import org.mifospay.core.designsystem.component.MifosButton -import org.mifospay.core.designsystem.component.MifosOverlayLoadingWheel -import org.mifospay.core.designsystem.icon.MifosIcons -import org.mifospay.core.ui.EmptyContentScreen -import org.mifospay.kyc.R - -@Composable -fun KYCScreen( - onLevel1Clicked: () -> Unit, - onLevel2Clicked: () -> Unit, - onLevel3Clicked: () -> Unit, - modifier: Modifier = Modifier, - viewModel: KYCDescriptionViewModel = koinViewModel(), -) { - val kUiState by viewModel.kycDescriptionState.collectAsState() - val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle() - - KYCDescriptionScreen( - modifier = modifier, - kUiState = kUiState, - onLevel1Clicked = { - // Todo : Implement onLevel1Clicked flow - onLevel1Clicked.invoke() - }, - onLevel2Clicked = { - // Todo : Implement onLevel2Clicked flow - onLevel2Clicked.invoke() - }, - onLevel3Clicked = { - // Todo : Implement onLevel3Clicked flow - onLevel3Clicked.invoke() - }, - isRefreshing = isRefreshing, - onRefresh = viewModel::refresh, - ) -} - -@Composable -private fun KYCDescriptionScreen( - kUiState: KYCDescriptionUiState, - isRefreshing: Boolean, - onRefresh: () -> Unit, - onLevel1Clicked: () -> Unit, - onLevel2Clicked: () -> Unit, - onLevel3Clicked: () -> Unit, - modifier: Modifier = Modifier, -) { - val pullRefreshState = rememberPullRefreshState(isRefreshing, onRefresh) - Box( - modifier = modifier - .pullRefresh(pullRefreshState), - ) { - when (kUiState) { - KYCDescriptionUiState.Loading -> { - MifosOverlayLoadingWheel(contentDesc = stringResource(R.string.feature_kyc_loading)) - } - - is KYCDescriptionUiState.Error -> { - EmptyContentScreen( - title = stringResource(id = R.string.feature_kyc_error_oops), - subTitle = stringResource(id = R.string.feature_kyc_unexpected_error_subtitle), - modifier = Modifier, - iconTint = MaterialTheme.colorScheme.primary, - iconImageVector = MifosIcons.Info, - ) - } - - is KYCDescriptionUiState.KYCDescription -> { - val kyc = kUiState.kycLevel1Details - if (kyc != null) { - KYCDescriptionScreen( - kyc, - onLevel1Clicked, - onLevel2Clicked, - onLevel3Clicked, - ) - } - } - } - - PullRefreshIndicator( - refreshing = isRefreshing, - state = pullRefreshState, - modifier = Modifier.align(Alignment.TopCenter), - ) - } -} - -@Composable -private fun KYCDescriptionScreen( - kyc: org.mifospay.core.network.model.entity.kyc.KYCLevel1Details, - onLevel1Clicked: () -> Unit, - onLevel2Clicked: () -> Unit, - onLevel3Clicked: () -> Unit, - modifier: Modifier = Modifier, -) { - val currentLevel = kyc.currentLevel - - Column( - modifier = modifier - .fillMaxSize() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top, - ) { - Text( - text = stringResource(R.string.feature_kyc_complete_kyc), - modifier = Modifier.padding(top = 40.dp), - fontSize = 19.sp, - textAlign = TextAlign.Center, - ) - - KYCLevelButton( - level = 1, - enabled = currentLevel >= 0.toString(), - completed = currentLevel >= 1.toString(), - modifier = Modifier.padding(top = 90.dp), - onLevel1Clicked, - ) - - KYCLevelButton( - level = 2, - enabled = currentLevel >= 1.toString(), - completed = currentLevel >= 2.toString(), - modifier = Modifier.padding(top = 80.dp), - onLevel2Clicked, - ) - - KYCLevelButton( - level = 3, - enabled = currentLevel >= 2.toString(), - completed = currentLevel >= 3.toString(), - modifier = Modifier.padding(top = 80.dp), - onLevel3Clicked, - ) - } -} - -@Composable -private fun KYCLevelButton( - level: Int, - enabled: Boolean, - completed: Boolean, - modifier: Modifier = Modifier, - onClick: () -> Unit = {}, -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - ButtonComponent( - stringResource(R.string.feature_kyc_level) + "$level", - enabled, - completed, - onClick, - ) - Spacer(modifier = Modifier.weight(0.1f)) - IconComponent( - completed, - modifier = Modifier.weight(0.9f), - ) - } -} - -@Composable -private fun ButtonComponent( - value: String, - enabled: Boolean, - completed: Boolean, - onButtonClicked: () -> Unit, - modifier: Modifier = Modifier, -) { - MifosButton( - modifier = modifier - .width(130.dp) - .heightIn(38.dp) - .shadow(43.dp), - onClick = { - if (enabled) { - onButtonClicked.invoke() - } - }, - contentPadding = PaddingValues(), - colors = ButtonDefaults.buttonColors( - containerColor = when { - completed -> MaterialTheme.colorScheme.onPrimary - enabled -> MaterialTheme.colorScheme.primary - else -> Color.Gray - }, - contentColor = when { - completed -> MaterialTheme.colorScheme.primary - enabled -> MaterialTheme.colorScheme.onPrimary - else -> Color.Gray - }, - ), - shape = RoundedCornerShape(10.dp), - enabled = enabled, - ) { - Text( - text = value, - fontSize = 18.sp, - fontWeight = FontWeight.Normal, - ) - } -} - -@Composable -private fun IconComponent( - completed: Boolean, - modifier: Modifier = Modifier, -) { - if (completed) { - Row( - modifier = modifier - .height(23.dp), - ) { - Icon( - imageVector = MifosIcons.Check, - contentDescription = stringResource(R.string.feature_kyc_check), - modifier = Modifier - .size(20.dp), - ) - Spacer(modifier = Modifier.width(26.dp)) - Text( - text = stringResource(R.string.feature_kyc_completion), - ) - } - } -} - -@Preview(showBackground = true) -@Composable -private fun KYCDescriptionPreview() { - val onLevel1Clicked: () -> Unit = { } - val onLevel2Clicked: () -> Unit = { } - val onLevel3Clicked: () -> Unit = { } - KYCDescriptionScreen( - kyc = org.mifospay.core.network.model.entity.kyc.KYCLevel1Details(), - onLevel1Clicked, - onLevel2Clicked, - onLevel3Clicked, - ) -} - -internal class KYCDescriptionUiStatePreviewProvider : - PreviewParameterProvider { - override val values = sequenceOf( - KYCDescriptionUiState.Loading, - KYCDescriptionUiState.Error, - KYCDescriptionUiState.KYCDescription( - org.mifospay.core.network.model.entity.kyc.KYCLevel1Details().apply { - currentLevel = "0" - }, - ), - KYCDescriptionUiState.KYCDescription( - org.mifospay.core.network.model.entity.kyc.KYCLevel1Details().apply { - currentLevel = "1" - }, - ), - KYCDescriptionUiState.KYCDescription( - org.mifospay.core.network.model.entity.kyc.KYCLevel1Details().apply { - currentLevel = "2" - }, - ), - KYCDescriptionUiState.KYCDescription( - org.mifospay.core.network.model.entity.kyc.KYCLevel1Details().apply { - currentLevel = "3" - }, - ), - ) -} - -@Preview(showBackground = true) -@Composable -private fun KYCDescriptionScreenPreview( - @PreviewParameter(KYCDescriptionUiStatePreviewProvider::class) uiState: KYCDescriptionUiState, -) { - KYCDescriptionScreen( - kUiState = uiState, - onLevel1Clicked = {}, - onLevel2Clicked = {}, - onLevel3Clicked = {}, - isRefreshing = false, - onRefresh = {}, - ) -} - -@Preview(showBackground = true) -@Composable -private fun ButtonComponentPreview() { - Column { - ButtonComponent(value = "Level 1", enabled = true, completed = false, onButtonClicked = {}) - ButtonComponent(value = "Level 2", enabled = true, completed = true, {}) - ButtonComponent(value = "Level 3", enabled = false, completed = false, {}) - } -} - -@Preview(showBackground = true) -@Composable -private fun IconComponentPreview() { - Column { - IconComponent(completed = true) - IconComponent(completed = false) - } -} diff --git a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCDescriptionViewModel.kt b/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCDescriptionViewModel.kt deleted file mode 100644 index 084224b08..000000000 --- a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCDescriptionViewModel.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md - */ -package org.mifospay.feature.kyc - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import org.mifospay.core.data.base.UseCase -import org.mifospay.core.data.base.UseCaseHandler -import org.mifospay.core.data.domain.usecase.kyc.FetchKYCLevel1Details -import org.mifospay.core.data.repository.local.LocalRepository -import org.mifospay.core.network.model.entity.kyc.KYCLevel1Details -import org.mifospay.feature.kyc.KYCDescriptionUiState.Loading - -class KYCDescriptionViewModel( - private val mUseCaseHandler: UseCaseHandler, - private val mLocalRepository: LocalRepository, - private val fetchKYCLevel1DetailsUseCase: FetchKYCLevel1Details, -) : ViewModel() { - private val descriptionState = MutableStateFlow(Loading) - val kycDescriptionState: StateFlow = descriptionState - - init { - fetchCurrentLevel() - } - - private val _isRefreshing = MutableStateFlow(false) - val isRefreshing: StateFlow get() = _isRefreshing.asStateFlow() - - fun refresh() { - viewModelScope.launch { - _isRefreshing.emit(true) - delay(2000) - fetchCurrentLevel() - _isRefreshing.emit(false) - } - } - - private fun fetchCurrentLevel() { - fetchKYCLevel1DetailsUseCase.walletRequestValues = - FetchKYCLevel1Details.RequestValues(mLocalRepository.clientDetails.clientId.toInt()) - val requestValues = fetchKYCLevel1DetailsUseCase.walletRequestValues - mUseCaseHandler.execute( - fetchKYCLevel1DetailsUseCase, - requestValues, - object : UseCase.UseCaseCallback { - override fun onSuccess(response: FetchKYCLevel1Details.ResponseValue) { - if (response.kycLevel1DetailsList.size == 1) { - descriptionState.value = KYCDescriptionUiState.KYCDescription( - response.kycLevel1DetailsList.first()!!, - ) - } else { - descriptionState.value = KYCDescriptionUiState.Error - } - } - - override fun onError(message: String) { - descriptionState.value = KYCDescriptionUiState.Error - } - }, - ) - } -} - -sealed interface KYCDescriptionUiState { - data class KYCDescription(val kycLevel1Details: KYCLevel1Details?) : KYCDescriptionUiState - data object Error : KYCDescriptionUiState - data object Loading : KYCDescriptionUiState -} diff --git a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel1Screen.kt b/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel1Screen.kt deleted file mode 100644 index e56cf5a68..000000000 --- a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel1Screen.kt +++ /dev/null @@ -1,286 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md - */ -package org.mifospay.feature.kyc - -import android.widget.Toast -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextFieldDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState -import com.maxkeppeler.sheets.calendar.CalendarDialog -import com.maxkeppeler.sheets.calendar.models.CalendarConfig -import com.maxkeppeler.sheets.calendar.models.CalendarSelection -import com.maxkeppeler.sheets.calendar.models.CalendarStyle -import com.mifos.library.countrycodepicker.CountryCodePicker -import org.koin.androidx.compose.koinViewModel -import org.mifospay.core.designsystem.component.MfOverlayLoadingWheel -import org.mifospay.core.designsystem.component.MifosButton -import org.mifospay.core.designsystem.component.MifosOutlinedTextField -import org.mifospay.core.designsystem.theme.MifosTheme -import org.mifospay.kyc.R -import java.time.format.DateTimeFormatter - -@Composable -internal fun KYCLevel1Screen( - navigateToKycLevel2: () -> Unit, - modifier: Modifier = Modifier, - viewModel: KYCLevel1ViewModel = koinViewModel(), -) { - val kyc1uiState by viewModel.kyc1uiState.collectAsStateWithLifecycle() - - KYCLevel1Screen( - uiState = kyc1uiState, - submitData = viewModel::submitData, - navigateToKycLevel2 = navigateToKycLevel2, - modifier = modifier, - ) -} - -@Composable -private fun KYCLevel1Screen( - uiState: KYCLevel1UiState, - submitData: (KYCLevel1DetailsState) -> Unit, - navigateToKycLevel2: () -> Unit, - modifier: Modifier = Modifier, -) { - val context = LocalContext.current - - Kyc1Form( - submitData = submitData, - modifier = modifier, - ) - - when (uiState) { - KYCLevel1UiState.Loading -> { - MfOverlayLoadingWheel( - contentDesc = stringResource(id = R.string.feature_kyc_submitting), - ) - } - - KYCLevel1UiState.Error -> { - Toast.makeText( - context, - stringResource(R.string.feature_kyc_error_adding_KYC_Level_1_details), - Toast.LENGTH_SHORT, - ).show() - navigateToKycLevel2.invoke() - } - - KYCLevel1UiState.Success -> { - Toast.makeText( - context, - stringResource(R.string.feature_kyc_successkyc1), - Toast.LENGTH_SHORT, - ).show() - navigateToKycLevel2.invoke() - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun Kyc1Form( - submitData: (KYCLevel1DetailsState) -> Unit, - modifier: Modifier = Modifier, -) { - var firstName by rememberSaveable { mutableStateOf("") } - var lastName by rememberSaveable { mutableStateOf("") } - var address1 by rememberSaveable { mutableStateOf("") } - var address2 by rememberSaveable { mutableStateOf("") } - var mobileNumber by rememberSaveable { mutableStateOf("") } - var dateOfBirth by rememberSaveable { mutableStateOf("") } - val dateState = rememberUseCaseState() - val dateFormatter = - DateTimeFormatter.ofPattern(stringResource(R.string.feature_kyc_date_format)) - - val kycDetails = KYCLevel1DetailsState( - firstName = firstName, - lastName = lastName, - addressLine1 = address1, - addressLine2 = address2, - mobileNo = mobileNumber, - dob = dateOfBirth, - ) - - Column( - modifier = modifier - .fillMaxWidth() - .padding(16.dp), - ) { - Spacer(modifier = Modifier.height(20.dp)) - MifosOutlinedTextField( - label = R.string.feature_kyc_first_name, - value = firstName, - onValueChange = { - firstName = it - }, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), - ) - MifosOutlinedTextField( - label = R.string.feature_kyc_last_name, - value = lastName, - onValueChange = { - lastName = it - }, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), - ) - MifosOutlinedTextField( - label = R.string.feature_kyc_address_line_1, - value = address1, - onValueChange = { - address1 = it - }, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), - ) - MifosOutlinedTextField( - label = R.string.feature_kyc_address_line_2, - value = address2, - onValueChange = { - address2 = it - }, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), - ) - - Box( - modifier = Modifier - .padding(vertical = 7.dp), - ) { - val keyboardController = LocalSoftwareKeyboardController.current - CountryCodePicker( - modifier = Modifier, - shape = RoundedCornerShape(3.dp), - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - ), - onValueChange = { (code, phone), isValid -> - if (isValid) { - mobileNumber = code + phone - } - }, - label = { - Text(stringResource(id = R.string.feature_kyc_phone_number)) - }, - keyboardActions = KeyboardActions { keyboardController?.hide() }, - ) - } - - CalendarDialog( - state = dateState, - config = CalendarConfig( - monthSelection = true, - yearSelection = true, - style = CalendarStyle.MONTH, - ), - selection = CalendarSelection.Date { date -> - dateOfBirth = dateFormatter.format(date) - }, - ) - - Box( - modifier = Modifier - .fillMaxWidth() - .height(70.dp) - .padding(vertical = 9.dp) - .clickable { dateState.show() } - .border( - width = 1.dp, - color = Color.Black, - ) - .padding(12.dp) - .clip(shape = RoundedCornerShape(8.dp)), - ) { - Text( - text = dateOfBirth.ifEmpty { stringResource(R.string.feature_kyc_select_dob) }, - style = MaterialTheme.typography.bodyLarge, - ) - } - - MifosButton( - onClick = { - submitData(kycDetails) - }, - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(vertical = 7.dp), - ) { - Text(text = stringResource(R.string.feature_kyc_submit)) - } - } -} - -@Preview(showBackground = true) -@Composable -private fun Kyc1FormPreview() { - MifosTheme { - KYCLevel1Screen( - uiState = KYCLevel1UiState.Loading, - submitData = { _ -> }, - navigateToKycLevel2 = {}, - ) - } -} - -@Preview(showBackground = true) -@Composable -private fun Kyc1PreviewWithError() { - MifosTheme { - KYCLevel1Screen( - uiState = KYCLevel1UiState.Error, - submitData = { _ -> }, - navigateToKycLevel2 = {}, - ) - } -} - -@Preview(showBackground = true) -@Composable -private fun Kyc1FormPreviewWithSuccess() { - MifosTheme { - KYCLevel1Screen( - uiState = KYCLevel1UiState.Success, - submitData = { _ -> }, - navigateToKycLevel2 = {}, - ) - } -} diff --git a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel1ViewModel.kt b/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel1ViewModel.kt deleted file mode 100644 index 77529d816..000000000 --- a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel1ViewModel.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md - */ -package org.mifospay.feature.kyc - -import androidx.lifecycle.ViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import org.mifospay.core.data.base.UseCase -import org.mifospay.core.data.base.UseCaseHandler -import org.mifospay.core.data.domain.usecase.kyc.UploadKYCLevel1Details -import org.mifospay.core.data.repository.local.LocalRepository -import org.mifospay.core.network.model.entity.kyc.KYCLevel1Details -import org.mifospay.feature.kyc.KYCLevel1UiState.Loading - -class KYCLevel1ViewModel( - private val mUseCaseHandler: UseCaseHandler, - private val mLocalRepository: LocalRepository, - private val uploadKYCLevel1DetailsUseCase: UploadKYCLevel1Details, -) : ViewModel() { - - private val kycUiState = MutableStateFlow(Loading) - val kyc1uiState: StateFlow = kycUiState - - fun submitData(kycLevel1Details: KYCLevel1DetailsState) { - uploadKYCLevel1DetailsUseCase.walletRequestValues = UploadKYCLevel1Details.RequestValues( - mLocalRepository.clientDetails.clientId.toInt(), - kycLevel1Details.toModel(), - ) - val requestValues = uploadKYCLevel1DetailsUseCase.walletRequestValues - mUseCaseHandler.execute( - uploadKYCLevel1DetailsUseCase, - requestValues, - object : UseCase.UseCaseCallback { - override fun onSuccess(response: UploadKYCLevel1Details.ResponseValue) { - kycUiState.value = KYCLevel1UiState.Success - } - - override fun onError(message: String) { - kycUiState.value = KYCLevel1UiState.Error - } - }, - ) - } -} - -sealed interface KYCLevel1UiState { - data object Loading : KYCLevel1UiState - data object Success : KYCLevel1UiState - data object Error : KYCLevel1UiState -} - -data class KYCLevel1DetailsState( - val firstName: String, - - val lastName: String, - - val addressLine1: String, - - val addressLine2: String, - - val mobileNo: String, - - val dob: String, - - val currentLevel: String = "1", -) - -internal fun KYCLevel1DetailsState.toModel(): org.mifospay.core.network.model.entity.kyc.KYCLevel1Details { - return org.mifospay.core.network.model.entity.kyc.KYCLevel1Details( - firstName = firstName.trim(), - lastName = lastName.trim(), - addressLine1 = addressLine1.trim(), - addressLine2 = addressLine2.trim(), - mobileNo = mobileNo.trim(), - dob = dob.trim(), - currentLevel = currentLevel, - ) -} diff --git a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel2Screen.kt b/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel2Screen.kt deleted file mode 100644 index 35d9bb24a..000000000 --- a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel2Screen.kt +++ /dev/null @@ -1,389 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md - */ -package org.mifospay.feature.kyc - -import android.Manifest -import android.app.Activity -import android.content.Intent -import android.content.pm.PackageManager -import android.net.Uri -import android.os.Build.VERSION.SDK_INT -import android.provider.Settings -import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarDuration -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.SnackbarResult -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale -import androidx.core.content.ContextCompat -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.compose.LocalLifecycleOwner -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import kotlinx.coroutines.launch -import org.koin.androidx.compose.koinViewModel -import org.mifospay.core.designsystem.component.MfOverlayLoadingWheel -import org.mifospay.core.designsystem.component.MifosButton -import org.mifospay.core.designsystem.component.MifosOutlinedTextField -import org.mifospay.core.designsystem.theme.MifosTheme -import org.mifospay.kyc.R - -@Composable -internal fun KYCLevel2Screen( - onSuccessKyc2: () -> Unit, - modifier: Modifier = Modifier, - viewModel: KYCLevel2ViewModel = koinViewModel(), -) { - val kyc2uiState by viewModel.kyc2uiState.collectAsStateWithLifecycle() - - KYCLevel2Screen( - uiState = kyc2uiState, - uploadData = viewModel::uploadKYCDocs, - onSuccessKyc2 = onSuccessKyc2, - modifier = modifier, - ) -} - -@Composable -private fun KYCLevel2Screen( - uiState: KYCLevel2UiState, - uploadData: (String, Uri) -> Unit, - onSuccessKyc2: () -> Unit, - modifier: Modifier = Modifier, -) { - val context = LocalContext.current - - Kyc2Form( - modifier = modifier, - uploadData = uploadData, - ) - - when (uiState) { - KYCLevel2UiState.Loading -> { - MfOverlayLoadingWheel( - contentDesc = stringResource(id = R.string.feature_kyc_submitting), - ) - } - - KYCLevel2UiState.Error -> { - Toast.makeText( - context, - stringResource(R.string.feature_kyc_error_adding_KYC_Level_2_details), - Toast.LENGTH_SHORT, - ).show() - } - - KYCLevel2UiState.Success -> { - Toast.makeText( - context, - stringResource(R.string.feature_kyc_successkyc2), - Toast.LENGTH_SHORT, - ).show() - onSuccessKyc2.invoke() - } - } -} - -@Suppress("LongMethod", "CyclomaticComplexMethod") -@Composable -private fun Kyc2Form( - uploadData: (String, Uri) -> Unit, - modifier: Modifier = Modifier, -) { - var idType by rememberSaveable { mutableStateOf("") } - val context = LocalContext.current - var result by rememberSaveable { mutableStateOf(null) } - val lifecycleOwner = LocalLifecycleOwner.current - val scope = rememberCoroutineScope() - val snackBarHostState = remember { SnackbarHostState() } - val docLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { - result = it - } - - var storagePermissionGranted by remember { - mutableStateOf( - if (SDK_INT >= 33) { - ContextCompat.checkSelfPermission(context, Manifest.permission.READ_MEDIA_IMAGES) == - PackageManager.PERMISSION_GRANTED - } else { - ContextCompat.checkSelfPermission( - context, - Manifest.permission.READ_EXTERNAL_STORAGE, - ) == - PackageManager.PERMISSION_GRANTED - }, - ) - } - - var shouldShowPermissionRationale = - if (SDK_INT >= 33) { - shouldShowRequestPermissionRationale( - context as Activity, - Manifest.permission.READ_MEDIA_IMAGES, - ) - } else { - shouldShowRequestPermissionRationale( - context as Activity, - Manifest.permission.READ_EXTERNAL_STORAGE, - ) - } - - var shouldDirectUserToApplicationSettings by remember { - mutableStateOf(false) - } - - val decideCurrentPermissionStatus: (Boolean, Boolean) -> String = - { granted, permissionRationale -> - if (granted) { - "Granted" - } else if (permissionRationale) { - "Rejected" - } else { - "Denied" - } - } - - var currentPermissionStatus by remember { - mutableStateOf( - decideCurrentPermissionStatus( - storagePermissionGranted, - shouldShowPermissionRationale, - ), - ) - } - - val permission = if (SDK_INT >= 33) { - Manifest.permission.READ_MEDIA_IMAGES - } else { - Manifest.permission.READ_EXTERNAL_STORAGE - } - - val storagePermissionLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.RequestPermission(), - onResult = { isGranted -> - storagePermissionGranted = isGranted - - if (!isGranted) { - shouldShowPermissionRationale = - if (SDK_INT >= 33) { - shouldShowRequestPermissionRationale( - context, - Manifest.permission.READ_MEDIA_IMAGES, - ) - } else { - shouldShowRequestPermissionRationale( - context, - Manifest.permission.READ_EXTERNAL_STORAGE, - ) - } - } - shouldDirectUserToApplicationSettings = - !shouldShowPermissionRationale && !storagePermissionGranted - currentPermissionStatus = decideCurrentPermissionStatus( - storagePermissionGranted, - shouldShowPermissionRationale, - ) - }, - ) - - DisposableEffect( - key1 = lifecycleOwner, - effect = { - val observer = LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_START && - !storagePermissionGranted && - !shouldShowPermissionRationale - ) { - storagePermissionLauncher.launch(permission) - } - } - lifecycleOwner.lifecycle.addObserver(observer) - onDispose { - lifecycleOwner.lifecycle.removeObserver(observer) - } - }, - ) - - Scaffold( - modifier = modifier, - snackbarHost = { - SnackbarHost(hostState = snackBarHostState) - }, - ) { contentPadding -> - Box( - modifier = Modifier - .padding(contentPadding) - .fillMaxSize(), - contentAlignment = Alignment.TopCenter, - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp), - verticalArrangement = Arrangement.spacedBy(20.dp), - ) { - MifosOutlinedTextField( - label = R.string.feature_kyc_id_type, - value = idType, - onValueChange = { - idType = it - }, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), - ) - - Row { - MifosButton( - onClick = { - if (storagePermissionGranted) { - docLauncher.launch(arrayOf("application/pdf", "image/*")) - } else { - Toast.makeText( - context, - R.string.feature_kyc_approve_permission, - Toast.LENGTH_SHORT, - ).show() - } - }, - ) { - Text(text = stringResource(id = R.string.feature_kyc_browse)) - } - result?.let { doc -> - val fileName = doc.path?.substringAfterLast("/").toString() - Text( - text = stringResource(id = R.string.feature_kyc_file_name) + fileName, - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.padding(horizontal = 2.dp), - ) - } - } - - MifosButton( - modifier = Modifier.align(Alignment.CenterHorizontally), - onClick = { - result?.let { uri -> - uploadData(idType, uri) - } - }, - ) { - Text(text = stringResource(id = R.string.feature_kyc_submit)) - } - } - } - - if (shouldShowPermissionRationale) { - LaunchedEffect(Unit) { - scope.launch { - val userAction = snackBarHostState.showSnackbar( - message = R.string.feature_kyc_approve_permission.toString(), - actionLabel = R.string.feature_kyc_approve.toString(), - duration = SnackbarDuration.Indefinite, - withDismissAction = true, - ) - when (userAction) { - SnackbarResult.ActionPerformed -> { - shouldShowPermissionRationale = false - storagePermissionLauncher.launch(permission) - } - - SnackbarResult.Dismissed -> { - shouldShowPermissionRationale = false - } - } - } - } - } - - if (shouldDirectUserToApplicationSettings) { - Intent( - Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.fromParts("package", context.packageName, null), - ).also { - context.startActivity(it) - } - } - } -} - -@Preview(showBackground = true) -@Composable -private fun EmptyKyc2FormPreview() { - MifosTheme { - Kyc2Form( - modifier = Modifier, - uploadData = { _, _ -> }, - ) - } -} - -@Preview(showBackground = true) -@Composable -private fun Kyc2FormPreviewWithLoading() { - MifosTheme { - KYCLevel2Screen( - uiState = KYCLevel2UiState.Loading, - uploadData = { _, _ -> }, - onSuccessKyc2 = {}, - ) - } -} - -@Preview(showBackground = true) -@Composable -private fun Kyc2FormPreviewWithError() { - MifosTheme { - KYCLevel2Screen( - uiState = KYCLevel2UiState.Error, - uploadData = { _, _ -> }, - onSuccessKyc2 = {}, - ) - } -} - -@Preview(showBackground = true) -@Composable -private fun Kyc2FormPreviewWithSuccess() { - MifosTheme { - KYCLevel2Screen( - uiState = KYCLevel2UiState.Success, - uploadData = { _, _ -> }, - onSuccessKyc2 = {}, - ) - } -} diff --git a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel2ViewModel.kt b/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel2ViewModel.kt deleted file mode 100644 index 6452e6dc0..000000000 --- a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel2ViewModel.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md - */ -package org.mifospay.feature.kyc - -import android.net.Uri -import androidx.lifecycle.ViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.MultipartBody -import okhttp3.RequestBody.Companion.asRequestBody -import org.mifospay.core.common.Constants -import org.mifospay.core.data.base.UseCase -import org.mifospay.core.data.base.UseCaseHandler -import org.mifospay.core.data.domain.usecase.kyc.UploadKYCDocs -import org.mifospay.core.datastore.PreferencesHelper -import org.mifospay.feature.kyc.KYCLevel2UiState.Loading -import java.io.File - -class KYCLevel2ViewModel( - private val mUseCaseHandler: UseCaseHandler, - private val preferencesHelper: PreferencesHelper, - private val uploadKYCDocsUseCase: UploadKYCDocs, -) : ViewModel() { - - private val kycUiState = MutableStateFlow(Loading) - val kyc2uiState: StateFlow = kycUiState - - fun uploadKYCDocs(identityType: String, result: Uri) { - val file = result.path?.let { File(it) } - if (file != null) { - uploadKYCDocsUseCase.walletRequestValues = identityType.let { - UploadKYCDocs.RequestValues( - org.mifospay.core.data.util.Constants.ENTITY_TYPE_CLIENTS, - preferencesHelper.clientId, file.name, it, - getRequestFileBody(file), - ) - } - } - val requestValues = uploadKYCDocsUseCase.walletRequestValues - mUseCaseHandler.execute( - uploadKYCDocsUseCase, - requestValues, - object : UseCase.UseCaseCallback { - override fun onSuccess(response: UploadKYCDocs.ResponseValue) { - kycUiState.value = KYCLevel2UiState.Success - } - - override fun onError(message: String) { - kycUiState.value = KYCLevel2UiState.Error - } - }, - ) - } - - private fun getRequestFileBody(file: File): MultipartBody.Part { - // create RequestBody instance from file - val requestFile = file.asRequestBody(Constants.MULTIPART_FORM_DATA.toMediaTypeOrNull()) - - // MultipartBody.Part is used to send also the actual file name - return MultipartBody.Part.createFormData(Constants.FILE, file.name, requestFile) - } -} - -sealed interface KYCLevel2UiState { - data object Loading : KYCLevel2UiState - data object Success : KYCLevel2UiState - data object Error : KYCLevel2UiState -} diff --git a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel3Screen.kt b/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel3Screen.kt deleted file mode 100644 index 0f37d7b17..000000000 --- a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel3Screen.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md - */ -package org.mifospay.feature.kyc - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import org.koin.androidx.compose.koinViewModel -import org.mifospay.core.designsystem.component.MfOverlayLoadingWheel -import org.mifospay.core.designsystem.component.MifosButton -import org.mifospay.core.designsystem.component.MifosOutlinedTextField -import org.mifospay.core.designsystem.theme.MifosTheme -import org.mifospay.kyc.R - -@Composable -internal fun KYCLevel3Screen( - modifier: Modifier = Modifier, - viewModel: KYCLevel3ViewModel = koinViewModel(), -) { - val kyc3uiState by viewModel.kyc3uiState.collectAsStateWithLifecycle() - - KYCLevel3Screen( - uiState = kyc3uiState, - modifier = modifier, - ) -} - -@Composable -private fun KYCLevel3Screen( - uiState: KYCLevel3UiState, - modifier: Modifier = Modifier, -) { - Kyc3Form(modifier = modifier) - - when (uiState) { - KYCLevel3UiState.Loading -> { - MfOverlayLoadingWheel(contentDesc = stringResource(id = R.string.feature_kyc_submitting)) - } - - KYCLevel3UiState.Error -> { - // Todo : Implement Error state - } - - KYCLevel3UiState.Success -> { - // Todo : Implement Success state - } - } -} - -@Composable -private fun Kyc3Form( - modifier: Modifier = Modifier, -) { - var panIdValue by rememberSaveable { mutableStateOf("") } - - Column( - modifier = modifier - .fillMaxSize() - .padding(20.dp), - verticalArrangement = Arrangement.spacedBy(10.dp), - ) { - MifosOutlinedTextField( - label = R.string.feature_kyc_pan_id, - value = panIdValue, - onValueChange = { - panIdValue = it - }, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), - ) - - MifosButton( - onClick = {}, - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(16.dp), - ) { - Text(stringResource(R.string.feature_kyc_submit)) - } - } -} - -@Preview(showBackground = true) -@Composable -private fun KYCLevel3ScreenPreview() { - MifosTheme { - Kyc3Form(modifier = Modifier) - } -} diff --git a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel3ViewModel.kt b/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel3ViewModel.kt deleted file mode 100644 index bddd96f58..000000000 --- a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel3ViewModel.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md - */ -package org.mifospay.feature.kyc - -import androidx.lifecycle.ViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import org.mifospay.core.data.base.UseCaseHandler -import org.mifospay.core.data.repository.local.LocalRepository -import org.mifospay.feature.kyc.KYCLevel3UiState.Loading - -@Suppress("UnusedPrivateProperty") -class KYCLevel3ViewModel( - private val mUseCaseHandler: UseCaseHandler, - private val mLocalRepository: LocalRepository, -) : ViewModel() { - private val kycUiState = MutableStateFlow(Loading) - val kyc3uiState: StateFlow = kycUiState - - // Todo: Implement KYCLevel3ViewModel flow -} - -sealed interface KYCLevel3UiState { - data object Loading : KYCLevel3UiState - data object Success : KYCLevel3UiState - data object Error : KYCLevel3UiState -} diff --git a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/di/KYCModule.kt b/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/di/KYCModule.kt deleted file mode 100644 index ba1a6d1aa..000000000 --- a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/di/KYCModule.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md - */ -package org.mifospay.feature.kyc.di - -import org.koin.core.module.dsl.viewModel -import org.koin.dsl.module -import org.mifospay.feature.kyc.KYCDescriptionViewModel -import org.mifospay.feature.kyc.KYCLevel1ViewModel -import org.mifospay.feature.kyc.KYCLevel2ViewModel -import org.mifospay.feature.kyc.KYCLevel3ViewModel - -val KYCModule = module { - viewModel { - KYCDescriptionViewModel( - mUseCaseHandler = get(), - mLocalRepository = get(), - fetchKYCLevel1DetailsUseCase = get(), - ) - } - - viewModel { - KYCLevel1ViewModel( - mUseCaseHandler = get(), - mLocalRepository = get(), - uploadKYCLevel1DetailsUseCase = get(), - ) - } - - viewModel { - KYCLevel2ViewModel( - mUseCaseHandler = get(), - preferencesHelper = get(), - uploadKYCDocsUseCase = get(), - ) - } - - viewModel { - KYCLevel3ViewModel( - mUseCaseHandler = get(), - mLocalRepository = get(), - ) - } -} diff --git a/feature/kyc/src/main/res/values/colors.xml b/feature/kyc/src/main/res/values/colors.xml deleted file mode 100644 index 7f718a9d3..000000000 --- a/feature/kyc/src/main/res/values/colors.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - #000000 - #8A000000 - #DE000000 - #FFF700 - @color/feature_kyc_colorBlack87 - @color/feature_kyc_colorBlack54 - \ No newline at end of file diff --git a/feature/kyc/src/main/res/values/dimens.xml b/feature/kyc/src/main/res/values/dimens.xml deleted file mode 100644 index 108b4b615..000000000 --- a/feature/kyc/src/main/res/values/dimens.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - 40dp - 15sp - 70dp - 100dp - 50dp - 20dp - 13sp - 64dp - 20sp - 24dp - 16sp - 14sp - - \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c4539c725..42c7bc944 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -79,7 +79,7 @@ rxandroidVersion = "1.1.0" rxjavaVersion = "1.3.8" sandwichVersion = "2.0.9" secrets = "2.0.1" -sheets_compose_dialogs_core = "1.3.0" +composeDialog = "1.3.0" spotlessVersion = "6.25.0" targetSdk = "34" truth = "1.4.4" @@ -301,8 +301,8 @@ sandwich-ktorfit = { module = "com.github.skydoves:sandwich-ktorfit", version.re squareup-okio = { group = "com.squareup.okio", name = "okio", version.ref = "okioVersion" } -sheets-compose-dialogs-calender = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "calendar", version.ref = "sheets_compose_dialogs_core" } -sheets-compose-dialogs-core = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "core", version.ref = "sheets_compose_dialogs_core" } +sheets-compose-dialogs-calender = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "calendar", version.ref = "composeDialog" } +sheets-compose-dialogs-core = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "core", version.ref = "composeDialog" } spotless-gradle = { group = "com.diffplug.spotless", name = "spotless-plugin-gradle", version.ref = "spotlessVersion" } diff --git a/mifospay-android/dependencies/prodReleaseRuntimeClasspath.tree.txt b/mifospay-android/dependencies/prodReleaseRuntimeClasspath.tree.txt index 1bc711a45..bfb7e695b 100644 --- a/mifospay-android/dependencies/prodReleaseRuntimeClasspath.tree.txt +++ b/mifospay-android/dependencies/prodReleaseRuntimeClasspath.tree.txt @@ -2257,6 +2257,39 @@ | | +--- org.jetbrains.compose.components:components-resources:1.7.0-rc01 (*) | | +--- org.jetbrains.compose.components:components-ui-tooling-preview:1.7.0-rc01 (*) | | \--- org.jetbrains.kotlin:kotlin-parcelize-runtime:2.0.20 (*) +| +--- project :feature:kyc +| | +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.6 (*) +| | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6 (*) +| | +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| | +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) +| | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| | +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| | +--- project :core:ui (*) +| | +--- project :core:designsystem (*) +| | +--- project :core:data (*) +| | +--- io.insert-koin:koin-compose:1.2.0-Beta4 -> 4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-compose-viewmodel:1.2.0-Beta4 -> 4.0.0-RC2 (*) +| | +--- org.jetbrains.compose.runtime:runtime:1.7.0-rc01 (*) +| | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.8.2 (*) +| | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.8.2 -> 2.8.3-rc01 (*) +| | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.2 (*) +| | +--- org.jetbrains.androidx.savedstate:savedstate:1.2.2 (*) +| | +--- org.jetbrains.androidx.core:core-bundle:1.0.1 (*) +| | +--- org.jetbrains.androidx.navigation:navigation-compose:2.8.0-alpha10 (*) +| | +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.8 (*) +| | +--- org.jetbrains.compose.ui:ui:1.7.0-rc01 (*) +| | +--- org.jetbrains.compose.foundation:foundation:1.7.0-rc01 (*) +| | +--- org.jetbrains.compose.material3:material3:1.7.0-rc01 (*) +| | +--- org.jetbrains.compose.components:components-resources:1.7.0-rc01 (*) +| | +--- org.jetbrains.compose.components:components-ui-tooling-preview:1.7.0-rc01 (*) +| | +--- io.github.vinceglb:filekit-compose:0.8.7 (*) +| | +--- io.coil-kt.coil3:coil-compose-core:3.0.0-alpha10 (*) +| | \--- org.jetbrains.kotlin:kotlin-parcelize-runtime:2.0.20 (*) | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) | +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) | +--- project :core:ui (*) diff --git a/mifospay-android/dependencies/prodReleaseRuntimeClasspath.txt b/mifospay-android/dependencies/prodReleaseRuntimeClasspath.txt index f9b6343f9..f2419058d 100644 --- a/mifospay-android/dependencies/prodReleaseRuntimeClasspath.txt +++ b/mifospay-android/dependencies/prodReleaseRuntimeClasspath.txt @@ -16,6 +16,7 @@ :feature:history :feature:home :feature:invoices +:feature:kyc :feature:payments :feature:profile :feature:settings diff --git a/mifospay-shared/build.gradle.kts b/mifospay-shared/build.gradle.kts index 5a03e1914..8c6861c31 100644 --- a/mifospay-shared/build.gradle.kts +++ b/mifospay-shared/build.gradle.kts @@ -46,6 +46,7 @@ kotlin { api(projects.feature.finance) api(projects.feature.accounts) api(projects.feature.invoices) + api(projects.feature.kyc) } desktopMain.dependencies { diff --git a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/di/KoinModules.kt b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/di/KoinModules.kt index 68daef32b..532f04335 100644 --- a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/di/KoinModules.kt +++ b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/di/KoinModules.kt @@ -28,6 +28,7 @@ import org.mifospay.feature.faq.di.FaqModule import org.mifospay.feature.history.di.HistoryModule import org.mifospay.feature.home.di.HomeModule import org.mifospay.feature.invoices.di.InvoicesModule +import org.mifospay.feature.kyc.di.KYCModule import org.mifospay.feature.payments.di.PaymentsModule import org.mifospay.feature.profile.di.ProfileModule import org.mifospay.feature.settings.di.SettingsModule @@ -64,6 +65,7 @@ object KoinModules { PaymentsModule, AccountsModule, InvoicesModule, + KYCModule, ) } private val LibraryModule = module { diff --git a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt index edb2ccbab..53278639a 100644 --- a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt +++ b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.navigation.compose.NavHost +import androidx.navigation.navOptions import org.mifospay.core.ui.utility.TabContent import org.mifospay.feature.accounts.AccountsScreen import org.mifospay.feature.accounts.beneficiary.addEditBeneficiaryScreen @@ -29,6 +30,7 @@ import org.mifospay.feature.editpassword.navigation.navigateToEditPassword import org.mifospay.feature.faq.navigation.faqScreen import org.mifospay.feature.faq.navigation.navigateToFAQ import org.mifospay.feature.finance.FinanceScreenContents +import org.mifospay.feature.finance.navigation.FINANCE_ROUTE import org.mifospay.feature.finance.navigation.financeScreen import org.mifospay.feature.history.HistoryScreen import org.mifospay.feature.history.navigation.historyNavigation @@ -41,6 +43,13 @@ import org.mifospay.feature.home.navigation.homeScreen import org.mifospay.feature.invoices.InvoiceScreen import org.mifospay.feature.invoices.navigation.invoiceDetailScreen import org.mifospay.feature.invoices.navigation.navigateToInvoiceDetail +import org.mifospay.feature.kyc.KYCScreen +import org.mifospay.feature.kyc.navigation.kycLevel1Screen +import org.mifospay.feature.kyc.navigation.kycLevel2Screen +import org.mifospay.feature.kyc.navigation.kycLevel3Screen +import org.mifospay.feature.kyc.navigation.navigateToKYCLevel1 +import org.mifospay.feature.kyc.navigation.navigateToKYCLevel2 +import org.mifospay.feature.kyc.navigation.navigateToKYCLevel3 import org.mifospay.feature.payments.PaymentsScreenContents import org.mifospay.feature.payments.RequestScreen import org.mifospay.feature.payments.paymentsScreen @@ -95,9 +104,11 @@ internal fun MifosNavHost( } }, TabContent(FinanceScreenContents.KYC.name) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text("KYC Screen || TODO", modifier = Modifier.align(Alignment.Center)) - } + KYCScreen( + onLevel1Clicked = navController::navigateToKYCLevel1, + onLevel2Clicked = navController::navigateToKYCLevel2, + onLevel3Clicked = navController::navigateToKYCLevel3, + ) }, ) @@ -171,5 +182,33 @@ internal fun MifosNavHost( invoiceDetailScreen( onNavigateBack = navController::navigateUp, ) + + kycLevel1Screen( + navigateBack = navController::navigateUp, + navigateToKycLevel2 = { + navController.navigateToKYCLevel2( + navOptions { + restoreState = true + popUpTo(FINANCE_ROUTE) + }, + ) + }, + ) + + kycLevel2Screen( + navigateBack = navController::navigateUp, + navigateToLevel3 = { + navController.navigateToKYCLevel3( + navOptions { + restoreState = true + popUpTo(FINANCE_ROUTE) + }, + ) + }, + ) + + kycLevel3Screen( + navigateBack = navController::navigateUp, + ) } }