From d745815f743ef96d06a9de232a97281229ec2068 Mon Sep 17 00:00:00 2001 From: moon <56061215+MgKyawLay@users.noreply.github.com> Date: Wed, 10 Jun 2026 20:52:51 +0630 Subject: [PATCH] password screen added --- .../main/java/com/mob/utsmyanmar/AGENTS.md | 6 +- .../ui/dashboard/DashboardScreen2.kt | 2 +- .../utsmyanmar/ui/navigation/AppNavGraph.kt | 57 +++- .../mob/utsmyanmar/ui/navigation/Routes.kt | 4 + .../ui/password_input/PasswordInput.kt | 303 ++++++++++++++++++ 5 files changed, 353 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/com/mob/utsmyanmar/ui/password_input/PasswordInput.kt diff --git a/app/src/main/java/com/mob/utsmyanmar/AGENTS.md b/app/src/main/java/com/mob/utsmyanmar/AGENTS.md index c0ef129..898970e 100644 --- a/app/src/main/java/com/mob/utsmyanmar/AGENTS.md +++ b/app/src/main/java/com/mob/utsmyanmar/AGENTS.md @@ -1,3 +1,7 @@ ## Working agreements -- use Scaffold and AppBar() in every main Screen \ No newline at end of file +- use Scaffold and AppBar() in every main Screen + +### Re-usable Screens +- PasswordInput.kt +- InputAmountScreen.kt \ No newline at end of file diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen2.kt b/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen2.kt index 3b5ee78..e071274 100644 --- a/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen2.kt +++ b/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen2.kt @@ -93,7 +93,7 @@ fun DashboardScreen2( deviceInfoViewModel.loadDeviceInfo() } - val drawerState = rememberDrawerState(initialValue = DrawerValue.Open) + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val scope = rememberCoroutineScope() val mainHandler = remember { Handler(Looper.getMainLooper()) } var showHostActionDialog by remember { mutableStateOf(false) } diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/navigation/AppNavGraph.kt b/app/src/main/java/com/mob/utsmyanmar/ui/navigation/AppNavGraph.kt index e1b3e35..a88953d 100644 --- a/app/src/main/java/com/mob/utsmyanmar/ui/navigation/AppNavGraph.kt +++ b/app/src/main/java/com/mob/utsmyanmar/ui/navigation/AppNavGraph.kt @@ -1,49 +1,51 @@ package com.mob.utsmyanmar.ui.navigation import android.annotation.SuppressLint +import android.net.Uri import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.navArgument import com.mob.utsmyanmar.model.ProcessCode -import com.mob.utsmyanmar.ui.input_amount.AmountRoute import com.mob.utsmyanmar.ui.cardwaiting.CardWaitingScreen import com.mob.utsmyanmar.ui.cardwaiting.CardWaitingViewModel import com.mob.utsmyanmar.ui.dashboard.DashboardScreen2 import com.mob.utsmyanmar.ui.dashboard.SeeMoreScreen import com.mob.utsmyanmar.ui.device_info.DeviceInfoViewModel +import com.mob.utsmyanmar.ui.functions.FunctionsScreen +import com.mob.utsmyanmar.ui.input_amount.AmountRoute +import com.mob.utsmyanmar.ui.password_input.InputPassword +import com.mob.utsmyanmar.ui.password_input.PasswordType import com.mob.utsmyanmar.ui.pinpad.PinPadRoute +import com.mob.utsmyanmar.ui.pinpad.PinPadViewModel +import com.mob.utsmyanmar.ui.print_receipt.PrintReceiptScreen import com.mob.utsmyanmar.ui.processing_card.ProcessingCardRoute import com.mob.utsmyanmar.ui.processing_card.ProcessingCardViewModel -import com.mob.utsmyanmar.ui.print_receipt.PrintReceiptScreen import com.mob.utsmyanmar.ui.refund_rrn.InputRrnRoute -import com.mob.utsmyanmar.ui.sign_on.SignOnResultScreen -import com.mob.utsmyanmar.ui.sign_on.SignOnRoute +import com.mob.utsmyanmar.ui.sale_void.TranDetailPage +import com.mob.utsmyanmar.ui.sale_void.VoidTraceScreen +import com.mob.utsmyanmar.ui.sale_void.VoidViewModel import com.mob.utsmyanmar.ui.sending_to_host.ProcessingRoute import com.mob.utsmyanmar.ui.settlement.SettlementScreen -import com.mob.utsmyanmar.ui.transaction_result.TransactionResultRoute -import com.mob.utsmyanmar.ui.sale_void.TranDetailPage -import com.mob.utsmyanmar.ui.sale_void.VoidViewModel -import com.mob.utsmyanmar.ui.sale_void.VoidTraceScreen -import com.mob.utsmyanmar.viewmodel.CardReaderViewModel -import com.mob.utsmyanmar.viewmodel.EmvTransactionProcessViewModel -import com.mob.utsmyanmar.ui.pinpad.PinPadViewModel import com.mob.utsmyanmar.ui.settlement.SettlementViewModel -import com.mob.utsmyanmar.ui.transaction_result.TransactionResultEvent -import com.mob.utsmyanmar.ui.transaction_result.TransactionResultViewModel -import com.mob.utsmyanmar.ui.functions.FunctionsScreen +import com.mob.utsmyanmar.ui.sign_on.SignOnResultScreen +import com.mob.utsmyanmar.ui.sign_on.SignOnRoute import com.mob.utsmyanmar.ui.tms_setup.TmsSetupRoute import com.mob.utsmyanmar.ui.tms_setup.TmsSetupViewModel +import com.mob.utsmyanmar.ui.transaction_result.TransactionResultEvent +import com.mob.utsmyanmar.ui.transaction_result.TransactionResultRoute +import com.mob.utsmyanmar.ui.transaction_result.TransactionResultViewModel import com.mob.utsmyanmar.ui.version.VersionScreen +import com.mob.utsmyanmar.viewmodel.CardReaderViewModel +import com.mob.utsmyanmar.viewmodel.EmvTransactionProcessViewModel import com.mob.utsmyanmar.viewmodel.SharedViewModel import com.mob.utsmyanmar.viewmodel.TransProcessViewModel -import com.utsmyanmar.ecr.data.TransType import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType @SuppressLint("ContextCastToActivity") @@ -104,7 +106,7 @@ fun AppNavGraph( navController.navigate(Routes.Version.route) }, onNavigateFunctions = { - navController.navigate(Routes.Functions.route) { + navController.navigate(Routes.Password.createRoute(Routes.Functions.route, PasswordType.SETTING)) { launchSingleTop = true } } @@ -136,6 +138,27 @@ fun AppNavGraph( ) } + composable( + route = Routes.Password.route, + arguments = listOf( + navArgument("destination") { type = NavType.StringType }, + navArgument("passwordType") { type = NavType.StringType } + ) + ) { backStackEntry -> + val destination = Uri.decode(backStackEntry.arguments?.getString("destination").orEmpty()) + val passwordType = backStackEntry.arguments?.getString("passwordType").orEmpty() + InputPassword( + passwordType = passwordType, + onBack = { navController.popBackStack() }, + onPasswordCorrect = { + navController.navigate(destination) { + popUpTo(Routes.Password.route) { inclusive = true } + launchSingleTop = true + } + } + ) + } + composable(Routes.Functions.route) { FunctionsScreen( onBack = { navController.popBackStack() } diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/navigation/Routes.kt b/app/src/main/java/com/mob/utsmyanmar/ui/navigation/Routes.kt index c798df9..9da6e55 100644 --- a/app/src/main/java/com/mob/utsmyanmar/ui/navigation/Routes.kt +++ b/app/src/main/java/com/mob/utsmyanmar/ui/navigation/Routes.kt @@ -29,4 +29,8 @@ sealed class Routes(val route: String) { data object PrintReceipt : Routes("print_receipt") data object Version : Routes("version") data object Functions : Routes("functions") + data object Password : Routes("password/{destination}/{passwordType}") { + fun createRoute(destination: String, passwordType: String): String = + "password/${Uri.encode(destination)}/$passwordType" + } } diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/password_input/PasswordInput.kt b/app/src/main/java/com/mob/utsmyanmar/ui/password_input/PasswordInput.kt new file mode 100644 index 0000000..acf48ed --- /dev/null +++ b/app/src/main/java/com/mob/utsmyanmar/ui/password_input/PasswordInput.kt @@ -0,0 +1,303 @@ +package com.mob.utsmyanmar.ui.password_input + +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.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBackIosNew +import androidx.compose.material.icons.rounded.Backspace +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +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.draw.shadow +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.mob.utsmyanmar.ui.components.appbar.AppBar +import com.mob.utsmyanmar.ui.preview.P2Preview +import com.mob.utsmyanmar.ui.preview.P3Preview +import com.mob.utsmyanmar.ui.theme.Color + +object PasswordType { + const val SYSTEM = "system_password" + const val SETTLEMENT = "settlement_password" + const val SETTING = "setting_password" +} + +private val passwords = mapOf( + PasswordType.SYSTEM to "111111", + PasswordType.SETTING to "222222", + PasswordType.SETTLEMENT to "123456", +) + +private const val PASSWORD_LENGTH = 6 + +@Composable +fun InputPassword( + passwordType: String, + onBack: () -> Unit = {}, + onPasswordCorrect: () -> Unit = {} +) { + val expectedPassword = passwords[passwordType] ?: passwords[PasswordType.SETTING]!! + var input by remember { mutableStateOf("") } + var errorMessage by remember { mutableStateOf(null) } + + Scaffold( + topBar = { + AppBar(title = "Password", icon = Icons.Default.ArrowBackIosNew, onIconClick = onBack) + }, + containerColor = Color.IvoryBeige + ) { paddingValues -> + PasswordEntryScreen( + modifier = Modifier.padding(paddingValues), + input = input, + errorMessage = errorMessage, + onBack = onBack, + onConfirm = { + if (input == expectedPassword) { + onPasswordCorrect() + } else { + errorMessage = "Incorrect password" + input = "" + } + }, + onKey = { digit -> + if (input.length < PASSWORD_LENGTH) { + input += digit + errorMessage = null + } + }, + onDelete = { + input = input.dropLast(1) + errorMessage = null + } + ) + } +} + +@Composable +private fun PasswordEntryScreen( + input: String, + errorMessage: String?, + onBack: () -> Unit, + onConfirm: () -> Unit, + onKey: (String) -> Unit, + onDelete: () -> Unit, + modifier: Modifier = Modifier +) { + val keys = listOf( + listOf("1", "2", "3"), + listOf("4", "5", "6"), + listOf("7", "8", "9"), + listOf("", "0", "") + ) + + Box( + modifier = modifier + .fillMaxSize() + .background(Color.White) + .navigationBarsPadding() + .statusBarsPadding() + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 20.dp) + ) { + Text( + text = "Enter Password", + color = Color.Gray, + fontSize = 11.sp, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + PasswordDots( + filledCount = input.length, + totalCount = PASSWORD_LENGTH, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = errorMessage ?: "", + color = Color.LegacyRed, + fontSize = 11.sp, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + + Spacer(modifier = Modifier.height(24.dp)) + + NumericKeypad(keys = keys, onKeyClick = onKey) + + Spacer(modifier = Modifier.weight(1f)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Button( + onClick = onBack, + modifier = Modifier.weight(1f).height(56.dp), + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.White, + contentColor = Color.LegacyRed + ) + ) { + Text("Cancel") + } + + Button( + onClick = onConfirm, + modifier = Modifier.weight(1f).height(56.dp), + enabled = input.length == PASSWORD_LENGTH, + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.LegacyRed, + contentColor = Color.White, + disabledContainerColor = Color.LegacyRed.copy(alpha = 0.5f), + disabledContentColor = Color.White + ) + ) { + Text(text = "Next", fontSize = 14.sp, fontWeight = FontWeight.Medium) + } + } + + Spacer(modifier = Modifier.height(18.dp)) + } + + Card( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(top = 32.dp, end = 20.dp) + .clickable(enabled = input.isNotEmpty()) { onDelete() }, + shape = RoundedCornerShape(18.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 6.dp) + ) { + Icon( + imageVector = Icons.Rounded.Backspace, + contentDescription = "Delete", + tint = if (input.isNotEmpty()) Color.LegacyRed else Color.Gray, + modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp) + ) + } + } +} + +@Composable +private fun PasswordDots( + filledCount: Int, + totalCount: Int, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + repeat(totalCount) { index -> + Box( + modifier = Modifier + .size(14.dp) + .background( + color = if (index < filledCount) Color.LegacyRed else Color.Gray.copy(alpha = 0.3f), + shape = CircleShape + ) + ) + } + } +} + +@Composable +private fun NumericKeypad( + keys: List>, + onKeyClick: (String) -> Unit +) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + keys.forEach { row -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + row.forEach { key -> + KeypadButton( + text = key, + modifier = Modifier.weight(1f), + onClick = { onKeyClick(key) } + ) + } + } + } + } +} + +@Composable +private fun KeypadButton( + text: String, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + val enabled = text.isNotBlank() + + Box( + modifier = modifier + .height(66.dp) + .then( + if (enabled) Modifier.shadow(elevation = 2.dp, shape = RoundedCornerShape(8.dp), clip = false) + else Modifier + ) + .background( + color = if (enabled) Color.White else androidx.compose.ui.graphics.Color.Transparent, + shape = RoundedCornerShape(8.dp) + ) + .clickable(enabled = enabled) { onClick() }, + contentAlignment = Alignment.Center + ) { + Text( + text = text, + color = if (enabled) Color.LegacyRed else androidx.compose.ui.graphics.Color.Transparent, + fontSize = 24.sp, + fontWeight = FontWeight.Normal, + textAlign = TextAlign.Center + ) + } +} + +@P3Preview +@P2Preview +@Composable +fun PreviewInputPassword() { + InputPassword(passwordType = PasswordType.SETTING) +}