diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/di/RepositoryModule.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/di/RepositoryModule.kt index 80064d7ed..aa48af381 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/di/RepositoryModule.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/di/RepositoryModule.kt @@ -52,6 +52,7 @@ import org.mifospay.core.data.util.NetworkMonitor import org.mifospay.core.data.util.TimeZoneMonitor private val ioDispatcher = named(MifosDispatchers.IO.name) +private val unconfined = named(MifosDispatchers.Unconfined.name) val RepositoryModule = module { single { AccountRepositoryImpl(get(), get(ioDispatcher)) } @@ -59,7 +60,14 @@ val RepositoryModule = module { AuthenticationRepositoryImpl(get(), get(ioDispatcher)) } single { BeneficiaryRepositoryImpl(get(), get(ioDispatcher)) } - single { ClientRepositoryImpl(get(), get(), get(ioDispatcher)) } + single { + ClientRepositoryImpl( + apiManager = get(), + fineractApiManager = get(), + ioDispatcher = get(ioDispatcher), + unconfinedDispatcher = get(unconfined), + ) + } single { DocumentRepositoryImpl(get(), get(ioDispatcher)) } single { InvoiceRepositoryImpl(get(), get(ioDispatcher)) } single { KycLevelRepositoryImpl(get(), get(ioDispatcher)) } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/mapper/ClientDetailsMapper.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/mapper/ClientDetailsMapper.kt index ed597cbbb..cffeca119 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/mapper/ClientDetailsMapper.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/mapper/ClientDetailsMapper.kt @@ -11,20 +11,38 @@ package org.mifospay.core.data.mapper import org.mifospay.core.model.client.Client import org.mifospay.core.model.client.ClientAddress +import org.mifospay.core.model.client.ClientStatus +import org.mifospay.core.model.client.ClientTimeline import org.mifospay.core.model.client.NewClient +import org.mifospay.core.model.client.UpdatedClient import org.mifospay.core.network.model.entity.Page import org.mifospay.core.network.model.entity.client.Address import org.mifospay.core.network.model.entity.client.ClientEntity +import org.mifospay.core.network.model.entity.client.ClientTimelineEntity import org.mifospay.core.network.model.entity.client.NewClientEntity +import org.mifospay.core.network.model.entity.client.Status +import org.mifospay.core.network.model.entity.client.UpdateClientEntity fun ClientEntity.toModel(): Client { return Client( - name = this.displayName ?: "", - clientId = this.id.toLong(), - externalId = this.externalId, - mobileNo = this.mobileNo, - displayName = this.displayName ?: "", - image = this.imageId.toString(), + id = id ?: 0, + accountNo = accountNo ?: "", + externalId = externalId ?: "", + active = active, + activationDate = activationDate, + firstname = firstname ?: "", + lastname = lastname ?: "", + displayName = displayName ?: "", + mobileNo = mobileNo ?: "", + emailAddress = emailAddress ?: "", + dateOfBirth = dateOfBirth, + isStaff = isStaff ?: false, + officeId = officeId ?: 0, + officeName = officeName ?: "", + savingsProductName = savingsProductName ?: "", + status = status?.toModel() ?: ClientStatus(), + timeline = timeline?.toModel() ?: ClientTimeline(), + legalForm = legalForm?.toModel() ?: ClientStatus(), ) } @@ -58,3 +76,31 @@ fun ClientAddress.toEntity(): Address { addressTypeId = addressTypeId, ) } + +fun Status.toModel(): ClientStatus { + return ClientStatus( + id = id ?: 0, + code = code ?: "", + value = value ?: "", + ) +} + +fun ClientTimelineEntity.toModel(): ClientTimeline { + return ClientTimeline( + submittedOnDate = submittedOnDate, + activatedOnDate = activatedOnDate, + activatedByUsername = activatedByUsername, + activatedByFirstname = activatedByFirstname, + activatedByLastname = activatedByLastname, + ) +} + +fun UpdatedClient.toEntity(): UpdateClientEntity { + return UpdateClientEntity( + firstname = firstname, + lastname = lastname, + externalId = externalId, + mobileNo = mobileNo, + emailAddress = emailAddress, + ) +} diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/ClientRepository.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/ClientRepository.kt index 13112b3f4..829e928ea 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/ClientRepository.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/ClientRepository.kt @@ -10,24 +10,28 @@ package org.mifospay.core.data.repository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow import org.mifospay.core.common.Result import org.mifospay.core.model.account.Account import org.mifospay.core.model.client.Client import org.mifospay.core.model.client.NewClient +import org.mifospay.core.model.client.UpdatedClient import org.mifospay.core.network.model.entity.Page import org.mifospay.core.network.model.entity.client.ClientAccountsEntity interface ClientRepository { + fun getClientInfo(clientId: Long): StateFlow> + suspend fun getClients(): Flow>> suspend fun getClient(clientId: Long): Result - suspend fun updateClient(clientId: Long, client: Client): Flow> + suspend fun updateClient(clientId: Long, client: UpdatedClient): Result - suspend fun getClientImage(clientId: Long): Flow> + fun getClientImage(clientId: Long): Flow> - suspend fun updateClientImage(clientId: Long, image: String): Flow> + suspend fun updateClientImage(clientId: Long, image: String): Result suspend fun getClientAccounts(clientId: Long): Flow> diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/ClientRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/ClientRepositoryImpl.kt index 1b935e27d..8ef867da7 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/ClientRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/ClientRepositoryImpl.kt @@ -10,9 +10,14 @@ package org.mifospay.core.data.repositoryImp import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext import org.mifospay.core.common.Result import org.mifospay.core.common.asResult @@ -23,6 +28,7 @@ import org.mifospay.core.data.repository.ClientRepository import org.mifospay.core.model.account.Account import org.mifospay.core.model.client.Client import org.mifospay.core.model.client.NewClient +import org.mifospay.core.model.client.UpdatedClient import org.mifospay.core.network.FineractApiManager import org.mifospay.core.network.SelfServiceApiManager import org.mifospay.core.network.model.entity.Page @@ -32,34 +38,67 @@ class ClientRepositoryImpl( private val apiManager: SelfServiceApiManager, private val fineractApiManager: FineractApiManager, private val ioDispatcher: CoroutineDispatcher, + unconfinedDispatcher: CoroutineDispatcher, ) : ClientRepository { + private val coroutineScope = CoroutineScope(unconfinedDispatcher) + override suspend fun getClients(): Flow>> { return apiManager.clientsApi.clients().map { it.toModel() }.asResult().flowOn(ioDispatcher) } + override fun getClientInfo(clientId: Long): StateFlow> { + return fineractApiManager.clientsApi + .getClient(clientId) + .catch { Result.Error(it) } + .map { Result.Success(it.toModel()) } + .stateIn( + scope = coroutineScope, + started = SharingStarted.Eagerly, + initialValue = Result.Loading, + ) + } + override suspend fun getClient(clientId: Long): Result { return try { - val result = apiManager.clientsApi.getClientForId(clientId) + val result = fineractApiManager.clientsApi.getClientForId(clientId) Result.Success(result.toModel()) } catch (e: Exception) { Result.Error(e) } } - override suspend fun updateClient(clientId: Long, client: Client): Flow> { - return apiManager.clientsApi - .updateClient(clientId, client) - .asResult().flowOn(ioDispatcher) + override suspend fun updateClient(clientId: Long, client: UpdatedClient): Result { + return try { + withContext(ioDispatcher) { + fineractApiManager.clientsApi.updateClient(clientId, client.toEntity()) + } + + Result.Success("Client updated successfully") + } catch (e: Exception) { + Result.Error(e) + } } - override suspend fun getClientImage(clientId: Long): Flow> { - return apiManager.clientsApi.getClientImage(clientId).asResult().flowOn(ioDispatcher) + override fun getClientImage(clientId: Long): Flow> { + return fineractApiManager.clientsApi + .getClientImage(clientId) + .catch { Result.Error(it) } + .map { Result.Success(it) } } - override suspend fun updateClientImage(clientId: Long, image: String): Flow> { - return apiManager.clientsApi - .updateClientImage(clientId, image) - .asResult().flowOn(ioDispatcher) + override suspend fun updateClientImage(clientId: Long, image: String): Result { + return try { + withContext(ioDispatcher) { + fineractApiManager.clientsApi.updateClientImage( + clientId = clientId, + typedFile = "data:image/png;base64,$image", + ) + } + + Result.Success("Client image updated successfully") + } catch (e: Exception) { + Result.Error(e) + } } override suspend fun getClientAccounts(clientId: Long): Flow> { diff --git a/core/datastore-proto/src/commonMain/kotlin/org/mifospay/core/datastore/proto/ClientPreferences.kt b/core/datastore-proto/src/commonMain/kotlin/org/mifospay/core/datastore/proto/ClientPreferences.kt index 4d5edcf1b..1bc5477f4 100644 --- a/core/datastore-proto/src/commonMain/kotlin/org/mifospay/core/datastore/proto/ClientPreferences.kt +++ b/core/datastore-proto/src/commonMain/kotlin/org/mifospay/core/datastore/proto/ClientPreferences.kt @@ -13,21 +13,39 @@ import kotlinx.serialization.Serializable @Serializable data class ClientPreferences( - val name: String, - val image: String, + val id: Long, + val accountNo: String, val externalId: String, - val clientId: Long, + val active: Boolean, + val activationDate: List, + val firstname: String, + val lastname: String, val displayName: String, val mobileNo: String, + val emailAddress: String, + val dateOfBirth: List, + val isStaff: Boolean, + val officeId: Long, + val officeName: String, + val savingsProductName: String, ) { companion object { val DEFAULT = ClientPreferences( - name = "", - image = "", + id = 0L, + accountNo = "", externalId = "", - clientId = 0, + active = false, + activationDate = emptyList(), + firstname = "", + lastname = "", displayName = "", mobileNo = "", + emailAddress = "", + dateOfBirth = emptyList(), + isStaff = false, + officeId = 0, + officeName = "", + savingsProductName = "", ) } } diff --git a/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/PreferencesMapper.kt b/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/PreferencesMapper.kt index 095f10293..4825e32fc 100644 --- a/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/PreferencesMapper.kt +++ b/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/PreferencesMapper.kt @@ -18,23 +18,41 @@ import org.mifospay.core.model.user.UserInfo fun ClientPreferences.toClientInfo(): Client { return Client( - name = name, - image = image, + id = id, + accountNo = accountNo, externalId = externalId, - clientId = clientId, + active = active, + activationDate = activationDate, + firstname = firstname, + lastname = lastname, displayName = displayName, mobileNo = mobileNo, + emailAddress = emailAddress, + dateOfBirth = dateOfBirth, + isStaff = isStaff, + officeId = officeId, + officeName = officeName, + savingsProductName = savingsProductName, ) } fun Client.toClientPreferences(): ClientPreferences { return ClientPreferences( - name = name, - image = image, + id = id, + accountNo = accountNo, externalId = externalId, - clientId = clientId, + active = active, + activationDate = activationDate, + firstname = firstname, + lastname = lastname, displayName = displayName, mobileNo = mobileNo, + emailAddress = emailAddress, + dateOfBirth = dateOfBirth, + isStaff = isStaff, + officeId = officeId, + officeName = officeName, + savingsProductName = savingsProductName, ) } diff --git a/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesDataSource.kt b/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesDataSource.kt index 7c4527b6e..ee53419fd 100644 --- a/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesDataSource.kt @@ -24,6 +24,7 @@ import kotlinx.serialization.ExperimentalSerializationApi import org.mifospay.core.datastore.proto.ClientPreferences import org.mifospay.core.datastore.proto.UserInfoPreferences import org.mifospay.core.model.client.Client +import org.mifospay.core.model.client.UpdatedClient import org.mifospay.core.model.user.UserInfo private const val USER_INFO_KEY = "userInfo" @@ -63,6 +64,8 @@ class UserPreferencesDataSource( val clientInfo = _clientInfo.map(ClientPreferences::toClientInfo) + val clientId = _clientInfo.map { it.id } + suspend fun updateClientInfo(client: Client) { withContext(dispatcher) { settings.putClientPreference(client.toClientPreferences()) @@ -77,6 +80,22 @@ class UserPreferencesDataSource( } } + suspend fun updateClientProfile(client: UpdatedClient) { + withContext(dispatcher) { + val updatedClient = _clientInfo.value.copy( + firstname = client.firstname, + lastname = client.lastname, + displayName = client.firstname + " " + client.lastname, + emailAddress = client.emailAddress, + mobileNo = client.mobileNo, + externalId = client.externalId, + ) + + settings.putClientPreference(updatedClient) + _clientInfo.value = updatedClient + } + } + suspend fun updateToken(token: String) { withContext(dispatcher) { settings.putUserInfoPreference( diff --git a/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesRepository.kt b/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesRepository.kt index 8c7d072d8..d9ec3c51f 100644 --- a/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesRepository.kt +++ b/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesRepository.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import org.mifospay.core.common.Result import org.mifospay.core.model.client.Client +import org.mifospay.core.model.client.UpdatedClient import org.mifospay.core.model.user.UserInfo interface UserPreferencesRepository { @@ -22,6 +23,8 @@ interface UserPreferencesRepository { val client: StateFlow + val clientId: StateFlow + val authToken: String? suspend fun updateToken(token: String): Result @@ -30,5 +33,7 @@ interface UserPreferencesRepository { suspend fun updateClientInfo(client: Client): Result + suspend fun updateClientProfile(client: UpdatedClient): Result + suspend fun logOut(): Unit } diff --git a/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesRepositoryImpl.kt b/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesRepositoryImpl.kt index 1ffb73f55..ccf476904 100644 --- a/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesRepositoryImpl.kt +++ b/core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/UserPreferencesRepositoryImpl.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.stateIn import org.mifospay.core.common.Result import org.mifospay.core.model.client.Client +import org.mifospay.core.model.client.UpdatedClient import org.mifospay.core.model.user.UserInfo class UserPreferencesRepositoryImpl( @@ -44,6 +45,13 @@ class UserPreferencesRepositoryImpl( started = SharingStarted.Eagerly, ) + override val clientId: StateFlow + get() = preferenceManager.clientId.stateIn( + scope = unconfinedScope, + initialValue = null, + started = SharingStarted.Eagerly, + ) + override val authToken: String? get() = preferenceManager.getAuthToken() @@ -67,6 +75,15 @@ class UserPreferencesRepositoryImpl( } } + override suspend fun updateClientProfile(client: UpdatedClient): Result { + return try { + val result = preferenceManager.updateClientProfile(client) + Result.Success(result) + } catch (e: Exception) { + Result.Error(e) + } + } + override suspend fun updateUserInfo(user: UserInfo): Result { return try { val result = preferenceManager.updateUserInfo(user) diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/client/Client.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/client/Client.kt index b5b3cfe9d..81a6b41fb 100644 --- a/core/model/src/commonMain/kotlin/org/mifospay/core/model/client/Client.kt +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/client/Client.kt @@ -14,10 +14,22 @@ import org.mifospay.core.common.Parcelize @Parcelize data class Client( - val name: String, - val image: String, + val id: Long, + val accountNo: String, val externalId: String, - val clientId: Long, + val active: Boolean, + val activationDate: List, + val firstname: String, + val lastname: String, val displayName: String, val mobileNo: String, + val emailAddress: String, + val dateOfBirth: List, + val isStaff: Boolean, + val officeId: Long, + val officeName: String, + val savingsProductName: String, + val timeline: ClientTimeline = ClientTimeline(), + val status: ClientStatus = ClientStatus(), + val legalForm: ClientStatus = ClientStatus(), ) : Parcelable diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/client/ClientStatus.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/client/ClientStatus.kt new file mode 100644 index 000000000..5477923ba --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/client/ClientStatus.kt @@ -0,0 +1,20 @@ +/* + * 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.core.model.client + +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize + +@Parcelize +data class ClientStatus( + val id: Int = 0, + val code: String = "", + val value: String = "", +) : Parcelable diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/client/ClientTimeline.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/client/ClientTimeline.kt new file mode 100644 index 000000000..5922c4a66 --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/client/ClientTimeline.kt @@ -0,0 +1,22 @@ +/* + * 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.core.model.client + +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize + +@Parcelize +data class ClientTimeline( + val submittedOnDate: List = emptyList(), + val activatedOnDate: List = emptyList(), + val activatedByUsername: String? = null, + val activatedByFirstname: String? = null, + val activatedByLastname: String? = null, +) : Parcelable diff --git a/core/model/src/commonMain/kotlin/org/mifospay/core/model/client/UpdatedClient.kt b/core/model/src/commonMain/kotlin/org/mifospay/core/model/client/UpdatedClient.kt new file mode 100644 index 000000000..d5237614b --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/mifospay/core/model/client/UpdatedClient.kt @@ -0,0 +1,22 @@ +/* + * 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.core.model.client + +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize + +@Parcelize +data class UpdatedClient( + val firstname: String, + val lastname: String, + val externalId: String, + val mobileNo: String, + val emailAddress: String, +) : Parcelable diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/client/ClientEntity.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/client/ClientEntity.kt index 4a74a4718..91e91b8d3 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/client/ClientEntity.kt +++ b/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/client/ClientEntity.kt @@ -9,30 +9,35 @@ */ package org.mifospay.core.network.model.entity.client -import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class ClientEntity( - val id: Int = 0, + val id: Long? = null, val accountNo: String? = null, + val externalId: String? = null, val status: Status? = null, - val active: Boolean? = null, - val activationDate: List = ArrayList(), - val dobDate: List = ArrayList(), + val active: Boolean = false, + val activationDate: List = emptyList(), val firstname: String? = null, - val middlename: String? = null, val lastname: String? = null, val displayName: String? = null, - val fullname: String? = null, - val officeId: Int = 0, + val mobileNo: String? = null, + val emailAddress: String? = null, + val dateOfBirth: List = emptyList(), + val isStaff: Boolean? = null, + val officeId: Long? = null, val officeName: String? = null, - val staffId: Int? = null, - val staffName: String? = null, - val timeline: org.mifospay.core.network.model.entity.Timeline? = null, - val imageId: Int = 0, - @SerialName("imagePresent") - val isImagePresent: Boolean = false, - val externalId: String = "", - val mobileNo: String = "", + val timeline: ClientTimelineEntity? = null, + val savingsProductName: String? = null, + val legalForm: Status? = null, +) + +@Serializable +data class ClientTimelineEntity( + val submittedOnDate: List = emptyList(), + val activatedOnDate: List = emptyList(), + val activatedByUsername: String? = null, + val activatedByFirstname: String? = null, + val activatedByLastname: String? = null, ) diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/client/UpdateClientEntityMobile.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/client/UpdateClientEntity.kt similarity index 62% rename from core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/client/UpdateClientEntityMobile.kt rename to core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/client/UpdateClientEntity.kt index 651a2962d..1ff787c0c 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/client/UpdateClientEntityMobile.kt +++ b/core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/client/UpdateClientEntity.kt @@ -9,4 +9,13 @@ */ package org.mifospay.core.network.model.entity.client -data class UpdateClientEntityMobile(val mobileNo: String) +import kotlinx.serialization.Serializable + +@Serializable +data class UpdateClientEntity( + val firstname: String, + val lastname: String, + val externalId: String, + val mobileNo: String, + val emailAddress: String, +) diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/ClientService.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/ClientService.kt index 416cfb737..0ad7e090a 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/ClientService.kt +++ b/core/network/src/commonMain/kotlin/org/mifospay/core/network/services/ClientService.kt @@ -23,6 +23,7 @@ import org.mifospay.core.network.model.entity.Page import org.mifospay.core.network.model.entity.client.ClientAccountsEntity import org.mifospay.core.network.model.entity.client.ClientEntity import org.mifospay.core.network.model.entity.client.NewClientEntity +import org.mifospay.core.network.model.entity.client.UpdateClientEntity import org.mifospay.core.network.utils.ApiEndPoints interface ClientService { @@ -32,21 +33,24 @@ interface ClientService { @GET(ApiEndPoints.CLIENTS + "/{clientId}") suspend fun getClientForId(@Path("clientId") clientId: Long): ClientEntity + @GET(ApiEndPoints.CLIENTS + "/{clientId}") + fun getClient(@Path("clientId") clientId: Long): Flow + @PUT(ApiEndPoints.CLIENTS + "/{clientId}") suspend fun updateClient( @Path("clientId") clientId: Long, - @Body payload: Any, - ): Flow + @Body payload: UpdateClientEntity, + ): Unit @GET(ApiEndPoints.CLIENTS + "/{clientId}/images") @Headers("Accept: text/plain") - suspend fun getClientImage(@Path("clientId") clientId: Long): Flow + fun getClientImage(@Path("clientId") clientId: Long): Flow @PUT(ApiEndPoints.CLIENTS + "/{clientId}/images") suspend fun updateClientImage( @Path("clientId") clientId: Long, - @Body typedFile: String?, - ): Flow + @Body typedFile: String, + ): Unit @GET(ApiEndPoints.CLIENTS + "/{clientId}/accounts") suspend fun getClientAccounts(@Path("clientId") clientId: Long): Flow diff --git a/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/ProfileConcentricImage.kt b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/ProfileConcentricImage.kt deleted file mode 100644 index 0ed47bb10..000000000 --- a/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/ProfileConcentricImage.kt +++ /dev/null @@ -1,56 +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.core.ui - -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.unit.dp - -@Composable -fun ProfileImage( - modifier: Modifier = Modifier, - bitmap: ImageBitmap? = null, -) { - Row( - modifier = modifier - .fillMaxWidth() - .padding(top = 64.dp, bottom = 12.dp), - horizontalArrangement = Arrangement.Center, - ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .size(200.dp) - .border( - width = 2.dp, - color = Color.Gray, - shape = CircleShape, - ), - ) { - MifosUserImage( - modifier = Modifier - .size(200.dp) - .padding(10.dp), - bitmap = bitmap, - ) - } - } -} diff --git a/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/HomeScreen.kt b/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/HomeScreen.kt index 3454f5c6e..924d1fe95 100644 --- a/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/HomeScreen.kt +++ b/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/HomeScreen.kt @@ -84,6 +84,8 @@ import org.mifospay.core.designsystem.component.scrollbar.scrollbarState import org.mifospay.core.designsystem.theme.NewUi import org.mifospay.core.model.account.Account import org.mifospay.core.model.client.Client +import org.mifospay.core.model.client.ClientStatus +import org.mifospay.core.model.client.ClientTimeline import org.mifospay.core.model.savingsaccount.Currency import org.mifospay.core.model.savingsaccount.Transaction import org.mifospay.core.model.savingsaccount.TransactionType @@ -201,7 +203,7 @@ private fun HomeScreen( item { MifosWalletCard( account = account, - clientName = client.name, + clientName = client.displayName, ) } @@ -554,12 +556,38 @@ private fun HomeScreenPreview() { productId = 1223, ), client = Client( - name = "Morris Dillon", - image = "sale", - externalId = "condimentum", - clientId = 2028, - displayName = "Carlo Wyatt", - mobileNo = "sed", + id = 8858, + accountNo = "dignissim", + externalId = "sonet", + active = false, + activationDate = listOf(), + firstname = "Hollis Tyler", + lastname = "Lindsay Salazar", + displayName = "Janell Howell", + mobileNo = "principes", + emailAddress = "vicky.dominguez@example.com", + dateOfBirth = listOf(), + isStaff = false, + officeId = 2628, + officeName = "Enrique Dickson", + savingsProductName = "Lamont Brady", + timeline = ClientTimeline( + submittedOnDate = listOf(), + activatedOnDate = listOf(), + activatedByUsername = null, + activatedByFirstname = null, + activatedByLastname = null, + ), + status = ClientStatus( + id = 6242, + code = "possim", + value = "accommodare", + ), + legalForm = ClientStatus( + id = 2235, + code = "unum", + value = "laudem", + ), ), transactions = List(25) { index -> Transaction( diff --git a/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/HomeViewModel.kt b/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/HomeViewModel.kt index 3b11e1ad5..5207805bc 100644 --- a/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/HomeViewModel.kt +++ b/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/HomeViewModel.kt @@ -45,6 +45,14 @@ class HomeViewModel( ) { init { trySendAction(HomeAction.Internal.LoadAccounts) + + preferencesRepository.client.onEach { data -> + data?.let { client -> + mutableStateFlow.update { + it.copy(client = client) + } + } + }.launchIn(viewModelScope) } override fun handleAction(action: HomeAction) { @@ -80,7 +88,7 @@ class HomeViewModel( sendEvent(HomeEvent.NavigateBack) } - is HomeAction.Internal.LoadAccounts -> loadAccounts(state.client.clientId) + is HomeAction.Internal.LoadAccounts -> loadAccounts(state.client.id) is SelectAccount -> selectAccount(action.accountId) } diff --git a/feature/profile/build.gradle.kts b/feature/profile/build.gradle.kts index 637ee6226..c8316537d 100644 --- a/feature/profile/build.gradle.kts +++ b/feature/profile/build.gradle.kts @@ -9,19 +9,39 @@ */ 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.feature.profile" - buildFeatures { - buildConfig = true + + defaultConfig { + consumerProguardFiles("consumer-rules.pro") } } -dependencies { - implementation(projects.libs.countryCodePicker) - implementation(libs.squareup.okhttp) - implementation(libs.coil.kt.compose) +kotlin { + sourceSets { + commonMain.dependencies { + implementation(compose.ui) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.components.resources) + implementation(compose.components.uiToolingPreview) + implementation(libs.coil.kt.compose) + implementation(libs.filekit.core) + implementation(libs.filekit.compose) + } + } +} + +compose.desktop { + application { + nativeDistributions { + linux { + modules("jdk.security.auth") + } + } + } } \ No newline at end of file diff --git a/feature/profile/consumer-rules.pro b/feature/profile/consumer-rules.pro index e69de29bb..9444e2c67 100644 --- a/feature/profile/consumer-rules.pro +++ b/feature/profile/consumer-rules.pro @@ -0,0 +1,2 @@ +-keep class com.sun.jna.** { *; } +-keep class * implements com.sun.jna.** { *; } \ No newline at end of file diff --git a/feature/profile/proguard-rules.pro b/feature/profile/proguard-rules.pro deleted file mode 100644 index 481bb4348..000000000 --- a/feature/profile/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/profile/src/main/AndroidManifest.xml b/feature/profile/src/androidMain/AndroidManifest.xml similarity index 100% rename from feature/profile/src/main/AndroidManifest.xml rename to feature/profile/src/androidMain/AndroidManifest.xml diff --git a/feature/profile/src/commonMain/composeResources/drawable/placeholder.png b/feature/profile/src/commonMain/composeResources/drawable/placeholder.png new file mode 100644 index 000000000..4f7005f77 Binary files /dev/null and b/feature/profile/src/commonMain/composeResources/drawable/placeholder.png differ diff --git a/feature/profile/src/main/res/values/strings.xml b/feature/profile/src/commonMain/composeResources/values/strings.xml similarity index 90% rename from feature/profile/src/main/res/values/strings.xml rename to feature/profile/src/commonMain/composeResources/values/strings.xml index 0b29c63b8..f89cd59c7 100644 --- a/feature/profile/src/main/res/values/strings.xml +++ b/feature/profile/src/commonMain/composeResources/values/strings.xml @@ -25,10 +25,13 @@ Proceed Dismiss Username + First Name + Last Name Phone Number Failed To Save Changes Save Click profile picture Pick profile picture from device Remove profile picture + Updated Successfully \ No newline at end of file diff --git a/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/ProfileScreen.kt b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/ProfileScreen.kt new file mode 100644 index 000000000..1d3f0ef19 --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/ProfileScreen.kt @@ -0,0 +1,218 @@ +/* + * 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.profile + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +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.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults +import androidx.compose.material3.pulltorefresh.pullToRefresh +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +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.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import mobile_wallet.feature.profile.generated.resources.Res +import mobile_wallet.feature.profile.generated.resources.feature_profile_link_bank_account +import mobile_wallet.feature.profile.generated.resources.feature_profile_personal_qr_code +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.MifosOverlayLoadingWheel +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.ui.ErrorScreenContent +import org.mifospay.core.ui.utils.EventsEffect +import org.mifospay.feature.profile.components.ProfileDetailsCard +import org.mifospay.feature.profile.components.ProfileImage + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun ProfileScreen( + onEditProfile: () -> Unit, + onLinkBackAccount: () -> Unit, + modifier: Modifier = Modifier, + viewModel: ProfileViewModel = koinViewModel(), +) { + val pullToRefreshState = rememberPullToRefreshState() + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + + EventsEffect(viewModel) { event -> + when (event) { + ProfileEvent.OnEditProfile -> onEditProfile.invoke() + ProfileEvent.OnLinkBankAccount -> onLinkBackAccount.invoke() + ProfileEvent.ShowQRCode -> { + // TODO: Show QR code + } + } + } + + Box( + modifier = modifier + .fillMaxSize() + .pullToRefresh( + isRefreshing = state.viewState is ProfileState.ViewState.Loading, + state = pullToRefreshState, + onRefresh = remember(viewModel) { + { viewModel.trySendAction(ProfileAction.RefreshProfile) } + }, + ), + contentAlignment = Alignment.Center, + ) { + ProfileScreenContent( + state = state, + onAction = remember(viewModel) { + { action -> viewModel.trySendAction(action) } + }, + modifier = Modifier.align(Alignment.Center), + ) + + ProfileDialogs( + dialogState = state.dialogState, + onDismissRequest = remember(viewModel) { + { viewModel.trySendAction(ProfileAction.DismissErrorDialog) } + }, + ) + + PullToRefreshDefaults.Indicator( + modifier = Modifier + .align(Alignment.TopCenter), + isRefreshing = state.viewState is ProfileState.ViewState.Loading, + state = pullToRefreshState, + ) + } +} + +@Composable +internal fun ProfileScreenContent( + state: ProfileState, + onAction: (ProfileAction) -> Unit, + modifier: Modifier = Modifier, +) { + when (state.viewState) { + is ProfileState.ViewState.Loading -> { + MifosOverlayLoadingWheel( + contentDesc = "ProfileLoading", + modifier = modifier, + ) + } + + is ProfileState.ViewState.Error -> { + ErrorScreenContent( + onClickRetry = { onAction(ProfileAction.RefreshProfile) }, + modifier = modifier, + ) + } + + is ProfileState.ViewState.Success -> { + ProfileScreenContent( + state = state.viewState, + clientImage = state.clientImage, + onAction = onAction, + modifier = modifier, + ) + } + } +} + +@Composable +private fun ProfileScreenContent( + state: ProfileState.ViewState.Success, + clientImage: String?, + modifier: Modifier = Modifier, + onAction: (ProfileAction) -> Unit, +) { + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + ProfileImage(bitmap = clientImage) + + ProfileDetailsCard( + client = state.client, + modifier = Modifier.fillMaxWidth(), + ) + + MifosButton( + modifier = Modifier + .fillMaxWidth() + .height(54.dp), + color = MaterialTheme.colorScheme.primary, + text = { Text(text = stringResource(Res.string.feature_profile_personal_qr_code)) }, + onClick = { + onAction(ProfileAction.ShowPersonalQRCode) + }, + leadingIcon = { + Icon( + imageVector = MifosIcons.QrCode, + contentDescription = "Personal QR Code", + ) + }, + ) + + MifosButton( + modifier = Modifier + .fillMaxWidth() + .height(55.dp), + color = MaterialTheme.colorScheme.primary, + text = { Text(text = stringResource(Res.string.feature_profile_link_bank_account)) }, + onClick = { + onAction(ProfileAction.NavigateToLinkBankAccount) + }, + leadingIcon = { + Icon(imageVector = MifosIcons.AttachMoney, contentDescription = "") + }, + ) + + Spacer(modifier = Modifier.height(1.dp)) + } +} + +@Composable +private fun ProfileDialogs( + dialogState: ProfileState.DialogState?, + onDismissRequest: () -> Unit, +) { + when (dialogState) { + is ProfileState.DialogState.Error -> MifosBasicDialog( + visibilityState = BasicDialogState.Shown( + message = dialogState.message, + ), + onDismissRequest = onDismissRequest, + ) + + is ProfileState.DialogState.Loading -> MifosLoadingDialog( + visibilityState = LoadingDialogState.Shown, + ) + + null -> Unit + } +} diff --git a/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/ProfileViewModel.kt b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/ProfileViewModel.kt new file mode 100644 index 000000000..8d1829a4b --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/ProfileViewModel.kt @@ -0,0 +1,188 @@ +/* + * 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.profile + +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.mifospay.core.common.Result +import org.mifospay.core.data.repository.ClientRepository +import org.mifospay.core.datastore.UserPreferencesRepository +import org.mifospay.core.model.client.Client +import org.mifospay.core.ui.utils.BaseViewModel +import org.mifospay.feature.profile.ProfileAction.Internal.HandleLoadClientImageResult +import org.mifospay.feature.profile.ProfileAction.Internal.HandleLoadClientResult +import org.mifospay.feature.profile.ProfileAction.Internal.LoadClient +import org.mifospay.feature.profile.ProfileAction.Internal.LoadClientImage + +internal class ProfileViewModel( + private val preferencesRepository: UserPreferencesRepository, + private val clientRepository: ClientRepository, +) : BaseViewModel( + initialState = run { + val clientId = requireNotNull(preferencesRepository.clientId.value) + ProfileState( + clientId = clientId, + viewState = ProfileState.ViewState.Loading, + ) + }, +) { + init { + viewModelScope.launch { + sendAction(LoadClient(state.clientId)) + sendAction(LoadClientImage(state.clientId)) + } + } + + override fun handleAction(action: ProfileAction) { + when (action) { + ProfileAction.NavigateToEditProfile -> { + sendEvent(ProfileEvent.OnEditProfile) + } + + ProfileAction.NavigateToLinkBankAccount -> { + sendEvent(ProfileEvent.OnLinkBankAccount) + } + + ProfileAction.ShowPersonalQRCode -> { + sendEvent(ProfileEvent.ShowQRCode) + } + + ProfileAction.DismissErrorDialog -> { + mutableStateFlow.update { + it.copy(dialogState = null) + } + } + + is HandleLoadClientImageResult -> handleLoadClientImageResult(action) + + is LoadClientImage -> loadClientImage(action) + + is LoadClient -> loadClient(action) + + is HandleLoadClientResult -> handleClientResult(action) + + is ProfileAction.RefreshProfile -> { + viewModelScope.launch { + sendAction(LoadClient(state.clientId)) + } + } + } + } + + private fun handleLoadClientImageResult(action: HandleLoadClientImageResult) { + when (action.result) { + is Result.Success -> { + mutableStateFlow.update { + it.copy(clientImage = action.result.data) + } + } + + is Result.Error -> { +// mutableStateFlow.update { +// it.copy(dialogState = Error(action.result.exception.message ?: "")) +// } + } + + is Result.Loading -> { + mutableStateFlow.update { + it.copy(dialogState = ProfileState.DialogState.Loading) + } + } + } + } + + private fun loadClientImage(action: LoadClientImage) { + clientRepository.getClientImage(action.clientId).onEach { + sendAction(HandleLoadClientImageResult(it)) + }.launchIn(viewModelScope) + } + + private fun loadClient(action: LoadClient) { + clientRepository + .getClientInfo(action.clientId) + .onEach { sendAction(HandleLoadClientResult(it)) } + .launchIn(viewModelScope) + } + + private fun handleClientResult(action: HandleLoadClientResult) { + when (action.result) { + is Result.Error -> { + mutableStateFlow.update { + it.copy( + viewState = ProfileState.ViewState.Error( + action.result.exception.message ?: "", + ), + ) + } + } + + is Result.Loading -> { + mutableStateFlow.update { + it.copy(viewState = ProfileState.ViewState.Loading) + } + } + + is Result.Success -> { + mutableStateFlow.update { + it.copy(viewState = ProfileState.ViewState.Success(action.result.data)) + } + + viewModelScope.launch { + preferencesRepository.updateClientInfo(action.result.data) + } + } + } + } +} + +internal data class ProfileState( + val clientId: Long, + val viewState: ViewState = ViewState.Loading, + val clientImage: String? = null, + val dialogState: DialogState? = null, +) { + sealed interface ViewState { + data object Loading : ViewState + data class Error(val message: String) : ViewState + data class Success(val client: Client) : ViewState + } + + sealed interface DialogState { + data object Loading : DialogState + + data class Error(val message: String) : DialogState + } +} + +internal sealed interface ProfileEvent { + data object OnEditProfile : ProfileEvent + data object OnLinkBankAccount : ProfileEvent + data object ShowQRCode : ProfileEvent +} + +internal sealed interface ProfileAction { + data object NavigateToEditProfile : ProfileAction + data object NavigateToLinkBankAccount : ProfileAction + data object ShowPersonalQRCode : ProfileAction + + data object DismissErrorDialog : ProfileAction + data object RefreshProfile : ProfileAction + + sealed interface Internal : ProfileAction { + data class LoadClientImage(val clientId: Long) : Internal + data class HandleLoadClientImageResult(val result: Result) : Internal + + data class LoadClient(val clientId: Long) : Internal + data class HandleLoadClientResult(val result: Result) : Internal + } +} diff --git a/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/components/ProfileCard.kt b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/components/ProfileCard.kt new file mode 100644 index 000000000..17e428cb6 --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/components/ProfileCard.kt @@ -0,0 +1,105 @@ +/* + * 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.profile.components + +import androidx.compose.foundation.layout.Arrangement +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.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import mobile_wallet.feature.profile.generated.resources.Res +import mobile_wallet.feature.profile.generated.resources.feature_profile_email +import mobile_wallet.feature.profile.generated.resources.feature_profile_mobile +import mobile_wallet.feature.profile.generated.resources.feature_profile_username +import mobile_wallet.feature.profile.generated.resources.feature_profile_vpa +import org.jetbrains.compose.resources.stringResource +import org.mifospay.core.model.client.Client + +@Composable +fun ProfileDetailsCard( + client: Client, + modifier: Modifier = Modifier, +) { + Card( + modifier = modifier + .fillMaxWidth(), + elevation = CardDefaults.cardElevation( + defaultElevation = 1.dp, + ), + shape = RoundedCornerShape(15.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + ), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + ProfileItem( + label = stringResource(Res.string.feature_profile_username), + value = client.displayName, + ) + ProfileItem( + label = stringResource(Res.string.feature_profile_email), + value = client.emailAddress, + ) + ProfileItem( + label = stringResource(Res.string.feature_profile_vpa), + value = client.externalId, + ) + ProfileItem( + label = stringResource(Res.string.feature_profile_mobile), + value = client.mobileNo, + ) + } + } +} + +@Composable +fun ProfileItem( + label: String, + value: String, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxWidth(), + ) { + Text( + text = label, + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.labelLarge, + ) + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = value, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight(400), + ) + Spacer(modifier = Modifier.height(4.dp)) + HorizontalDivider( + thickness = 1.dp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f), + ) + } +} diff --git a/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/components/ProfileImage.kt b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/components/ProfileImage.kt new file mode 100644 index 000000000..f3b28f18a --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/components/ProfileImage.kt @@ -0,0 +1,170 @@ +/* + * 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.profile.components + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +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.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.layout.ContentScale +import androidx.compose.ui.unit.dp +import coil3.ImageLoader +import coil3.compose.AsyncImage +import coil3.compose.LocalPlatformContext +import coil3.request.ImageRequest +import io.github.vinceglb.filekit.compose.rememberFilePickerLauncher +import io.github.vinceglb.filekit.core.PickerMode +import kotlinx.coroutines.launch +import mobile_wallet.feature.profile.generated.resources.Res +import mobile_wallet.feature.profile.generated.resources.placeholder +import org.jetbrains.compose.resources.painterResource +import org.mifospay.core.designsystem.icon.MifosIcons +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi + +@OptIn(ExperimentalEncodingApi::class) +@Composable +fun ProfileImage( + modifier: Modifier = Modifier, + bitmap: String? = null, +) { + val context = LocalPlatformContext.current + + Box( + modifier = modifier + .size(150.dp), + contentAlignment = Alignment.Center, + ) { + val image = bitmap?.let { Base64.decode(it) } + + AsyncImage( + model = ImageRequest.Builder(context) + .data(image) + .build(), + error = painterResource(Res.drawable.placeholder), + fallback = painterResource(Res.drawable.placeholder), + imageLoader = ImageLoader(context), + contentDescription = "Profile Image", + contentScale = ContentScale.Crop, + modifier = Modifier + .size(150.dp) + .clip(CircleShape) + .border(4.dp, MaterialTheme.colorScheme.primary, CircleShape), + ) + } +} + +@OptIn(ExperimentalEncodingApi::class) +@Composable +fun EditableProfileImage( + modifier: Modifier = Modifier, + serverImage: String? = null, + onChooseImage: (String) -> Unit, +) { + val context = LocalPlatformContext.current + val scope = rememberCoroutineScope() + + var bytes by remember(serverImage) { mutableStateOf(null) } + + LaunchedEffect(serverImage) { + if (serverImage != null) { + bytes = Base64.decode(serverImage) + } + } + + // Pick files from Compose + val launcher = rememberFilePickerLauncher(mode = PickerMode.Single) { file -> + scope.launch { + if (file != null) { + bytes = 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() + } + bytes?.let { + onChooseImage(Base64.encode(it)) + } + } + } + } + + Box( + modifier = modifier + .size(150.dp), + contentAlignment = Alignment.Center, + ) { + AsyncImage( + model = bytes, + error = painterResource(Res.drawable.placeholder), + fallback = painterResource(Res.drawable.placeholder), + imageLoader = ImageLoader(context), + contentDescription = "Profile Image", + contentScale = ContentScale.Crop, + modifier = Modifier + .size(150.dp) + .clip(CircleShape) + .border(4.dp, MaterialTheme.colorScheme.primary, CircleShape), + ) + + IconButton( + onClick = { + launcher.launch() + }, + modifier = Modifier + .offset(y = 12.dp) + .size(36.dp) + .clip(CircleShape) + .align(Alignment.BottomCenter), + colors = IconButtonDefaults.iconButtonColors(Color.White), + ) { + Icon( + imageVector = MifosIcons.Edit2, + contentDescription = null, + modifier = Modifier + .size(24.dp), + ) + } + } +} diff --git a/feature/profile/src/main/kotlin/org/mifospay/feature/profile/di/ProfileModule.kt b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/di/ProfileModule.kt similarity index 52% rename from feature/profile/src/main/kotlin/org/mifospay/feature/profile/di/ProfileModule.kt rename to feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/di/ProfileModule.kt index c93e8ab9d..ea31e2938 100644 --- a/feature/profile/src/main/kotlin/org/mifospay/feature/profile/di/ProfileModule.kt +++ b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/di/ProfileModule.kt @@ -9,27 +9,12 @@ */ package org.mifospay.feature.profile.di -import org.koin.core.module.dsl.viewModel +import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module import org.mifospay.feature.profile.ProfileViewModel import org.mifospay.feature.profile.edit.EditProfileViewModel val ProfileModule = module { - viewModel { - ProfileViewModel( - mUseCaseHandler = get(), - fetchClientImageUseCase = get(), - localRepository = get(), - mPreferencesHelper = get(), - ) - } - - viewModel { - EditProfileViewModel( - mUseCaseHandler = get(), - mPreferencesHelper = get(), - updateUserUseCase = get(), - updateClientUseCase = get(), - ) - } + viewModelOf(::ProfileViewModel) + viewModelOf(::EditProfileViewModel) } diff --git a/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/edit/EditProfileScreen.kt b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/edit/EditProfileScreen.kt new file mode 100644 index 000000000..9a6971063 --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/edit/EditProfileScreen.kt @@ -0,0 +1,213 @@ +/* + * 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.profile.edit + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +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.lazy.LazyColumn +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.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.launch +import mobile_wallet.feature.profile.generated.resources.Res +import mobile_wallet.feature.profile.generated.resources.feature_profile_edit_profile +import mobile_wallet.feature.profile.generated.resources.feature_profile_email +import mobile_wallet.feature.profile.generated.resources.feature_profile_firstname +import mobile_wallet.feature.profile.generated.resources.feature_profile_lastname +import mobile_wallet.feature.profile.generated.resources.feature_profile_mobile +import mobile_wallet.feature.profile.generated.resources.feature_profile_save +import mobile_wallet.feature.profile.generated.resources.feature_profile_vpa +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.theme.MifosBlue +import org.mifospay.core.ui.utils.EventsEffect +import org.mifospay.feature.profile.components.EditableProfileImage + +@Composable +internal fun EditProfileScreen( + onBackClick: () -> Unit, + modifier: Modifier = Modifier, + viewModel: EditProfileViewModel = koinViewModel(), +) { + val snackbarHostState = remember { SnackbarHostState() } + val scope = rememberCoroutineScope() + + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + + EventsEffect(viewModel) { event -> + when (event) { + is EditProfileEvent.NavigateBack -> onBackClick.invoke() + is EditProfileEvent.ShowToast -> { + scope.launch { + snackbarHostState.showSnackbar(event.message) + } + } + } + } + + Box(modifier) { + EditProfileScreenContent( + state = state, + onAction = remember(viewModel) { + { action -> viewModel.trySendAction(action) } + }, + snackbarHostState = snackbarHostState, + ) + + EditProfileDialogs( + dialogState = state.dialogState, + onDismissRequest = remember(viewModel) { + { viewModel.trySendAction(EditProfileAction.DismissErrorDialog) } + }, + ) + } +} + +@Composable +private fun EditProfileScreenContent( + modifier: Modifier = Modifier, + state: EditProfileState, + onAction: (EditProfileAction) -> Unit, + snackbarHostState: SnackbarHostState, +) { + MifosScaffold( + topBarTitle = stringResource(Res.string.feature_profile_edit_profile), + backPress = { + onAction(EditProfileAction.NavigateBack) + }, + snackbarHost = { SnackbarHost(snackbarHostState) }, + ) { paddingValues -> + LazyColumn( + modifier = modifier + .padding(paddingValues) + .fillMaxSize(), + contentPadding = PaddingValues(top = 30.dp, bottom = 10.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + item { + EditableProfileImage( + serverImage = state.imageInput, + onChooseImage = { + onAction(EditProfileAction.ProfileImageChange(it)) + }, + ) + } + + item { + MifosTextField( + value = state.firstNameInput, + onValueChange = { + onAction(EditProfileAction.FirstNameInputChange(it)) + }, + label = stringResource(Res.string.feature_profile_firstname), + ) + } + + item { + MifosTextField( + value = state.lastNameInput, + onValueChange = { + onAction(EditProfileAction.LastNameInputChange(it)) + }, + label = stringResource(Res.string.feature_profile_lastname), + ) + } + + item { + MifosTextField( + value = state.emailInput, + onValueChange = { + onAction(EditProfileAction.EmailInputChange(it)) + }, + label = stringResource(Res.string.feature_profile_email), + ) + } + + item { + MifosTextField( + value = state.externalIdInput, + onValueChange = { + onAction(EditProfileAction.ExternalIdInputChange(it)) + }, + label = stringResource(Res.string.feature_profile_vpa), + ) + } + + item { + MifosTextField( + value = state.phoneNumberInput, + onValueChange = { + onAction(EditProfileAction.PhoneNumberInputChange(it)) + }, + label = stringResource(Res.string.feature_profile_mobile), + ) + } + + item { + Spacer(modifier.height(16.dp)) + + MifosButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .height(54.dp), + color = MifosBlue, + text = { Text(text = stringResource(Res.string.feature_profile_save)) }, + onClick = { + onAction(EditProfileAction.UpdateProfile) + }, + ) + } + } + } +} + +@Composable +private fun EditProfileDialogs( + dialogState: EditProfileState.DialogState?, + onDismissRequest: () -> Unit, +) { + when (dialogState) { + is EditProfileState.DialogState.Error -> MifosBasicDialog( + visibilityState = BasicDialogState.Shown( + message = dialogState.message, + ), + onDismissRequest = onDismissRequest, + ) + + is EditProfileState.DialogState.Loading -> MifosLoadingDialog( + visibilityState = LoadingDialogState.Shown, + ) + + null -> Unit + } +} diff --git a/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/edit/EditProfileViewModel.kt b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/edit/EditProfileViewModel.kt new file mode 100644 index 000000000..62689eb58 --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/edit/EditProfileViewModel.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.profile.edit + +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 org.mifospay.core.common.IgnoredOnParcel +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize +import org.mifospay.core.common.Result +import org.mifospay.core.common.utils.isValidEmail +import org.mifospay.core.data.repository.ClientRepository +import org.mifospay.core.datastore.UserPreferencesRepository +import org.mifospay.core.model.client.UpdatedClient +import org.mifospay.core.ui.utils.BaseViewModel +import org.mifospay.feature.profile.edit.EditProfileAction.Internal.HandleLoadClientImageResult +import org.mifospay.feature.profile.edit.EditProfileAction.Internal.HandleUpdateClientImageResult +import org.mifospay.feature.profile.edit.EditProfileAction.Internal.LoadClientImage +import org.mifospay.feature.profile.edit.EditProfileAction.Internal.OnUpdateProfileResult +import org.mifospay.feature.profile.edit.EditProfileState.DialogState.Error + +private const val KEY = "edit_profile_state" + +internal class EditProfileViewModel( + private val preferencesRepository: UserPreferencesRepository, + private val clientRepository: ClientRepository, + savedStateHandle: SavedStateHandle, +) : BaseViewModel( + initialState = savedStateHandle[KEY] ?: run { + val client = requireNotNull(preferencesRepository.client.value) + + EditProfileState( + clientId = client.id, + firstNameInput = client.firstname, + lastNameInput = client.lastname, + emailInput = client.emailAddress, + phoneNumberInput = client.mobileNo, + externalIdInput = client.externalId, + ) + }, +) { + init { + stateFlow + .onEach { savedStateHandle[KEY] = it } + .launchIn(viewModelScope) + + trySendAction(LoadClientImage(state.clientId)) + } + + override fun handleAction(action: EditProfileAction) { + when (action) { + is EditProfileAction.FirstNameInputChange -> { + mutableStateFlow.update { + it.copy(firstNameInput = action.firstName) + } + } + + is EditProfileAction.LastNameInputChange -> { + mutableStateFlow.update { + it.copy(lastNameInput = action.lastName) + } + } + + is EditProfileAction.EmailInputChange -> { + mutableStateFlow.update { + it.copy(emailInput = action.email) + } + } + + is EditProfileAction.ExternalIdInputChange -> { + mutableStateFlow.update { + it.copy(externalIdInput = action.externalId) + } + } + + is EditProfileAction.PhoneNumberInputChange -> { + mutableStateFlow.update { + it.copy(phoneNumberInput = action.phoneNumber) + } + } + + is EditProfileAction.DismissErrorDialog -> { + mutableStateFlow.update { + it.copy(dialogState = null) + } + } + + is EditProfileAction.NavigateBack -> { + sendEvent(EditProfileEvent.NavigateBack) + } + + is EditProfileAction.ProfileImageChange -> handleChangeProfileImage(action) + + is OnUpdateProfileResult -> handleUpdateProfileResult(action) + + is HandleLoadClientImageResult -> handleLoadClientImageResult(action) + + is LoadClientImage -> loadClientImage(action) + + is EditProfileAction.UpdateProfile -> handleUpdateProfile() + + is HandleUpdateClientImageResult -> handleUpdateClientImageResult(action) + } + } + + private fun handleLoadClientImageResult(action: HandleLoadClientImageResult) { + when (action.result) { + is Result.Success -> { + mutableStateFlow.update { + it.copy(imageInput = action.result.data) + } + } + + is Result.Error -> { +// mutableStateFlow.update { +// it.copy(dialogState = Error(action.result.exception.message ?: "")) +// } + } + + is Result.Loading -> { + mutableStateFlow.update { + it.copy(dialogState = EditProfileState.DialogState.Loading) + } + } + } + } + + private fun loadClientImage(action: LoadClientImage) { + clientRepository.getClientImage(action.clientId).onEach { + sendAction(HandleLoadClientImageResult(it)) + }.launchIn(viewModelScope) + } + + private fun handleChangeProfileImage(action: EditProfileAction.ProfileImageChange) { + mutableStateFlow.update { + it.copy(imageInput = action.imageString) + } + } + + private fun handleUpdateProfile() = when { + state.firstNameInput.isEmpty() -> { + mutableStateFlow.update { + it.copy(dialogState = Error("Please enter client firstname.")) + } + } + + state.lastNameInput.isEmpty() -> { + mutableStateFlow.update { + it.copy(dialogState = Error("Please enter client lastname.")) + } + } + + state.emailInput.isEmpty() -> { + mutableStateFlow.update { + it.copy(dialogState = Error("Please enter your email.")) + } + } + + !state.emailInput.isValidEmail() -> { + mutableStateFlow.update { + it.copy(dialogState = Error("Please enter a valid email.")) + } + } + + state.phoneNumberInput.isEmpty() -> { + mutableStateFlow.update { + it.copy(dialogState = Error("Please enter your mobile number.")) + } + } + + state.phoneNumberInput.length < 10 -> { + mutableStateFlow.update { + it.copy(dialogState = Error("Mobile number must be 10 digits long.")) + } + } + + else -> initiateUpdateProfile() + } + + private fun initiateUpdateProfile() { + viewModelScope.launch { + val result = clientRepository.updateClient(state.clientId, state.updatedClient) + + sendAction(OnUpdateProfileResult(result)) + } + } + + private fun handleUpdateProfileResult(action: OnUpdateProfileResult) { + when (action.result) { + is Result.Error -> { + mutableStateFlow.update { + it.copy(dialogState = Error(action.result.exception.message ?: "")) + } + } + + is Result.Loading -> { + mutableStateFlow.update { + it.copy(dialogState = EditProfileState.DialogState.Loading) + } + } + + is Result.Success -> { + viewModelScope.launch { + if (state.imageInput != null) { + val result = clientRepository.updateClientImage( + state.clientId, + state.imageInput!!, + ) + sendAction(HandleUpdateClientImageResult(result)) + } + + val result = preferencesRepository.updateClientProfile(state.updatedClient) + + when (result) { + is Result.Success -> { + sendEvent(EditProfileEvent.ShowToast("Profile updated successfully")) + trySendAction(EditProfileAction.NavigateBack) + } + + else -> {} + } + } + } + } + } + + private fun handleUpdateClientImageResult(action: HandleUpdateClientImageResult) { + when (action.result) { + is Result.Error -> { + mutableStateFlow.update { + it.copy(dialogState = Error(action.result.exception.message ?: "")) + } + } + + is Result.Loading -> { + mutableStateFlow.update { + it.copy(dialogState = EditProfileState.DialogState.Loading) + } + } + + is Result.Success -> { + sendEvent(EditProfileEvent.ShowToast("Profile image updated successfully")) + } + } + } +} + +@Parcelize +internal data class EditProfileState( + val clientId: Long, + val firstNameInput: String, + val lastNameInput: String, + val phoneNumberInput: String, + val emailInput: String, + val externalIdInput: String, + val imageInput: String? = null, + val dialogState: DialogState? = null, +) : Parcelable { + @IgnoredOnParcel + internal val updatedClient = UpdatedClient( + firstname = this.firstNameInput, + lastname = this.lastNameInput, + emailAddress = this.emailInput, + mobileNo = this.phoneNumberInput, + externalId = this.externalIdInput, + ) + + sealed interface DialogState : Parcelable { + @Parcelize + data object Loading : DialogState + + @Parcelize + data class Error(val message: String) : DialogState + } +} + +sealed interface EditProfileEvent { + data object NavigateBack : EditProfileEvent + data class ShowToast(val message: String) : EditProfileEvent +} + +sealed interface EditProfileAction { + data class FirstNameInputChange(val firstName: String) : EditProfileAction + data class LastNameInputChange(val lastName: String) : EditProfileAction + data class PhoneNumberInputChange(val phoneNumber: String) : EditProfileAction + data class EmailInputChange(val email: String) : EditProfileAction + data class ExternalIdInputChange(val externalId: String) : EditProfileAction + + data class ProfileImageChange(val imageString: String) : EditProfileAction + + data object DismissErrorDialog : EditProfileAction + data object NavigateBack : EditProfileAction + + data object UpdateProfile : EditProfileAction + + sealed interface Internal : EditProfileAction { + data class LoadClientImage(val clientId: Long) : Internal + data class HandleLoadClientImageResult(val result: Result) : Internal + + data class OnUpdateProfileResult(val result: Result) : Internal + + data class HandleUpdateClientImageResult(val result: Result) : Internal + } +} diff --git a/feature/profile/src/main/kotlin/org/mifospay/feature/profile/navigation/EditProfileNavigation.kt b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/navigation/EditProfileNavigation.kt similarity index 74% rename from feature/profile/src/main/kotlin/org/mifospay/feature/profile/navigation/EditProfileNavigation.kt rename to feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/navigation/EditProfileNavigation.kt index eee0bd522..aa94c7f5a 100644 --- a/feature/profile/src/main/kotlin/org/mifospay/feature/profile/navigation/EditProfileNavigation.kt +++ b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/navigation/EditProfileNavigation.kt @@ -9,27 +9,22 @@ */ package org.mifospay.feature.profile.navigation -import android.content.Context -import android.net.Uri import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable -import org.mifospay.feature.profile.edit.EditProfileScreenRoute -import java.io.File +import org.mifospay.feature.profile.edit.EditProfileScreen const val EDIT_PROFILE_ROUTE = "edit_profile_route" fun NavController.navigateToEditProfile(navOptions: NavOptions? = null) = navigate(EDIT_PROFILE_ROUTE, navOptions) -fun NavGraphBuilder.editProfileScreen( +internal fun NavGraphBuilder.editProfileScreen( onBackPress: () -> Unit, - getUri: (context: Context, file: File) -> Uri, ) { composable(route = EDIT_PROFILE_ROUTE) { - EditProfileScreenRoute( + EditProfileScreen( onBackClick = onBackPress, - getUri = getUri, ) } } diff --git a/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/navigation/ProfileNavigation.kt b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/navigation/ProfileNavigation.kt new file mode 100644 index 000000000..cf42d5a46 --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/org/mifospay/feature/profile/navigation/ProfileNavigation.kt @@ -0,0 +1,54 @@ +/* + * 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.profile.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import androidx.navigation.compose.navigation +import org.mifospay.feature.profile.ProfileScreen + +private const val PROFILE_NAVIGATION = "profile_navigation" +const val PROFILE_ROUTE = "profile_route" + +fun NavController.navigateToProfile(navOptions: NavOptions) = + navigate(PROFILE_NAVIGATION, navOptions) + +internal fun NavGraphBuilder.profileScreen( + onEditProfile: () -> Unit, + onLinkBankAccount: () -> Unit, +) { + composable(route = PROFILE_ROUTE) { + ProfileScreen( + onEditProfile = onEditProfile, + onLinkBackAccount = onLinkBankAccount, + ) + } +} + +fun NavGraphBuilder.profileNavGraph( + navController: NavController, + onLinkBankAccount: () -> Unit, +) { + navigation( + route = PROFILE_NAVIGATION, + startDestination = PROFILE_ROUTE, + ) { + profileScreen( + onEditProfile = navController::navigateToEditProfile, + onLinkBankAccount = onLinkBankAccount, + ) + + editProfileScreen( + onBackPress = navController::navigateUp, + ) + } +} diff --git a/feature/profile/src/main/kotlin/org/mifospay/feature/profile/ProfileItemCard.kt b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/ProfileItemCard.kt deleted file mode 100644 index 9c3a7e8a6..000000000 --- a/feature/profile/src/main/kotlin/org/mifospay/feature/profile/ProfileItemCard.kt +++ /dev/null @@ -1,110 +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.profile - -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -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.graphics.vector.rememberVectorPainter -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import org.mifospay.core.designsystem.icon.MifosIcons -import org.mifospay.core.designsystem.theme.MifosTheme - -@OptIn(ExperimentalLayoutApi::class) -@Composable -internal fun ProfileItemCard( - icon: ImageVector, - text: Int, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - val combinedModifier = modifier - .border(width = 1.dp, color = Color.Gray, shape = RoundedCornerShape(8.dp)) - .padding(16.dp) - .clickable { onClick.invoke() } - - FlowRow(modifier = combinedModifier) { - Icon( - painter = rememberVectorPainter(icon), - modifier = Modifier.size(32.dp), - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface, - ) - Text( - modifier = if (text == R.string.feature_profile_edit_profile || - text == R.string.feature_profile_settings - ) { - Modifier - .padding(start = 18.dp) - .align(Alignment.CenterVertically) - } else { - Modifier - }, - text = stringResource(id = text), - style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.Medium), - color = MaterialTheme.colorScheme.onSurface, - ) - if (text == R.string.feature_profile_edit_profile || text == R.string.feature_profile_settings) { - Spacer(modifier = Modifier.fillMaxWidth()) - } - } -} - -@Composable -internal fun DetailItem( - label: String, - value: String, - modifier: Modifier = Modifier, -) { - Text( - text = "$label: $value", - style = MaterialTheme.typography.bodyLarge, - modifier = modifier.padding(bottom = 12.dp), - ) -} - -@Preview(showBackground = true) -@Composable -private fun PreviewProfileItemCard() { - MifosTheme { - ProfileItemCard( - icon = MifosIcons.Profile, - text = R.string.feature_profile_edit_profile, - onClick = {}, - ) - } -} - -@Preview(showBackground = true) -@Composable -private fun PreviewDetailItem() { - MifosTheme { - DetailItem(label = "Email", value = "john.doe@example.com") - } -} diff --git a/feature/profile/src/main/kotlin/org/mifospay/feature/profile/ProfileScreen.kt b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/ProfileScreen.kt deleted file mode 100644 index e33399b72..000000000 --- a/feature/profile/src/main/kotlin/org/mifospay/feature/profile/ProfileScreen.kt +++ /dev/null @@ -1,201 +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.profile - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -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.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -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.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -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 org.koin.androidx.compose.koinViewModel -import org.mifospay.core.designsystem.component.MfLoadingWheel -import org.mifospay.core.designsystem.icon.MifosIcons -import org.mifospay.core.ui.ProfileImage - -@Composable -fun ProfileRoute( - onEditProfile: () -> Unit, - onSettings: () -> Unit, - modifier: Modifier = Modifier, - viewModel: ProfileViewModel = koinViewModel(), -) { - val profileState by viewModel.profileState.collectAsStateWithLifecycle() - - ProfileScreenContent( - profileState = profileState, - onEditProfile = onEditProfile, - onSettings = onSettings, - modifier = modifier, - ) -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun ProfileScreenContent( - profileState: ProfileUiState, - onEditProfile: () -> Unit, - onSettings: () -> Unit, - modifier: Modifier = Modifier, -) { - var showDetails by remember { mutableStateOf(false) } - - Column( - modifier = modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()), - ) { - when (profileState) { - is ProfileUiState.Loading -> MfLoadingWheel() - - is ProfileUiState.Success -> { - ProfileImage(bitmap = profileState.bitmapImage) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 12.dp, bottom = 8.dp), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = profileState.name.toString(), - style = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Medium), - color = MaterialTheme.colorScheme.onSurface, - ) - IconButton(onClick = { showDetails = !showDetails }) { - Icon( - imageVector = MifosIcons.ArrowDropDown, - tint = MaterialTheme.colorScheme.onSurface, - contentDescription = null, - ) - } - } - - if (showDetails) { - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - DetailItem( - label = stringResource(id = R.string.feature_profile_email), - value = profileState.email.toString(), - ) - DetailItem( - label = stringResource(id = R.string.feature_profile_vpa), - value = profileState.vpa.toString(), - ) - DetailItem( - label = stringResource(id = R.string.feature_profile_mobile), - value = profileState.mobile.toString(), - ) - } - } - - Column( - modifier = Modifier - .fillMaxSize() - .padding(start = 24.dp, end = 24.dp), - ) { - FlowRow( - modifier = Modifier - .fillMaxWidth(), - maxItemsInEachRow = 2, - ) { - ProfileItemCard( - modifier = Modifier - .padding(end = 8.dp, bottom = 8.dp) - .weight(1f), - icon = MifosIcons.QR, - text = R.string.feature_profile_personal_qr_code, - onClick = {}, - ) - - ProfileItemCard( - modifier = Modifier - .padding(start = 8.dp, bottom = 8.dp) - .weight(1f), - icon = MifosIcons.Bank, - text = R.string.feature_profile_link_bank_account, - onClick = {}, - ) - - ProfileItemCard( - modifier = Modifier - .padding(top = 8.dp, bottom = 8.dp), - icon = MifosIcons.Contact, - text = R.string.feature_profile_edit_profile, - onClick = { onEditProfile.invoke() }, - ) - - ProfileItemCard( - modifier = Modifier - .padding(top = 8.dp), - icon = MifosIcons.Settings, - text = R.string.feature_profile_settings, - onClick = { onSettings.invoke() }, - ) - } - } - } - } - } -} - -internal class ProfilePreviewProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - ProfileUiState.Loading, - ProfileUiState.Success( - name = "John Doe", - email = "john.doe@example.com", - vpa = "john@vpa", - mobile = "+1234567890", - bitmapImage = null, - ), - ) -} - -@Preview(showSystemUi = true, showBackground = true) -@Composable -private fun ProfileScreenPreview( - @PreviewParameter(ProfilePreviewProvider::class) - profileState: ProfileUiState, -) { - ProfileScreenContent( - profileState = profileState, - onEditProfile = {}, - onSettings = {}, - ) -} diff --git a/feature/profile/src/main/kotlin/org/mifospay/feature/profile/ProfileViewModel.kt b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/ProfileViewModel.kt deleted file mode 100644 index e6c24ded3..000000000 --- a/feature/profile/src/main/kotlin/org/mifospay/feature/profile/ProfileViewModel.kt +++ /dev/null @@ -1,96 +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.profile - -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import okhttp3.ResponseBody -import org.mifospay.core.common.DebugUtil -import org.mifospay.core.data.base.UseCase -import org.mifospay.core.data.base.UseCaseHandler -import org.mifospay.core.data.domain.usecase.client.FetchClientImage -import org.mifospay.core.data.repository.local.LocalRepository -import org.mifospay.core.datastore.PreferencesHelper - -class ProfileViewModel( - private val mUseCaseHandler: UseCaseHandler, - private val fetchClientImageUseCase: FetchClientImage, - private val localRepository: LocalRepository, - private val mPreferencesHelper: PreferencesHelper, -) : ViewModel() { - - private val mProfileState = MutableStateFlow(ProfileUiState.Loading) - val profileState: StateFlow get() = mProfileState - - init { - fetchClientImage() - fetchProfileDetails() - } - - private fun fetchClientImage() { - viewModelScope.launch { - mUseCaseHandler.execute( - fetchClientImageUseCase, - FetchClientImage.RequestValues(localRepository.clientDetails.clientId), - object : UseCase.UseCaseCallback { - override fun onSuccess(response: FetchClientImage.ResponseValue) { - val bitmap = convertResponseToBitmap(response.responseBody) - val currentState = mProfileState.value as ProfileUiState.Success - mProfileState.value = currentState.copy(bitmapImage = bitmap) - } - - override fun onError(message: String) { - DebugUtil.log("image", message) - } - }, - ) - } - } - - private fun fetchProfileDetails() { - val name = mPreferencesHelper.fullName ?: "-" - val email = mPreferencesHelper.email ?: "-" - val vpa = mPreferencesHelper.clientVpa ?: "-" - val mobile = mPreferencesHelper.mobile ?: "-" - - mProfileState.value = ProfileUiState.Success( - name = name, - email = email, - vpa = vpa, - mobile = mobile, - ) - } - - private fun convertResponseToBitmap(responseBody: ResponseBody?): Bitmap? { - return try { - responseBody?.byteStream()?.use { inputStream -> - BitmapFactory.decodeStream(inputStream) - } - } catch (e: Exception) { - null - } - } -} - -sealed class ProfileUiState { - data object Loading : ProfileUiState() - data class Success( - val bitmapImage: Bitmap? = null, - val name: String?, - val email: String?, - val vpa: String?, - val mobile: String?, - ) : ProfileUiState() -} diff --git a/feature/profile/src/main/kotlin/org/mifospay/feature/profile/edit/EditProfileScreen.kt b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/edit/EditProfileScreen.kt deleted file mode 100644 index cefd425eb..000000000 --- a/feature/profile/src/main/kotlin/org/mifospay/feature/profile/edit/EditProfileScreen.kt +++ /dev/null @@ -1,474 +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.profile.edit - -import android.Manifest -import android.content.Context -import android.net.Uri -import android.os.Build -import android.os.Environment -import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -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.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Icon -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.platform.LocalContext -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource -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.lifecycle.compose.collectAsStateWithLifecycle -import com.mifos.library.countrycodepicker.CountryCodePicker -import org.koin.androidx.compose.koinViewModel -import org.mifospay.core.designsystem.component.MfLoadingWheel -import org.mifospay.core.designsystem.component.MfOutlinedTextField -import org.mifospay.core.designsystem.component.MifosBottomSheet -import org.mifospay.core.designsystem.component.MifosButton -import org.mifospay.core.designsystem.component.MifosDialogBox -import org.mifospay.core.designsystem.component.MifosScaffold -import org.mifospay.core.designsystem.component.PermissionBox -import org.mifospay.core.designsystem.icon.MifosIcons.Camera -import org.mifospay.core.designsystem.icon.MifosIcons.Delete -import org.mifospay.core.designsystem.icon.MifosIcons.PhotoLibrary -import org.mifospay.core.designsystem.theme.MifosTheme -import org.mifospay.core.designsystem.theme.historyItemTextStyle -import org.mifospay.core.designsystem.theme.styleMedium16sp -import org.mifospay.feature.profile.R -import java.io.File -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale - -@Composable -fun EditProfileScreenRoute( - onBackClick: () -> Unit, - getUri: (context: Context, file: File) -> Uri, - modifier: Modifier = Modifier, - viewModel: EditProfileViewModel = koinViewModel(), -) { - val editProfileUiState by viewModel.editProfileUiState.collectAsStateWithLifecycle() - val updateSuccess by viewModel.updateSuccess.collectAsStateWithLifecycle() - - val context = LocalContext.current - val file = createImageFile(context) - val uri = getUri(context, file) - - EditProfileScreen( - editProfileUiState = editProfileUiState, - updateSuccess = updateSuccess, - onBackClick = onBackClick, - updateEmail = viewModel::updateEmail, - updateMobile = viewModel::updateMobile, - modifier = modifier, - uri = uri, - ) -} - -@Composable -fun EditProfileScreen( - editProfileUiState: EditProfileUiState, - updateSuccess: Boolean, - onBackClick: () -> Unit, - updateEmail: (String) -> Unit, - updateMobile: (String) -> Unit, - modifier: Modifier = Modifier, - uri: Uri? = null, -) { - var showDiscardChangesDialog by rememberSaveable { mutableStateOf(false) } - - Box( - modifier = - modifier - .fillMaxSize(), - ) { - MifosScaffold( - topBarTitle = R.string.feature_profile_edit_profile, - backPress = { showDiscardChangesDialog = true }, - scaffoldContent = { - when (editProfileUiState) { - EditProfileUiState.Loading -> { - MfLoadingWheel( - contentDesc = stringResource(R.string.feature_profile_loading), - backgroundColor = MaterialTheme.colorScheme.surface, - ) - } - - is EditProfileUiState.Success -> { - val initialUsername = editProfileUiState.username - val initialMobile = editProfileUiState.mobile - val initialVpa = editProfileUiState.vpa - val initialEmail = editProfileUiState.email - - EditProfileScreenContent( - initialUsername, - initialMobile, - initialVpa, - initialEmail, - updateSuccess = updateSuccess, - contentPadding = it, - updateEmail = updateEmail, - updateMobile = updateMobile, - onBackClick = onBackClick, - uri = uri, - ) - } - } - }, - ) - - MifosDialogBox( - showDialogState = showDiscardChangesDialog, - onDismiss = { showDiscardChangesDialog = false }, - title = R.string.feature_profile_discard_changes, - confirmButtonText = R.string.feature_profile_confirm_text, - onConfirm = { - showDiscardChangesDialog = false - onBackClick.invoke() - }, - dismissButtonText = R.string.feature_profile_dismiss_text, - ) - } -} - -@Suppress("LongMethod") -@Composable -private fun EditProfileScreenContent( - initialUsername: String, - initialMobile: String, - initialVpa: String, - initialEmail: String, - updateSuccess: Boolean, - contentPadding: PaddingValues, - updateEmail: (String) -> Unit, - updateMobile: (String) -> Unit, - onBackClick: () -> Unit, - modifier: Modifier = Modifier, - uri: Uri? = null, -) { - var username by rememberSaveable { mutableStateOf(initialUsername) } - var mobile by rememberSaveable { mutableStateOf(initialMobile) } - var vpa by rememberSaveable { mutableStateOf(initialVpa) } - var email by rememberSaveable { mutableStateOf(initialEmail) } - var imageUri by rememberSaveable { mutableStateOf(null) } - var showBottomSheet by rememberSaveable { mutableStateOf(false) } - val context = LocalContext.current - - PermissionBox( - requiredPermissions = - if (Build.VERSION.SDK_INT >= 33) { - listOf(Manifest.permission.CAMERA) - } else { - listOf( - Manifest.permission.CAMERA, - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE, - ) - }, - title = R.string.feature_profile_permission_required, - confirmButtonText = R.string.feature_profile_proceed, - dismissButtonText = R.string.feature_profile_dismiss, - description = R.string.feature_profile_approve_description, - onGranted = { - val cameraLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { - imageUri = uri - } - - val galleryLauncher = - rememberLauncherForActivityResult( - contract = ActivityResultContracts.GetContent(), - ) { uri: Uri? -> - uri?.let { - imageUri = uri - } - } - - if (showBottomSheet) { - MifosBottomSheet( - content = { - EditProfileBottomSheetContent( - onClickProfilePicture = { - if (uri != null) { - cameraLauncher.launch(uri) - } - showBottomSheet = false - }, - onChangeProfilePicture = { - galleryLauncher.launch("image/*") - showBottomSheet = false - }, - onRemoveProfilePicture = { - imageUri = null - showBottomSheet = false - }, - ) - }, - onDismiss = { showBottomSheet = false }, - ) - } - }, - ) - Box( - modifier = - modifier - .padding(contentPadding) - .fillMaxSize(), - ) { - Column( - modifier = - Modifier - .fillMaxSize() - .background(color = MaterialTheme.colorScheme.surface) - .verticalScroll(rememberScrollState()), - ) { - EditProfileScreenImage( - imageUri = imageUri, - onCameraIconClick = { showBottomSheet = true }, - ) - MfOutlinedTextField( - value = username, - label = stringResource(id = R.string.feature_profile_username), - onValueChange = { username = it }, - modifier = - Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp), - ) - MfOutlinedTextField( - value = email, - label = stringResource(id = R.string.feature_profile_email), - onValueChange = { email = it }, - modifier = - Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp), - ) - MfOutlinedTextField( - value = vpa, - label = stringResource(id = R.string.feature_profile_vpa), - onValueChange = { vpa = it }, - modifier = - Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp), - ) - Box( - modifier = - Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp), - ) { - val keyboardController = LocalSoftwareKeyboardController.current - if (LocalInspectionMode.current) { - Text("Placeholder for TogiCountryCodePicker") - } else { - CountryCodePicker( - modifier = Modifier, - initialPhoneNumber = " ", - autoDetectCode = true, - shape = RoundedCornerShape(3.dp), - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.onSurface, - ), - onValueChange = { (code, phone), isValid -> - if (isValid) { - mobile = code + phone - } - }, - label = { Text(stringResource(id = R.string.feature_profile_phone_number)) }, - keyboardActions = KeyboardActions { keyboardController?.hide() }, - ) - } - } - EditProfileSaveButton( - onClick = { - if (isDataSaveNecessary(email, initialEmail)) { - updateEmail(email) - } - if (isDataSaveNecessary(mobile, initialMobile)) { - updateMobile(mobile) - } - if (updateSuccess) { - // if user details is successfully saved then go back to Profile Activity - // same behaviour as onBackPress, hence reused the callback - onBackClick.invoke() - } else { - Toast - .makeText( - context, - R.string.feature_profile_failed_to_save_changes, - Toast.LENGTH_SHORT, - ).show() - } - }, - buttonText = R.string.feature_profile_save, - ) - } - } -} - -private fun isDataSaveNecessary( - input: String, - initialInput: String, -): Boolean = input == initialInput - -@Composable -fun EditProfileBottomSheetContent( - onClickProfilePicture: () -> Unit, - onChangeProfilePicture: () -> Unit, - onRemoveProfilePicture: () -> Unit, - modifier: Modifier = Modifier, -) { - Column( - modifier = - modifier - .background(MaterialTheme.colorScheme.surface) - .padding(top = 8.dp, bottom = 12.dp), - ) { - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(12.dp) - .clickable { onClickProfilePicture.invoke() }, - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon(imageVector = Camera, contentDescription = null) - Text( - text = stringResource(id = R.string.feature_profile_click_profile_picture), - style = historyItemTextStyle, - ) - } - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(12.dp) - .clickable { onChangeProfilePicture.invoke() }, - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon(imageVector = PhotoLibrary, contentDescription = null) - Text( - text = stringResource(id = R.string.feature_profile_change_profile_picture), - style = historyItemTextStyle, - ) - } - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(12.dp) - .clickable { onRemoveProfilePicture.invoke() }, - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon(imageVector = Delete, contentDescription = null) - Text( - text = stringResource(id = R.string.feature_profile_remove_profile_picture), - style = historyItemTextStyle, - ) - } - } -} - -@Composable -private fun EditProfileSaveButton( - onClick: () -> Unit, - buttonText: Int, - modifier: Modifier = Modifier, -) { - MifosButton( - onClick = onClick, - colors = ButtonDefaults.buttonColors(MaterialTheme.colorScheme.primary), - modifier = modifier - .fillMaxWidth() - .padding(16.dp), - shape = RoundedCornerShape(10.dp), - contentPadding = PaddingValues(12.dp), - ) { - Text( - text = stringResource(id = buttonText), - style = styleMedium16sp.copy(MaterialTheme.colorScheme.onPrimary), - ) - } -} - -private fun createImageFile(context: Context): File { - val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) - val storageDir: File? = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) - return File.createTempFile( - "JPEG_${timeStamp}_", - ".jpg", - storageDir, - ) -} - -internal class EditProfilePreviewProvider : PreviewParameterProvider { - override val values: Sequence - get() = - sequenceOf( - EditProfileUiState.Loading, - EditProfileUiState.Success(), - EditProfileUiState.Success( - name = "John Doe", - username = "John", - email = "john@mifos.org", - vpa = "vpa", - mobile = "+1 55557772901", - ), - ) -} - -@Preview(showBackground = true, showSystemUi = true) -@Composable -private fun EditProfileScreenPreview( - @PreviewParameter(EditProfilePreviewProvider::class) - editProfileUiState: EditProfileUiState, -) { - MifosTheme { - EditProfileScreen( - editProfileUiState = editProfileUiState, - updateSuccess = false, - onBackClick = {}, - updateEmail = {}, - updateMobile = {}, - uri = null, - ) - } -} diff --git a/feature/profile/src/main/kotlin/org/mifospay/feature/profile/edit/EditProfileScreenImage.kt b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/edit/EditProfileScreenImage.kt deleted file mode 100644 index a330307b9..000000000 --- a/feature/profile/src/main/kotlin/org/mifospay/feature/profile/edit/EditProfileScreenImage.kt +++ /dev/null @@ -1,82 +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.profile.edit - -import android.net.Uri -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.IconButtonDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import org.mifospay.core.designsystem.icon.MifosIcons - -@Composable -fun EditProfileScreenImage( - modifier: Modifier = Modifier, - imageUri: Uri? = null, - onCameraIconClick: () -> Unit, -) { - Column(modifier.fillMaxSize()) { - Box( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(bottom = 32.dp), - ) { - Box( - modifier = Modifier - .padding(top = 32.dp) - .size(200.dp) - .clip(CircleShape) - .border(2.dp, MaterialTheme.colorScheme.primary, CircleShape), - contentAlignment = Alignment.Center, - ) { - AsyncImage( - model = imageUri, - modifier = Modifier - .size(200.dp) - .clip(CircleShape), - contentScale = ContentScale.Crop, - contentDescription = null, - ) - } - - IconButton( - onClick = onCameraIconClick, - modifier = Modifier - .size(48.dp) - .clip(CircleShape) - .align(Alignment.BottomEnd), - colors = IconButtonDefaults.iconButtonColors(MaterialTheme.colorScheme.primary), - ) { - Icon( - painter = rememberVectorPainter(MifosIcons.Camera), - contentDescription = null, - modifier = Modifier - .size(24.dp), - tint = MaterialTheme.colorScheme.onPrimary, - ) - } - } - } -} diff --git a/feature/profile/src/main/kotlin/org/mifospay/feature/profile/edit/EditProfileViewModel.kt b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/edit/EditProfileViewModel.kt deleted file mode 100644 index d35de05d6..000000000 --- a/feature/profile/src/main/kotlin/org/mifospay/feature/profile/edit/EditProfileViewModel.kt +++ /dev/null @@ -1,114 +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.profile.edit - -import android.graphics.Bitmap -import androidx.lifecycle.ViewModel -import com.mifospay.core.model.domain.user.UpdateUserEntityEmail -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.client.UpdateClient -import org.mifospay.core.data.domain.usecase.user.UpdateUser -import org.mifospay.core.datastore.PreferencesHelper -import org.mifospay.feature.profile.edit.EditProfileUiState.Loading - -class EditProfileViewModel( - private val mUseCaseHandler: UseCaseHandler, - private val mPreferencesHelper: PreferencesHelper, - private val updateUserUseCase: UpdateUser, - private val updateClientUseCase: UpdateClient, -) : ViewModel() { - - private val mEditProfileUiState = MutableStateFlow(Loading) - val editProfileUiState: StateFlow = mEditProfileUiState - - private val mUpdateSuccess = MutableStateFlow(false) - val updateSuccess: StateFlow = mUpdateSuccess - - init { - fetchProfileDetails() - } - - private fun fetchProfileDetails() { - val name = mPreferencesHelper.fullName ?: "-" - val username = mPreferencesHelper.username - val email = mPreferencesHelper.email ?: "-" - val vpa = mPreferencesHelper.clientVpa ?: "-" - val mobile = mPreferencesHelper.mobile ?: "-" - - mEditProfileUiState.value = EditProfileUiState.Success( - name = name, - username = username, - email = email, - vpa = vpa, - mobile = mobile, - ) - } - - fun updateEmail(email: String?) { - mUseCaseHandler.execute( - updateUserUseCase, - UpdateUser.RequestValues( - UpdateUserEntityEmail( - email, - ), - mPreferencesHelper.userId.toInt(), - ), - object : UseCase.UseCaseCallback { - override fun onSuccess(response: UpdateUser.ResponseValue?) { - mPreferencesHelper.saveEmail(email) - mEditProfileUiState.value = EditProfileUiState.Success(email = email!!) - mUpdateSuccess.value = true - } - - override fun onError(message: String) { - mUpdateSuccess.value = false - } - }, - ) - } - - fun updateMobile(fullNumber: String?) { - mUseCaseHandler.execute( - updateClientUseCase, - UpdateClient.RequestValues( - org.mifospay.core.model.utils.client.UpdateClientEntityMobile( - fullNumber!!, - ), - mPreferencesHelper.clientId.toInt().toLong(), - ), - object : UseCase.UseCaseCallback { - override fun onSuccess(response: UpdateClient.ResponseValue) { - mPreferencesHelper.saveMobile(fullNumber) - mEditProfileUiState.value = EditProfileUiState.Success(mobile = fullNumber) - mUpdateSuccess.value = true - } - - override fun onError(message: String) { - mUpdateSuccess.value = false - } - }, - ) - } -} - -sealed interface EditProfileUiState { - data object Loading : EditProfileUiState - data class Success( - val bitmapImage: Bitmap? = null, - val name: String = "", - var username: String = "", - val email: String = "", - val vpa: String = "", - val mobile: String = "", - ) : EditProfileUiState -} diff --git a/feature/profile/src/main/kotlin/org/mifospay/feature/profile/navigation/ProfileNavigation.kt b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/navigation/ProfileNavigation.kt deleted file mode 100644 index 8bcd69d08..000000000 --- a/feature/profile/src/main/kotlin/org/mifospay/feature/profile/navigation/ProfileNavigation.kt +++ /dev/null @@ -1,32 +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.profile.navigation - -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavOptions -import androidx.navigation.compose.composable -import org.mifospay.feature.profile.ProfileRoute - -const val PROFILE_ROUTE = "profile_route" - -fun NavController.navigateToProfile(navOptions: NavOptions) = navigate(PROFILE_ROUTE, navOptions) - -fun NavGraphBuilder.profileScreen( - onEditProfile: () -> Unit, - onSettings: () -> Unit, -) { - composable(route = PROFILE_ROUTE) { - ProfileRoute( - onEditProfile = onEditProfile, - onSettings = onSettings, - ) - } -} diff --git a/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsViewModel.kt b/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsViewModel.kt index 08b9bc325..1515b6441 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsViewModel.kt @@ -109,7 +109,7 @@ class SettingsViewModel( // TODO:: this shouldn't work, we need account id to block account viewModelScope.launch { - val result = repository.blockAccount(state.client.clientId) + val result = repository.blockAccount(state.client.id) sendAction(DisableAccountResult(result)) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d75f55986..c4539c725 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,6 +35,7 @@ fineractSdk = "1.0.3" firebaseBom = "33.3.0" firebaseCrashlyticsPlugin = "3.0.2" firebasePerfPlugin = "1.4.2" +fileKit = "0.8.7" gmsPlugin = "4.4.2" googleOss = "17.1.0" googleOssPlugin = "0.10.6" @@ -63,7 +64,7 @@ logbackClassicVersion = "1.3.14" minSdk = "24" moduleGraph = "2.5.0" multiplatformSettings = "1.2.0" -mokoPermission="0.18.0" +mokoPermission = "0.18.0" okHttp3Version = "4.12.0" okioVersion = "3.9.1" playServicesAuthVersion = "21.2.0" @@ -89,7 +90,7 @@ wire = "5.0.0" zxingVersion = "3.5.3" lifecycle-viewmodel-compose = "2.8.2" navigation-compose = "2.8.0-alpha02" -windowsSizeClass="0.5.0" +windowsSizeClass = "0.5.0" composeJB = "1.7.0-rc01" composeLifecycle = "2.8.2" @@ -164,7 +165,7 @@ androidx-tracing-ktx = { group = "androidx.tracing", name = "tracing-ktx", versi androidx-ui-desktop = { group = "androidx.compose.ui", name = "ui-desktop", version.ref = "uiDesktopVersion" } -back-handler= {group="com.arkivanov.essenty", name="back-handler",version.ref="backHandlerVersion"} +back-handler = { group = "com.arkivanov.essenty", name = "back-handler", version.ref = "backHandlerVersion" } coil-core = { group = "io.coil-kt.coil3", name = "coil-core", version.ref = "coil" } coil-kt = { group = "io.coil-kt.coil3", name = "coil", version.ref = "coil" } @@ -180,6 +181,8 @@ detekt-formatting = { group = "io.gitlab.arturbosch.detekt", name = "detekt-form detekt-gradlePlugin = { group = "io.gitlab.arturbosch.detekt", name = "detekt-gradle-plugin", version.ref = "detekt" } twitter-detekt-compose = { group = "com.twitter.compose.rules", name = "detekt", version.ref = "twitter-detekt-compose" } +filekit-core = { group = "io.github.vinceglb", name = "filekit-core", version.ref = "fileKit" } +filekit-compose = { group = "io.github.vinceglb", name = "filekit-compose", version.ref = "fileKit" } firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics-ktx" } firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } @@ -283,8 +286,8 @@ multiplatform-settings-coroutines = { group = "com.russhwolf", name = "multiplat multiplatform-settings-serialization = { group = "com.russhwolf", name = "multiplatform-settings-serialization", version.ref = "multiplatformSettings" } multiplatform-settings-test = { group = "com.russhwolf", name = "multiplatform-settings-test", version.ref = "multiplatformSettings" } -moko-permission = {group="dev.icerock.moko", name="permissions", version.ref="mokoPermission"} -moko-permission-compose = {group="dev.icerock.moko", name="permissions-compose", version.ref="mokoPermission"} +moko-permission = { group = "dev.icerock.moko", name = "permissions", version.ref = "mokoPermission" } +moko-permission-compose = { group = "dev.icerock.moko", name = "permissions-compose", version.ref = "mokoPermission" } play-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "playServicesAuthVersion" } @@ -317,7 +320,7 @@ zxing = { group = "com.google.zxing", name = "core", version.ref = "zxingVersion fineract-api = { group = "io.github.niyajali", name = "fineract-client-kmp", version.ref = "fineractSdk" } fineract-sdk = { group = "com.github.openMF", name = "mifos-android-sdk-arch", version.ref = "fineractSdk" } -window-size = {group="dev.chrisbanes.material3",name="material3-window-size-class-multiplatform",version.ref="windowsSizeClass"} +window-size = { group = "dev.chrisbanes.material3", name = "material3-window-size-class-multiplatform", version.ref = "windowsSizeClass" } [bundles] androidx-compose-ui-test = [ diff --git a/mifospay-android/dependencies/prodReleaseRuntimeClasspath.tree.txt b/mifospay-android/dependencies/prodReleaseRuntimeClasspath.tree.txt index e97e8f714..040c96c33 100644 --- a/mifospay-android/dependencies/prodReleaseRuntimeClasspath.tree.txt +++ b/mifospay-android/dependencies/prodReleaseRuntimeClasspath.tree.txt @@ -2022,7 +2022,6 @@ | | +--- 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:faq | +--- project :feature:editpassword | | +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.6 (*) | | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6 (*) @@ -2054,7 +2053,7 @@ | | +--- 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:settings +| +--- project :feature:profile | | +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.6 (*) | | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6 (*) | | +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) @@ -2069,6 +2068,8 @@ | | +--- 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 (*) @@ -2082,8 +2083,20 @@ | | +--- 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.insert-koin:koin-compose-viewmodel:1.2.0-Beta4 -> 4.0.0-RC2 (*) -| | +--- io.insert-koin:koin-compose:1.2.0-Beta4 -> 4.0.0-RC2 (*) +| | +--- io.coil-kt.coil3:coil-compose-core:3.0.0-alpha10 (*) +| | +--- io.github.vinceglb:filekit-core:0.8.7 +| | | \--- io.github.vinceglb:filekit-core-android:0.8.7 +| | | +--- androidx.activity:activity-ktx:1.9.2 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| | | \--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0 (*) +| | +--- io.github.vinceglb:filekit-compose:0.8.7 +| | | \--- io.github.vinceglb:filekit-compose-android:0.8.7 +| | | +--- androidx.activity:activity-compose:1.9.2 (*) +| | | +--- io.github.vinceglb:filekit-core:0.8.7 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| | | +--- org.jetbrains.compose.runtime:runtime:1.6.11 -> 1.7.0-rc01 (*) +| | | +--- org.jetbrains.compose.ui:ui:1.6.11 -> 1.7.0-rc01 (*) +| | | \--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0 (*) | | \--- 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 (*) diff --git a/mifospay-android/dependencies/prodReleaseRuntimeClasspath.txt b/mifospay-android/dependencies/prodReleaseRuntimeClasspath.txt index c4a671ffd..76126aded 100644 --- a/mifospay-android/dependencies/prodReleaseRuntimeClasspath.txt +++ b/mifospay-android/dependencies/prodReleaseRuntimeClasspath.txt @@ -12,6 +12,7 @@ :feature:editpassword :feature:faq :feature:home +:feature:profile :feature:settings :libs:country-code-picker :libs:mifos-passcode @@ -258,6 +259,10 @@ io.coil-kt.coil3:coil-network-ktor3:3.0.0-alpha10 io.coil-kt.coil3:coil-svg-android:3.0.0-alpha10 io.coil-kt.coil3:coil-svg:3.0.0-alpha10 io.coil-kt.coil3:coil:3.0.0-alpha10 +io.github.vinceglb:filekit-compose-android:0.8.7 +io.github.vinceglb:filekit-compose:0.8.7 +io.github.vinceglb:filekit-core-android:0.8.7 +io.github.vinceglb:filekit-core:0.8.7 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 diff --git a/mifospay-shared/build.gradle.kts b/mifospay-shared/build.gradle.kts index d635b8572..d88f848e8 100644 --- a/mifospay-shared/build.gradle.kts +++ b/mifospay-shared/build.gradle.kts @@ -40,6 +40,7 @@ kotlin { api(projects.feature.settings) api(projects.feature.faq) api(projects.feature.editpassword) + api(projects.feature.profile) } 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 bc40481dd..e0a22243a 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 @@ -25,6 +25,7 @@ import org.mifospay.feature.auth.di.AuthModule import org.mifospay.feature.editpassword.di.EditPasswordModule import org.mifospay.feature.faq.di.FaqModule import org.mifospay.feature.home.di.HomeModule +import org.mifospay.feature.profile.di.ProfileModule import org.mifospay.feature.settings.di.SettingsModule import org.mifospay.shared.MifosPayViewModel @@ -54,6 +55,7 @@ object KoinModules { SettingsModule, FaqModule, EditPasswordModule, + ProfileModule, ) } 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 84dae5f3d..3ce338a9e 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 @@ -18,6 +18,7 @@ import org.mifospay.feature.faq.navigation.faqScreen import org.mifospay.feature.faq.navigation.navigateToFAQ import org.mifospay.feature.home.navigation.HOME_ROUTE import org.mifospay.feature.home.navigation.homeScreen +import org.mifospay.feature.profile.navigation.profileNavGraph import org.mifospay.feature.settings.navigation.settingsScreen import org.mifospay.shared.ui.MifosAppState @@ -58,5 +59,10 @@ internal fun MifosNavHost( navigateBack = navController::navigateUp, onLogOut = onClickLogout, ) + + profileNavGraph( + navController = navController, + onLinkBankAccount = {}, + ) } } diff --git a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosApp.kt b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosApp.kt index c088cd5c0..1a76f3888 100644 --- a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosApp.kt +++ b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosApp.kt @@ -59,6 +59,7 @@ import org.mifospay.core.designsystem.component.MifosNavigationRail import org.mifospay.core.designsystem.component.MifosNavigationRailItem import org.mifospay.core.designsystem.icon.MifosIcons import org.mifospay.core.designsystem.theme.LocalGradientColors +import org.mifospay.feature.profile.navigation.navigateToEditProfile import org.mifospay.feature.settings.navigation.navigateToSettings import org.mifospay.shared.navigation.MifosNavHost import org.mifospay.shared.utils.TopLevelDestination @@ -145,7 +146,9 @@ internal fun MifosApp( onNavigateToSettings = { appState.navController.navigateToSettings() }, - onNavigateToEditProfile = {}, + onNavigateToEditProfile = { + appState.navController.navigateToEditProfile() + }, destination = destination, ) } diff --git a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosAppState.kt b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosAppState.kt index 2e8b8b20d..425f3e00c 100644 --- a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosAppState.kt +++ b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosAppState.kt @@ -32,6 +32,9 @@ import kotlinx.datetime.TimeZone import org.mifospay.core.data.util.NetworkMonitor import org.mifospay.core.data.util.TimeZoneMonitor import org.mifospay.feature.home.navigation.HOME_ROUTE +import org.mifospay.feature.home.navigation.navigateToHome +import org.mifospay.feature.profile.navigation.PROFILE_ROUTE +import org.mifospay.feature.profile.navigation.navigateToProfile import org.mifospay.shared.utils.TopLevelDestination @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @@ -77,7 +80,7 @@ internal class MifosAppState( HOME_ROUTE -> TopLevelDestination.HOME // PAYMENTS_ROUTE -> TopLevelDestination.PAYMENTS // FINANCE_ROUTE -> TopLevelDestination.FINANCE -// PROFILE_ROUTE -> TopLevelDestination.PROFILE + PROFILE_ROUTE -> TopLevelDestination.PROFILE else -> null } @@ -121,10 +124,10 @@ internal class MifosAppState( } when (topLevelDestination) { -// TopLevelDestination.HOME -> navController.navigateToHome(topLevelNavOptions) + TopLevelDestination.HOME -> navController.navigateToHome(topLevelNavOptions) // TopLevelDestination.PAYMENTS -> navController.navigateToPayments(topLevelNavOptions) // TopLevelDestination.FINANCE -> navController.navigateToFinance(topLevelNavOptions) -// TopLevelDestination.PROFILE -> navController.navigateToProfile(topLevelNavOptions) + TopLevelDestination.PROFILE -> navController.navigateToProfile(topLevelNavOptions) else -> Unit } }