mirror of
https://github.com/openMF/mobile-wallet.git
synced 2026-02-06 11:36:57 +00:00
moved profile and editprofile to feature module (#1664)
This commit is contained in:
parent
85c15b4bd5
commit
7c5cd385d0
@ -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
1
feature/profile/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
||||
17
feature/profile/build.gradle.kts
Normal file
17
feature/profile/build.gradle.kts
Normal 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)
|
||||
}
|
||||
0
feature/profile/consumer-rules.pro
Normal file
0
feature/profile/consumer-rules.pro
Normal file
21
feature/profile/proguard-rules.pro
vendored
Normal file
21
feature/profile/proguard-rules.pro
vendored
Normal 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
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
4
feature/profile/src/main/AndroidManifest.xml
Normal file
4
feature/profile/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
@ -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 = {}
|
||||
)
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
25
feature/profile/src/main/res/values/strings.xml
Normal file
25
feature/profile/src/main/res/values/strings.xml
Normal 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>
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -45,4 +45,11 @@ include(":feature:kyc")
|
||||
include(":feature:savedcards")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
include(":feature:invoices")
|
||||
include(":feature:profile")
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user