diff --git a/core/designsystem/src/main/kotlin/org/mifospay/core/designsystem/icon/MifosIcons.kt b/core/designsystem/src/main/kotlin/org/mifospay/core/designsystem/icon/MifosIcons.kt
index fd0bb40c..71c7ac20 100644
--- a/core/designsystem/src/main/kotlin/org/mifospay/core/designsystem/icon/MifosIcons.kt
+++ b/core/designsystem/src/main/kotlin/org/mifospay/core/designsystem/icon/MifosIcons.kt
@@ -14,12 +14,16 @@ import androidx.compose.material.icons.outlined.AccountCircle
import androidx.compose.material.icons.outlined.Cancel
import androidx.compose.material.icons.outlined.Home
import androidx.compose.material.icons.outlined.Wallet
+import androidx.compose.material.icons.rounded.AccountBalance
import androidx.compose.material.icons.rounded.AccountCircle
import androidx.compose.material.icons.rounded.Add
+import androidx.compose.material.icons.rounded.Contacts
import androidx.compose.material.icons.rounded.Home
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material.icons.rounded.MoreVert
+import androidx.compose.material.icons.rounded.QrCode
import androidx.compose.material.icons.rounded.Search
+import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material.icons.rounded.SwapHoriz
import androidx.compose.material.icons.rounded.Wallet
import androidx.compose.ui.graphics.vector.ImageVector
@@ -51,4 +55,8 @@ object MifosIcons {
val PhotoLibrary = Icons.Filled.PhotoLibrary
val Delete = Icons.Filled.Delete
val RoundedInfo = Icons.Rounded.Info
+ val Contact = Icons.Rounded.Contacts
+ val Settings = Icons.Rounded.Settings
+ val QR = Icons.Rounded.QrCode
+ val Bank = Icons.Rounded.AccountBalance
}
diff --git a/feature/profile/.gitignore b/feature/profile/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/profile/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/profile/build.gradle.kts b/feature/profile/build.gradle.kts
new file mode 100644
index 00000000..2d35a373
--- /dev/null
+++ b/feature/profile/build.gradle.kts
@@ -0,0 +1,17 @@
+plugins {
+ alias(libs.plugins.mifospay.android.feature)
+ alias(libs.plugins.mifospay.android.library.compose)
+}
+
+android {
+ namespace = "org.mifospay.feature.profile"
+}
+
+dependencies {
+ implementation(projects.core.data)
+ implementation(libs.squareup.okhttp)
+ implementation(libs.compose.country.code.picker)
+ implementation(libs.compose.material)
+ implementation(libs.coil.kt.compose)
+ implementation(projects.mifospay)
+}
\ No newline at end of file
diff --git a/feature/profile/consumer-rules.pro b/feature/profile/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/feature/profile/proguard-rules.pro b/feature/profile/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/feature/profile/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/feature/profile/src/androidTest/java/org/mifospay/profile/ExampleInstrumentedTest.kt b/feature/profile/src/androidTest/java/org/mifospay/profile/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..5a659e98
--- /dev/null
+++ b/feature/profile/src/androidTest/java/org/mifospay/profile/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package org.mifospay.profile
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("org.mifospay.profile.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/feature/profile/src/main/AndroidManifest.xml b/feature/profile/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/feature/profile/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
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
new file mode 100644
index 00000000..56f1b450
--- /dev/null
+++ b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/ProfileItemCard.kt
@@ -0,0 +1,91 @@
+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
+fun ProfileItemCard(
+ modifier: Modifier = Modifier,
+ icon: ImageVector,
+ text: Int,
+ onClick: () -> Unit,
+) {
+ 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 = Color.Black
+ )
+ Text(
+ modifier = if (text == R.string.edit_profile || text == R.string.settings) Modifier
+ .padding(
+ start = 18.dp
+ )
+ .align(Alignment.CenterVertically) else Modifier,
+ text = stringResource(id = text),
+ style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.Medium)
+ )
+ if (text == R.string.edit_profile || text == R.string.settings) {
+ Spacer(modifier = Modifier.fillMaxWidth())
+ }
+ }
+}
+
+@Composable
+fun DetailItem(label: String, value: String) {
+ Text(
+ text = "$label: $value",
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier.padding(bottom = 12.dp)
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+fun PreviewProfileItemCard() {
+ MifosTheme {
+ ProfileItemCard(
+ icon = MifosIcons.Profile,
+ text = R.string.edit_profile,
+ onClick = {}
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun PreviewDetailItem() {
+ MifosTheme {
+ DetailItem(label = "Email", value = "john.doe@example.com")
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..ce78de19
--- /dev/null
+++ b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/ProfileScreen.kt
@@ -0,0 +1,189 @@
+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.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowDropDown
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+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.graphics.Color
+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.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import org.mifospay.core.designsystem.icon.MifosIcons
+import org.mifospay.core.ui.ProfileImage
+
+
+@Composable
+fun ProfileRoute(
+ viewModel: ProfileViewModel = hiltViewModel(),
+ onEditProfile: () -> Unit,
+ onSettings: () -> Unit,
+) {
+ val profileState by viewModel.profileState.collectAsStateWithLifecycle()
+ ProfileScreenContent(
+ profileState = profileState,
+ onEditProfile = onEditProfile,
+ onSettings = onSettings
+ )
+}
+
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+fun ProfileScreenContent(
+ profileState: ProfileUiState,
+ onEditProfile: () -> Unit,
+ onSettings: () -> Unit,
+) {
+ var showDetails by remember { mutableStateOf(false) }
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ ) {
+ when (profileState) {
+ ProfileUiState.Loading -> {}
+ 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),
+ )
+ IconButton(onClick = { showDetails = !showDetails }) {
+ Icon(
+ imageVector = Icons.Default.ArrowDropDown,
+ tint = Color.Black,
+ contentDescription = null
+ )
+ }
+ }
+
+ if (showDetails) {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ DetailItem(
+ label = stringResource(id = R.string.email),
+ value = profileState.email.toString()
+ )
+ DetailItem(
+ label = stringResource(id = R.string.vpa),
+ value = profileState.vpa.toString()
+ )
+ DetailItem(
+ label = stringResource(id = R.string.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.personal_qr_code,
+ onClick = {}
+ )
+
+ ProfileItemCard(
+ modifier = Modifier
+ .padding(start = 8.dp, bottom = 8.dp)
+ .weight(1f),
+ icon = MifosIcons.Bank,
+ text = R.string.link_bank_account,
+ onClick = {}
+ )
+
+ ProfileItemCard(
+ modifier = Modifier
+ .padding(top = 8.dp, bottom = 8.dp),
+ icon = MifosIcons.Contact,
+ text = R.string.edit_profile,
+ onClick = { onEditProfile.invoke() }
+ )
+
+ ProfileItemCard(
+ modifier = Modifier
+ .padding(top = 8.dp),
+ icon = MifosIcons.Settings,
+ text = R.string.settings,
+ onClick = { onSettings.invoke() }
+ )
+ }
+ }
+ }
+
+ else -> {}
+ }
+ }
+}
+
+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
+fun ProfileScreenPreview(
+ @PreviewParameter(ProfilePreviewProvider::class) profileState: ProfileUiState
+) {
+ ProfileScreenContent(
+ profileState = profileState,
+ onEditProfile = {},
+ onSettings = {}
+ )
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..a182b558
--- /dev/null
+++ b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/ProfileViewModel.kt
@@ -0,0 +1,90 @@
+package org.mifospay.feature.profile
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import okhttp3.ResponseBody
+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.datastore.PreferencesHelper
+import org.mifospay.common.DebugUtil
+import org.mifospay.core.data.repository.local.LocalRepository
+
+import javax.inject.Inject
+
+@HiltViewModel
+class ProfileViewModel @Inject constructor(
+ private val mUsecaseHandler: UseCaseHandler,
+ private val fetchClientImageUseCase: FetchClientImage,
+ private val localRepository: LocalRepository,
+ private val mPreferencesHelper: PreferencesHelper
+) : ViewModel() {
+
+ private val _profileState = MutableStateFlow(ProfileUiState.Loading)
+ val profileState: StateFlow get() = _profileState
+
+ 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 = _profileState.value as ProfileUiState.Success
+ _profileState.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 ?: "-"
+
+ _profileState.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/EditProfileActivity.kt b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/edit/EditProfileActivity.kt
new file mode 100644
index 00000000..00894319
--- /dev/null
+++ b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/edit/EditProfileActivity.kt
@@ -0,0 +1,23 @@
+package org.mifospay.feature.profile.edit
+
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatActivity
+import dagger.hilt.android.AndroidEntryPoint
+import org.mifospay.core.designsystem.theme.MifosTheme
+
+
+@AndroidEntryPoint
+class EditProfileActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ MifosTheme {
+ EditProfileScreenRoute(
+ onBackClick = { finish() }
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..e2988c99
--- /dev/null
+++ b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/edit/EditProfileScreen.kt
@@ -0,0 +1,443 @@
+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.material.TextFieldDefaults
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+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.graphics.Color
+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.core.content.FileProvider
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.togitech.ccp.component.TogiCountryCodePicker
+import org.mifospay.BuildConfig
+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.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
+import java.util.Objects
+
+@Composable
+fun EditProfileScreenRoute(
+ viewModel: EditProfileViewModel = hiltViewModel(),
+ onBackClick: () -> Unit,
+) {
+ val editProfileUiState by viewModel.editProfileUiState.collectAsStateWithLifecycle()
+ val updateSuccess by viewModel.updateSuccess.collectAsStateWithLifecycle()
+
+ val context = LocalContext.current
+ val file = createImageFile(context)
+ val uri = FileProvider.getUriForFile(
+ Objects.requireNonNull(context),
+ BuildConfig.APPLICATION_ID + ".provider", file
+ )
+
+ LaunchedEffect(key1 = true) {
+ viewModel.fetchProfileDetails()
+ }
+
+ EditProfileScreen(
+ editProfileUiState = editProfileUiState,
+ onBackClick = onBackClick,
+ updateEmail = { email ->
+ viewModel.updateEmail(email)
+ },
+ updateMobile = { mobile ->
+ viewModel.updateMobile(mobile)
+ },
+ updateSuccess = updateSuccess,
+ uri
+ )
+}
+
+@Composable
+fun EditProfileScreen(
+ editProfileUiState: EditProfileUiState,
+ onBackClick: () -> Unit,
+ updateEmail: (String) -> Unit,
+ updateMobile: (String) -> Unit,
+ updateSuccess: Boolean,
+ uri: Uri?
+) {
+ var showDiscardChangesDialog by rememberSaveable { mutableStateOf(false) }
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ ) {
+ MifosScaffold(
+ topBarTitle = R.string.edit_profile,
+ backPress = { showDiscardChangesDialog = true },
+ scaffoldContent = {
+ when (editProfileUiState) {
+ EditProfileUiState.Loading -> {
+ MfLoadingWheel(
+ contentDesc = stringResource(R.string.loading),
+ backgroundColor = Color.White
+ )
+ }
+
+ is EditProfileUiState.Success -> {
+ val initialUsername = editProfileUiState.username
+ val initialMobile = editProfileUiState.mobile
+ val initialVpa = editProfileUiState.vpa
+ val initialEmail = editProfileUiState.email
+
+ EditProfileScreenContent(
+ initialUsername,
+ initialMobile,
+ initialVpa,
+ initialEmail,
+ uri,
+ updateEmail = updateEmail,
+ updateMobile = updateMobile,
+ contentPadding = it,
+ onBackClick = onBackClick,
+ updateSuccess = updateSuccess
+ )
+ }
+ }
+ })
+
+ MifosDialogBox(
+ showDialogState = showDiscardChangesDialog,
+ onDismiss = { showDiscardChangesDialog = false },
+ title = R.string.discard_changes,
+ confirmButtonText = R.string.confirm_text,
+ onConfirm = {
+ showDiscardChangesDialog = false
+ onBackClick.invoke()
+ },
+ dismissButtonText = R.string.dismiss_text
+ )
+ }
+}
+
+@Composable
+fun EditProfileScreenContent(
+ initialUsername: String,
+ initialMobile: String,
+ initialVpa: String,
+ initialEmail: String,
+ uri: Uri?,
+ contentPadding: PaddingValues,
+ updateEmail: (String) -> Unit,
+ updateMobile: (String) -> Unit,
+ updateSuccess: Boolean,
+ onBackClick: () -> Unit
+) {
+ 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.permission_required,
+ description = R.string.approve_permission_description_camera,
+ confirmButtonText = R.string.proceed,
+ dismissButtonText = R.string.dismiss,
+ 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(
+ {
+ cameraLauncher.launch(uri)
+ showBottomSheet = false
+ },
+ {
+ galleryLauncher.launch("image/*")
+ showBottomSheet = false
+ },
+ {
+ imageUri = null
+ showBottomSheet = false
+ })
+ },
+ onDismiss = { showBottomSheet = false }
+ )
+ }
+ }
+ )
+ Box(
+ modifier = Modifier
+ .padding(contentPadding)
+ .fillMaxSize()
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.White)
+ .verticalScroll(rememberScrollState())
+ ) {
+ EditProfileScreenImage(
+ imageUri = imageUri,
+ onCameraIconClick = { showBottomSheet = true })
+ MfOutlinedTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 16.dp, end = 16.dp),
+ value = username,
+ label = stringResource(id = R.string.username),
+ onValueChange = { username = it }
+ )
+ MfOutlinedTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 16.dp, end = 16.dp),
+ value = email,
+ label = stringResource(id = R.string.email),
+ onValueChange = { email = it }
+ )
+ MfOutlinedTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 16.dp, end = 16.dp),
+ value = vpa,
+ label = stringResource(id = R.string.vpa),
+ onValueChange = { vpa = it }
+ )
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 16.dp, end = 16.dp)
+ ) {
+ val keyboardController = LocalSoftwareKeyboardController.current
+ if (LocalInspectionMode.current) {
+ Text("Placeholder for TogiCountryCodePicker")
+ } else {
+ TogiCountryCodePicker(
+ modifier = Modifier,
+ initialPhoneNumber = mobile,
+ autoDetectCode = true,
+ shape = RoundedCornerShape(3.dp),
+ colors = TextFieldDefaults.outlinedTextFieldColors(
+ focusedBorderColor = Color.Black
+ ),
+ onValueChange = { (code, phone), isValid ->
+ if (isValid) {
+ mobile = code + phone
+ }
+ },
+ label = { Text(stringResource(id = R.string.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.failed_to_save_changes, Toast.LENGTH_SHORT)
+ .show()
+ }
+ },
+ buttonText = R.string.save
+ )
+ }
+ }
+}
+
+private fun isDataSaveNecessary(input: String, initialInput: String): Boolean {
+ return input == initialInput
+}
+
+@Composable
+fun EditProfileBottomSheetContent(
+ onClickProfilePicture: () -> Unit,
+ onChangeProfilePicture: () -> Unit,
+ onRemoveProfilePicture: () -> Unit
+) {
+ Column(
+ modifier = Modifier
+ .background(Color.White)
+ .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.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.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.remove_profile_picture),
+ style = historyItemTextStyle
+ )
+ }
+ }
+}
+
+@Composable
+fun EditProfileSaveButton(onClick: () -> Unit, buttonText: Int) {
+ Button(
+ onClick = { onClick.invoke() },
+ colors = ButtonDefaults.buttonColors(Color.Black),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ shape = RoundedCornerShape(10.dp),
+ contentPadding = PaddingValues(12.dp)
+ ) {
+ Text(text = stringResource(id = buttonText), style = styleMedium16sp.copy(Color.White))
+ }
+}
+
+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
+ )
+}
+
+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,
+ onBackClick = {},
+ updateEmail = {},
+ updateMobile = {},
+ updateSuccess = false,
+ uri = null
+ )
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..c3eb7f3c
--- /dev/null
+++ b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/edit/EditProfileScreenImage.kt
@@ -0,0 +1,69 @@
+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.runtime.Composable
+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.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(imageUri: Uri?, 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, Color.Black, CircleShape),
+ contentAlignment = Alignment.Center
+ ) {
+ AsyncImage(
+ model = imageUri,
+ modifier = Modifier
+ .size(200.dp)
+ .clip(CircleShape),
+ contentScale = ContentScale.Crop,
+ contentDescription = null
+ )
+ }
+ IconButton(
+ onClick = { onCameraIconClick.invoke() },
+ modifier = Modifier
+ .size(48.dp)
+ .clip(CircleShape)
+ .align(Alignment.BottomEnd),
+ colors = IconButtonDefaults.iconButtonColors(Color.Black)
+ ) {
+ Icon(
+ painter = rememberVectorPainter(MifosIcons.Camera),
+ contentDescription = null,
+ modifier = Modifier
+ .size(24.dp),
+ tint = Color.White
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..82373266
--- /dev/null
+++ b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/edit/EditProfileViewModel.kt
@@ -0,0 +1,101 @@
+package org.mifospay.feature.profile.edit
+
+import android.graphics.Bitmap
+import androidx.lifecycle.ViewModel
+import com.mifospay.core.model.domain.user.UpdateUserEntityEmail
+import dagger.hilt.android.lifecycle.HiltViewModel
+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 javax.inject.Inject
+
+@HiltViewModel
+class EditProfileViewModel @Inject constructor(
+ private val mUseCaseHandler: UseCaseHandler,
+ private val mPreferencesHelper: PreferencesHelper,
+ private val updateUserUseCase: UpdateUser,
+ private val updateClientUseCase: UpdateClient,
+) : ViewModel() {
+
+ private val _editProfileUiState =
+ MutableStateFlow(EditProfileUiState.Loading)
+ val editProfileUiState: StateFlow = _editProfileUiState
+
+ private val _updateSuccess = MutableStateFlow(false)
+ val updateSuccess: StateFlow = _updateSuccess
+
+ fun fetchProfileDetails() {
+ val name = mPreferencesHelper.fullName ?: "-"
+ val username = mPreferencesHelper.username
+ val email = mPreferencesHelper.email ?: "-"
+ val vpa = mPreferencesHelper.clientVpa ?: "-"
+ val mobile = mPreferencesHelper.mobile ?: "-"
+
+ _editProfileUiState.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)
+ _editProfileUiState.value = EditProfileUiState.Success(email = email!!)
+ _updateSuccess.value = true
+ }
+
+ override fun onError(message: String) {
+ _updateSuccess.value = false
+ }
+ })
+ }
+
+ fun updateMobile(fullNumber: String?) {
+ mUseCaseHandler.execute(updateClientUseCase,
+ UpdateClient.RequestValues(
+ com.mifospay.core.model.domain.client.UpdateClientEntityMobile(
+ fullNumber!!
+ ),
+ mPreferencesHelper.clientId.toInt().toLong()
+ ),
+ object : UseCase.UseCaseCallback {
+ override fun onSuccess(response: UpdateClient.ResponseValue) {
+ mPreferencesHelper.saveMobile(fullNumber)
+ _editProfileUiState.value = EditProfileUiState.Success(mobile = fullNumber)
+ _updateSuccess.value = true
+ }
+
+ override fun onError(message: String) {
+ _updateSuccess.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
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..1ec4fc25
--- /dev/null
+++ b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/navigation/ProfileNavigation.kt
@@ -0,0 +1,23 @@
+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/profile/src/main/res/values/strings.xml b/feature/profile/src/main/res/values/strings.xml
new file mode 100644
index 00000000..87dd0fd2
--- /dev/null
+++ b/feature/profile/src/main/res/values/strings.xml
@@ -0,0 +1,25 @@
+
+
+ E-mail
+ VPA
+ Mobile number
+ Personal QR Code
+ Link Bank Account
+ Edit Profile
+ Settings
+ Loading
+ Do you want to discard changes?
+ Discard
+ Cancel
+ Permissions Required
+ You need to approve these permissions in order to access the camera.
+ Proceed
+ Dismiss
+ Username
+ Phone Number
+ Failed To Save Changes
+ Save
+ Click profile picture
+ Pick profile picture from device
+ Remove profile picture
+
\ No newline at end of file
diff --git a/feature/profile/src/test/java/org/mifospay/profile/ExampleUnitTest.kt b/feature/profile/src/test/java/org/mifospay/profile/ExampleUnitTest.kt
new file mode 100644
index 00000000..ab70f76c
--- /dev/null
+++ b/feature/profile/src/test/java/org/mifospay/profile/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package org.mifospay.profile
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index da057630..eaaa064b 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -45,4 +45,11 @@ include(":feature:kyc")
include(":feature:savedcards")
+
+
+
+
+
include(":feature:invoices")
+include(":feature:profile")
+