moved profile and editprofile to feature module (#1664)

This commit is contained in:
Aditya Kumdale 2024-06-19 08:05:13 +05:30 committed by GitHub
parent 85c15b4bd5
commit 7c5cd385d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1153 additions and 0 deletions

View File

@ -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
}

1
feature/profile/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -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)
}

View File

21
feature/profile/proguard-rules.pro vendored Normal file
View File

@ -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

View File

@ -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)
}
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@ -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")
}
}

View File

@ -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<ProfileUiState> {
override val values: Sequence<ProfileUiState>
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 = {}
)
}

View File

@ -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>(ProfileUiState.Loading)
val profileState: StateFlow<ProfileUiState> get() = _profileState
init {
fetchClientImage()
fetchProfileDetails()
}
private fun fetchClientImage() {
viewModelScope.launch {
mUsecaseHandler.execute(fetchClientImageUseCase,
FetchClientImage.RequestValues(localRepository.clientDetails.clientId),
object : UseCase.UseCaseCallback<FetchClientImage.ResponseValue> {
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()
}

View File

@ -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() }
)
}
}
}
}

View File

@ -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<Uri?>(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<EditProfileUiState> {
override val values: Sequence<EditProfileUiState>
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
)
}
}

View File

@ -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
)
}
}
}
}

View File

@ -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>(EditProfileUiState.Loading)
val editProfileUiState: StateFlow<EditProfileUiState> = _editProfileUiState
private val _updateSuccess = MutableStateFlow(false)
val updateSuccess: StateFlow<Boolean> = _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<UpdateUser.ResponseValue?> {
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<UpdateClient.ResponseValue> {
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
}

View File

@ -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
)
}
}

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="email">E-mail</string>
<string name="vpa">VPA</string>
<string name="mobile">Mobile number</string>
<string name="personal_qr_code">Personal QR Code</string>
<string name="link_bank_account">Link Bank Account</string>
<string name="edit_profile">Edit Profile</string>
<string name="settings">Settings</string>
<string name="loading">Loading</string>
<string name="discard_changes">Do you want to discard changes?</string>
<string name="confirm_text">Discard</string>
<string name="dismiss_text">Cancel</string>
<string name="permission_required">Permissions Required</string>
<string name="approve_permission_description_camera">You need to approve these permissions in order to access the camera.</string>
<string name="proceed">Proceed</string>
<string name="dismiss">Dismiss</string>
<string name="username">Username</string>
<string name="phone_number">Phone Number</string>
<string name="failed_to_save_changes">Failed To Save Changes</string>
<string name="save">Save</string>
<string name="click_profile_picture">Click profile picture</string>
<string name="change_profile_picture">Pick profile picture from device</string>
<string name="remove_profile_picture">Remove profile picture</string>
</resources>

View File

@ -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)
}
}

View File

@ -45,4 +45,11 @@ include(":feature:kyc")
include(":feature:savedcards")
include(":feature:invoices")
include(":feature:profile")