last sync animation

This commit is contained in:
moon 2026-06-16 13:31:54 +06:30
parent efad1b4e14
commit 211b092c2d
12 changed files with 1074 additions and 1157 deletions

View File

@ -1,16 +0,0 @@
package com.mob.utsmyanmar.ui.dashboard
import androidx.compose.runtime.Composable
@Composable
fun DashboardRoute(
onNavigateAmount: (String) -> Unit,
settlementEnabled: Boolean,
wavePayEnabled: Boolean,
) {
DashboardScreen(
settlementEnabled = settlementEnabled,
wavePayEnabled = wavePayEnabled,
onNavigateAmount = onNavigateAmount
)
}

View File

@ -1,928 +0,0 @@
package com.mob.utsmyanmar.ui.dashboard
import android.os.Handler
import android.os.Looper
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
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.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountBalanceWallet
import androidx.compose.material.icons.filled.BarChart
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.CreditCard
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.LockOpen
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.Replay
import androidx.compose.material.icons.filled.SwapHoriz
import androidx.compose.material.icons.filled.Sync
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.VerticalDivider
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import com.mob.utsmyanmar.R
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.device_info.DeviceInfoUiState
import com.mob.utsmyanmar.ui.preview.P2Preview
import com.mob.utsmyanmar.ui.preview.P3Preview
import com.mob.utsmyanmar.ui.theme.Color
import com.utsmyanmar.paylibs.sign_on.EchoTestProcess
import com.utsmyanmar.paylibs.sign_on.SignOnListener
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.milliseconds
@Composable
fun DashboardScreen2(
onNavigateAmount: (String) -> Unit = {},
onNavigateSignOn: () -> Unit = {},
onNavigateSettlement: () -> Unit = {},
onNavigateVersion: () -> Unit = {},
onNavigateFunctions: () -> Unit = {},
onNavigateAction: (String) -> Unit = {},
onNavigateNotifications: () -> Unit = {},
dashboardUiState: DashboardUiState = DashboardUiState(),
deviceInfo: DeviceInfoUiState = DeviceInfoUiState()
) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
val mainHandler = remember { Handler(Looper.getMainLooper()) }
var showHostActionDialog by remember { mutableStateOf(false) }
var activeHostAction by remember { mutableStateOf("Log-On") }
var isHostActionRunning by remember { mutableStateOf(false) }
var dialogMessage by remember { mutableStateOf("") }
var reversalEnabled by remember { mutableStateOf(runCatching { SystemParamsOperation.getInstance().isReversalOn }.getOrDefault(false)) }
val isOnline = true
fun confirmationMessage(action: String) = "Do you want to start ${action.lowercase()}?"
fun processingMessage(action: String) = "Sending ${action.lowercase()} request to host..."
fun successMessage(action: String) = "$action success."
fun failureMessage(action: String, resultCode: Int?) =
"$action failed. Response code: ${resultCode ?: -1}"
fun networkFailureMessage(action: String) = "Network error during $action."
fun openHostActionDialog(action: String) {
activeHostAction = action
isHostActionRunning = false
dialogMessage = confirmationMessage(action)
showHostActionDialog = true
}
if (showHostActionDialog) {
AlertDialog(
onDismissRequest = {
if (!isHostActionRunning) {
showHostActionDialog = false
}
}, title = {
Text(
text = activeHostAction, color = Color.LegacyRed, fontWeight = FontWeight.Bold
)
}, text = {
Text(text = dialogMessage)
}, confirmButton = {
TextButton(
onClick = {
if (isHostActionRunning) return@TextButton
if (dialogMessage != confirmationMessage(activeHostAction)) {
showHostActionDialog = false
dialogMessage = confirmationMessage(activeHostAction)
return@TextButton
}
isHostActionRunning = true
dialogMessage = processingMessage(activeHostAction)
val signOnProcess = EchoTestProcess.getInstance()
val request = when (activeHostAction) {
"Echo Test" -> signOnProcess.enqueue(false)
"Log-Off" -> signOnProcess.enqueueLogOff()
else -> signOnProcess.enqueueLogOn()
}
request.startSignOn(object : SignOnListener {
override fun onSuccessSignOn() {
mainHandler.post {
isHostActionRunning = false
dialogMessage = successMessage(activeHostAction)
}
}
override fun onFailureSignOn(resultCode: Int?) {
mainHandler.post {
isHostActionRunning = false
mainHandler.post {
isHostActionRunning = false
dialogMessage = failureMessage(activeHostAction, resultCode)
}
}
}
override fun onNetworkFailSignOn(message: String?) {
mainHandler.post {
isHostActionRunning = false
dialogMessage = message?.takeIf { it.isNotBlank() }
?: networkFailureMessage(activeHostAction)
}
}
})
}) {
Text(
text = when {
isHostActionRunning -> "Processing"
dialogMessage == confirmationMessage(activeHostAction) -> "Start"
else -> "Close"
}, color = Color.LegacyRed
)
}
}, dismissButton = {
if (!isHostActionRunning) {
TextButton(
onClick = {
showHostActionDialog = false
dialogMessage = confirmationMessage(activeHostAction)
}) {
Text(text = "Cancel", color = Color.Gray)
}
}
}, containerColor = Color.White
)
}
ModalNavigationDrawer(
drawerState = drawerState, drawerContent = {
ModalDrawerSheet(
modifier = Modifier.fillMaxWidth(0.78f),
drawerContainerColor = Color.White,
drawerShape = RoundedCornerShape(topEnd = 24.dp, bottomEnd = 24.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(Color.IvoryBeige)
.padding(horizontal = 20.dp, vertical = 28.dp)
) {
Row {
Box(
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.background(Color.White), contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(R.drawable.logo_mob),
contentDescription = "mob logo",
modifier = Modifier
.size(100.dp)
.padding(16.dp),
contentScale = ContentScale.Fit
)
}
Column {
Text(
text = "MOB Merchant",
color = Color.CrimsonRed,
fontSize = 12.sp,
fontWeight = FontWeight.Bold
)
Text(
text = "S/N:${deviceInfo.serialNumber}",
color = Color.Black,
fontSize = 12.sp,
fontWeight = FontWeight.Bold
)
Text(
text = if (isOnline) "Online" else "Offline",
color = if (isOnline) Color.Success else Color.Error,
fontSize = 12.sp,
fontWeight = FontWeight.Bold
)
}
}
}
Spacer(modifier = Modifier.height(10.dp))
Text(
text = "Connection Settings",
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(horizontal = 16.dp)
)
Item(
title = "Echo Test", subTitle = "Test Connection Status",
onClick = {
scope.launch { drawerState.close() }
openHostActionDialog("Echo Test")
},
leadingIcon = {
Icon(
painterResource(R.drawable.ic_up_down_arrow),
contentDescription = "icon",
tint = Color.LegacyRed
)
},
trailingIcon = {},
)
Item(
title = "Log-On", subTitle = "Log on to System",
onClick = {
scope.launch { drawerState.close() }
openHostActionDialog("Log-On")
},
leadingIcon = {
Icon(
painterResource(R.drawable.ic_lock),
contentDescription = "icon",
tint = Color.LegacyRed
)
},
trailingIcon = {},
)
Item(
title = "Log-Off", subTitle = "Log off from System",
onClick = {
scope.launch { drawerState.close() }
openHostActionDialog("Log-Off")
},
leadingIcon = {
Icon(
painterResource(R.drawable.ic_cancel_circle),
contentDescription = "icon",
tint = Color.LegacyRed
)
},
trailingIcon = {},
)
Item(
title = "Reversal", subTitle = "Enable / Disable Reversal",
onClick = {
reversalEnabled = !reversalEnabled
runCatching { SystemParamsOperation.getInstance().setReversalFlag(reversalEnabled) }
},
leadingIcon = {
Icon(
Icons.Default.Sync,
contentDescription = "icon",
tint = Color.LegacyRed
)
},
trailingIcon = {
Switch(
checked = reversalEnabled,
onCheckedChange = { isChecked ->
reversalEnabled = isChecked
runCatching { SystemParamsOperation.getInstance().setReversalFlag(isChecked) }
}
)
},
)
Text(
text = "System Management",
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(horizontal = 16.dp)
)
Item(
title = "Functions", subTitle = "System Function Settings",
onClick = {
scope.launch { drawerState.close() }
onNavigateFunctions()
},
leadingIcon = {
Icon(
painterResource(R.drawable.ic_four_boxes),
contentDescription = "icon",
tint = Color.LegacyRed,
modifier = Modifier.size(16.dp)
)
},
trailingIcon = {},
)
Item(
title = "Version", subTitle = "View App Version Info",
onClick = {
scope.launch { drawerState.close() }
onNavigateVersion()
},
leadingIcon = {
Icon(
painterResource(R.drawable.ic_version),
contentDescription = "icon",
tint = Color.LegacyRed
)
},
trailingIcon = {},
)
}
}) {
//body start
Scaffold(
containerColor = Color.IvoryBeige, topBar = {
AppBar(
title = "Dashboard",
icon = Icons.Default.Menu,
onIconClick = { scope.launch { drawerState.open() } },
actions = {
IconButton(onClick = onNavigateNotifications) {
Box {
Icon(
imageVector = Icons.Default.Notifications,
contentDescription = "Notifications",
tint = Color.White
)
Box(
modifier = Modifier
.size(8.dp)
.background(Color.GoldenGlow, CircleShape)
.align(Alignment.TopEnd)
.offset(x = 2.dp, y = (-2).dp)
)
}
}
}
)
}) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
) {
//top section
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
) {
AdvertisingArea()
}
//center section
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
SummaryCard()
}
//pager section
Box(
modifier = Modifier
.weight(1.3f)
.fillMaxWidth(),
) {
MenuPager(
items = buildMenuItems(
onNavigateAmount = onNavigateAmount,
onNavigateSignOn = onNavigateSignOn,
onNavigateSettlement = onNavigateSettlement,
onNavigateAction = onNavigateAction
),
modifier = Modifier.fillMaxSize()
)
}
//transactions section
// RecentTransactions(
// transactions = dashboardUiState.recentTransactions,
// modifier = Modifier
// .weight(1.2f)
// .fillMaxWidth()
// )
}
}
}
}
@Composable
private fun AdvertisingArea() {
val imageArray = listOf(
"https://i.ytimg.com/vi/eRUVxGRp1Ms/maxresdefault.jpg",
"https://i.ytimg.com/vi/AwvmgTPd7qw/maxresdefault.jpg",
"https://mma.prnewswire.com/media/2080956/SUNMI_3rd_generation_products_T3_PRO_series_V3_MIX.jpg?p=facebook"
)
val pageState = rememberPagerState(pageCount = { imageArray.size })
LaunchedEffect(pageState) {
while (true) {
delay(10000.milliseconds)
val nextPage = (pageState.currentPage + 1) % imageArray.size
pageState.animateScrollToPage(
page = nextPage, animationSpec = tween(durationMillis = 700)
)
}
}
Card(
modifier = Modifier.fillMaxSize(),
shape = RoundedCornerShape(0.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
) {
HorizontalPager(
state = pageState, modifier = Modifier.fillMaxSize()
) { page ->
AsyncImage(
model = imageArray[page],
contentDescription = "ads images",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
}
}
}
@Composable
private fun SummaryCard() {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(4.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Column(modifier = Modifier.weight(1f)) {
SummaryItem(
title = "Sales Today",
value = "MMK 2,000,000",
subtitle = "47 Transactions",
icon = Icons.Default.BarChart,
iconBg = Color.CrimsonRed
)
HorizontalDivider(
color = Color.Gray,
modifier = Modifier.padding(vertical = 6.dp, horizontal = 6.dp)
)
SummaryItem(
title = "Settlement",
value = "Completed",
subtitle = "Today 11:00 PM",
icon = Icons.Default.Check,
iconBg = Color.CrimsonRed
)
}
VerticalDivider(color = Color.Gray, modifier = Modifier.height(160.dp))
Column(
modifier = Modifier
.weight(0.75f)
.padding(start = 18.dp),
horizontalAlignment = Alignment.Start
) {
Row(verticalAlignment = Alignment.Bottom) {
Text(
text = "Last Sync", fontSize = 12.sp
)
IconCircle(Icons.Default.Sync, Color.CrimsonRed)
}
Spacer(Modifier.height(6.dp))
Text(
text = "5 mins ago",
fontSize = 18.sp,
color = Color.LegacyRed,
fontWeight = FontWeight.SemiBold
)
}
}
}
}
@Composable
private fun SummaryItem(
title: String,
value: String,
subtitle: String,
icon: ImageVector,
iconBg: androidx.compose.ui.graphics.Color
) {
Row(
verticalAlignment = Alignment.Top, modifier = Modifier.padding(6.dp)
) {
Column(modifier = Modifier.weight(1f)) {
Text(title, fontSize = 12.sp)
Text(
value, fontSize = 14.sp, color = Color.LegacyRed, fontWeight = FontWeight.Bold
)
Text(subtitle, fontSize = 12.sp)
}
IconCircle(icon, iconBg)
}
}
@Composable
private fun IconCircle(
icon: ImageVector, color: androidx.compose.ui.graphics.Color
) {
Box(
modifier = Modifier
.size(38.dp)
.clip(CircleShape)
.background(color.copy(alpha = 0.1f)),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = color,
modifier = Modifier.size(22.dp)
)
}
}
private class DashboardMenuItem(
val title: String,
val iconContent: @Composable () -> Unit,
val onClick: () -> Unit
)
@Composable
private fun buildMenuItems(
onNavigateAmount: (String) -> Unit,
onNavigateSignOn: () -> Unit,
onNavigateSettlement: () -> Unit,
onNavigateAction: (String) -> Unit
): List<DashboardMenuItem> = listOf(
DashboardMenuItem("Sale", { Icon(painterResource(R.drawable.ic_terminal), contentDescription = null, modifier = Modifier.size(40.dp), tint = Color.LegacyRed) }) { onNavigateAmount("Sale") },
DashboardMenuItem("MMQR", { Image(painter = painterResource(R.drawable.ic_mmqr_logo), contentDescription = null, modifier = Modifier.height(48.dp)) }) { },
DashboardMenuItem("History", { Icon(painterResource(R.drawable.ic_history), contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { },
DashboardMenuItem("Sign On", { Icon(painterResource(R.drawable.ic_sign_on), contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateSignOn() },
DashboardMenuItem("Settlement", { Icon(painterResource(R.drawable.ic_settlement), contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateSettlement() },
DashboardMenuItem("Void", { Icon(Icons.Default.Lock, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Void") },
DashboardMenuItem("Refund", { Icon(Icons.Default.Replay, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Refund") },
DashboardMenuItem("Pre-Auth", { Icon(Icons.Default.Lock, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Pre-Auth") },
DashboardMenuItem("Pre-Auth Void", { Icon(Icons.Default.LockOpen, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Pre-Auth Void") },
DashboardMenuItem("Pre-Auth Complete", { Icon(Icons.Default.CreditCard, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Pre-Auth Complete") },
DashboardMenuItem("Pre-Auth Complete Void", { Icon(Icons.Default.SwapHoriz, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Pre-Auth Complete Void") },
DashboardMenuItem("Cash Out", { Icon(Icons.Default.AccountBalanceWallet, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Cash Out") },
)
@Composable
private fun MenuPager(
items: List<DashboardMenuItem>,
modifier: Modifier = Modifier
) {
val pages = items.chunked(6)
val pagerState = rememberPagerState(pageCount = { pages.size })
Column(modifier = modifier) {
HorizontalPager(
state = pagerState,
modifier = Modifier.weight(1f)
) { pageIndex ->
MenuPage(items = pages[pageIndex])
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
repeat(pages.size) { index ->
val selected = pagerState.currentPage == index
Box(
modifier = Modifier
.padding(horizontal = 4.dp)
.size(if (selected) 8.dp else 6.dp)
.background(
color = if (selected) Color.LegacyRed else Color.Gray.copy(alpha = 0.4f),
shape = CircleShape
)
)
}
}
}
}
@Composable
private fun MenuPage(items: List<DashboardMenuItem>) {
Column(
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.padding(horizontal = 16.dp)
) {
Spacer(Modifier.height(8.dp))
items.chunked(3).forEach { rowItems ->
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
rowItems.forEach { item ->
MenuCard(
title = item.title,
icon = item.iconContent,
modifier = Modifier.weight(1f),
onClick = item.onClick
)
}
repeat(3 - rowItems.size) {
Spacer(modifier = Modifier.weight(1f))
}
}
}
}
}
@Composable
private fun MenuCard(
title: String,
icon: @Composable () -> Unit,
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null
) {
Card(
modifier = modifier
.height(100.dp)
.then(
if (onClick != null) {
Modifier.clickable(onClick = onClick)
} else {
Modifier
}
),
shape = RoundedCornerShape(14.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(5.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(6.dp)
) {
Column {
icon()
Spacer(modifier = Modifier.weight(1f))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = title,
fontSize = 12.sp,
color = Color.Black,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
modifier = Modifier.weight(1f)
)
Icon(
imageVector = Icons.Default.ChevronRight,
contentDescription = null,
tint = Color.LegacyRed,
modifier = Modifier.size(28.dp)
)
}
}
}
}
}
@Composable
private fun RecentTransactions(
transactions: List<TrnxRecord>,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Recent Transactions",
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp
)
}
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
color = Color.Gray.copy(alpha = 0.2f)
)
LazyColumn(modifier = Modifier.fillMaxSize()) {
if (transactions.isEmpty()) {
item {
Text(
text = "No recent transactions",
color = Color.Gray,
fontSize = 13.sp,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 24.dp)
)
}
} else {
items(items = transactions, key = { it.pid }) { record ->
TrnxRow(record = record)
}
}
}
}
}
@Composable
private fun TrnxRow(record: TrnxRecord) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 4.dp)
.clip(RoundedCornerShape(10.dp))
.background(Color.White)
.padding(horizontal = 12.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(36.dp)
.background(Color.LegacyRed.copy(alpha = 0.1f), CircleShape),
contentAlignment = Alignment.Center
) {
Text(
text = record.typeLabel.take(1),
color = Color.LegacyRed,
fontWeight = FontWeight.Bold,
fontSize = 15.sp
)
}
Spacer(Modifier.width(10.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = if (record.isVoided) "${record.typeLabel} (Voided)" else record.typeLabel,
fontSize = 13.sp,
fontWeight = FontWeight.SemiBold,
color = if (record.isVoided) Color.Gray else Color.Black
)
Text(
text = record.maskedCard,
fontSize = 11.sp,
color = Color.Gray
)
}
Column(horizontalAlignment = Alignment.End) {
Text(
text = record.amountDisplay,
fontSize = 13.sp,
fontWeight = FontWeight.Bold,
color = if (record.isVoided) Color.Gray else Color.LegacyRed
)
Text(
text = record.dateTime,
fontSize = 10.sp,
color = Color.Gray
)
}
}
}
private val previewTransactions = listOf(
TrnxRecord(1L, "Sale", "MMK 10,000", "**** 1234", "06/10 14:32", isVoided = false, isApproved = true),
TrnxRecord(2L, "Void", "MMK 5,500", "**** 5678", "06/10 13:10", isVoided = true, isApproved = false),
TrnxRecord(3L, "Refund", "MMK 2,000", "**** 9012", "06/09 09:45", isVoided = false, isApproved = true),
TrnxRecord(4L, "Sale", "MMK 30,000", "**** 3456", "06/09 08:00", isVoided = false, isApproved = true),
TrnxRecord(5L, "Settlement", "MMK 0", "----", "06/08 18:00", isVoided = false, isApproved = true),
)
@P3Preview
@P2Preview
@Composable
fun PreviewDashboardScreen2() {
DashboardScreen2(dashboardUiState = DashboardUiState(previewTransactions))
}
@Preview
@Composable
fun PreviewItem() {
Item(
onClick = {},
title = "title",
subTitle = "sub-title"
)
}
@Composable
fun Item(
onClick: () -> Unit,
title: String,
subTitle: String,
leadingIcon: (@Composable () -> Unit)? = null,
trailingIcon: (@Composable () -> Unit)? = null,
) {
Button(
onClick = onClick,
colors = ButtonDefaults.buttonColors(
containerColor = Color.White,
contentColor = Color.Black
),
modifier = Modifier.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxWidth()
) {
// Square icon background
leadingIcon?.let {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(16.dp)) // change to RectangleShape for sharp corners
.background(Color.LegacyRed.copy(alpha = 0.1f))
.padding(8.dp)
) {
it()
}
}
Spacer(modifier = Modifier.width(12.dp))
Column(
modifier = Modifier
.weight(1f)
.padding(horizontal = 8.dp)
) {
Text(text = title)
Text(
text = subTitle,
style = MaterialTheme.typography.bodySmall,
color = Color.Gray
)
}
trailingIcon?.invoke()
}
}
}

View File

@ -0,0 +1,66 @@
package com.mob.utsmyanmar.ui.disable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
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.R
import com.mob.utsmyanmar.ui.theme.Color
@Composable
fun DisableScreen(
message: String,
onRetry: () -> Unit
) {
Scaffold(
containerColor = Color.IvoryBeige
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
painter = painterResource(R.drawable.ic_alert_triangle),
contentDescription = null,
tint = Color.LegacyRed,
modifier = Modifier.size(100.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Terminal Disabled",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = Color.LegacyRed
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = message,
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 32.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Button(onClick = onRetry) {
Text(text = "Retry")
}
}
}
}

View File

@ -0,0 +1,2 @@
package com.mob.utsmyanmar.ui.disable

View File

@ -0,0 +1,2 @@
package com.mob.utsmyanmar.ui.disable

View File

@ -41,6 +41,7 @@ import com.mob.utsmyanmar.ui.settlement.SettlementScreen
import com.mob.utsmyanmar.ui.settlement.SettlementViewModel import com.mob.utsmyanmar.ui.settlement.SettlementViewModel
import com.mob.utsmyanmar.ui.sign_on.SignOnResultScreen import com.mob.utsmyanmar.ui.sign_on.SignOnResultScreen
import com.mob.utsmyanmar.ui.sign_on.SignOnRoute import com.mob.utsmyanmar.ui.sign_on.SignOnRoute
import com.mob.utsmyanmar.ui.disable.DisableScreen
import com.mob.utsmyanmar.ui.tms_setup.TmsSetupRoute import com.mob.utsmyanmar.ui.tms_setup.TmsSetupRoute
import com.mob.utsmyanmar.ui.tms_setup.TmsSetupViewModel import com.mob.utsmyanmar.ui.tms_setup.TmsSetupViewModel
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultEvent import com.mob.utsmyanmar.ui.transaction_result.TransactionResultEvent
@ -70,9 +71,31 @@ fun AppNavGraph(
viewModel = tmsSetupViewModel, viewModel = tmsSetupViewModel,
onNavigateDashboard = { onNavigateDashboard = {
navController.navigate(Routes.Dashboard.route) { navController.navigate(Routes.Dashboard.route) {
popUpTo(Routes.TmsSetup.route) { popUpTo(Routes.TmsSetup.route) { inclusive = true }
inclusive = true launchSingleTop = true
} }
},
onNavigateDisable = { message ->
navController.navigate(Routes.Disable.createRoute(message)) {
popUpTo(Routes.TmsSetup.route) { inclusive = true }
launchSingleTop = true
}
}
)
}
composable(
route = Routes.Disable.route,
arguments = listOf(
navArgument("message") { type = NavType.StringType }
)
) { backStackEntry ->
val message = Uri.decode(backStackEntry.arguments?.getString("message").orEmpty())
DisableScreen(
message = message,
onRetry = {
navController.navigate(Routes.TmsSetup.route) {
popUpTo(Routes.Disable.route) { inclusive = true }
launchSingleTop = true launchSingleTop = true
} }
} }

View File

@ -37,4 +37,7 @@ sealed class Routes(val route: String) {
data object NotificationDetail : Routes("notification_detail/{notificationId}") { data object NotificationDetail : Routes("notification_detail/{notificationId}") {
fun createRoute(id: Int): String = "notification_detail/$id" fun createRoute(id: Int): String = "notification_detail/$id"
} }
data object Disable : Routes("disable/{message}") {
fun createRoute(message: String): String = "disable/${Uri.encode(message)}"
}
} }

View File

@ -40,7 +40,8 @@ import com.mob.utsmyanmar.ui.theme.Color as AppColor
@Composable @Composable
fun TmsSetupRoute( fun TmsSetupRoute(
viewModel: TmsSetupViewModel, viewModel: TmsSetupViewModel,
onNavigateDashboard: () -> Unit onNavigateDashboard: () -> Unit,
onNavigateDisable: (String) -> Unit
) { ) {
val state by viewModel.uiState.collectAsState() val state by viewModel.uiState.collectAsState()
@ -48,6 +49,10 @@ fun TmsSetupRoute(
viewModel.navigateToDashboard.collect { onNavigateDashboard() } viewModel.navigateToDashboard.collect { onNavigateDashboard() }
} }
LaunchedEffect(viewModel) {
viewModel.navigateToDisable.collect { message -> onNavigateDisable(message) }
}
TmsSetupScreen( TmsSetupScreen(
state = state, state = state,
onRetry = viewModel::downloadConfigs, onRetry = viewModel::downloadConfigs,
@ -82,7 +87,11 @@ fun TmsSetupScreen(
) )
Text( Text(
text = if (state.isError) "Configuration Error" else "Setting Up Terminal", text = when {
state.isTerminalDisabled -> "Terminal Disabled"
state.isError -> "Configuration Error"
else -> "Setting Up Terminal"
},
fontSize = 22.sp, fontSize = 22.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = AppColor.LegacyRed, color = AppColor.LegacyRed,
@ -159,13 +168,15 @@ fun TmsSetupScreen(
Text(text = "Retry", fontSize = 16.sp) Text(text = "Retry", fontSize = 16.sp)
} }
OutlinedButton( if (!state.isTerminalDisabled) {
onClick = onSkip, OutlinedButton(
modifier = Modifier.fillMaxWidth(), onClick = onSkip,
shape = RoundedCornerShape(12.dp), modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.outlinedButtonColors(contentColor = AppColor.LegacyRed) shape = RoundedCornerShape(12.dp),
) { colors = ButtonDefaults.outlinedButtonColors(contentColor = AppColor.LegacyRed)
Text(text = "Skip", fontSize = 16.sp) ) {
Text(text = "Skip", fontSize = 16.sp)
}
} }
} }
} }

View File

@ -12,6 +12,7 @@ import com.utsmyanmar.baselib.BaseApplication
import com.utsmyanmar.baselib.emv.EmvParamOperation import com.utsmyanmar.baselib.emv.EmvParamOperation
import com.utsmyanmar.baselib.network.model.sirius.SiriusRequest import com.utsmyanmar.baselib.network.model.sirius.SiriusRequest
import com.utsmyanmar.baselib.repo.Repository import com.utsmyanmar.baselib.repo.Repository
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
@ -25,14 +26,17 @@ import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.json.JSONObject
import sunmi.sunmiui.utils.LogUtil import sunmi.sunmiui.utils.LogUtil
import javax.inject.Inject import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
data class TmsSetupUiState( data class TmsSetupUiState(
val isLoading: Boolean = false, val isLoading: Boolean = false,
val statusText: String = "Initializing...", val statusText: String = "Initializing...",
val isError: Boolean = false, val isError: Boolean = false,
val errorMessage: String = "" val errorMessage: String = "",
val isTerminalDisabled: Boolean = false
) )
@HiltViewModel @HiltViewModel
@ -47,6 +51,9 @@ class TmsSetupViewModel @Inject constructor(
private val _navigateToDashboard = MutableSharedFlow<Unit>() private val _navigateToDashboard = MutableSharedFlow<Unit>()
val navigateToDashboard: SharedFlow<Unit> = _navigateToDashboard.asSharedFlow() val navigateToDashboard: SharedFlow<Unit> = _navigateToDashboard.asSharedFlow()
private val _navigateToDisable = MutableSharedFlow<String>()
val navigateToDisable: SharedFlow<String> = _navigateToDisable.asSharedFlow()
private val disposables = CompositeDisposable() private val disposables = CompositeDisposable()
private val tmsSetups = TMSSetupsImpl() private val tmsSetups = TMSSetupsImpl()
@ -82,12 +89,19 @@ class TmsSetupViewModel @Inject constructor(
onConfigApplied() onConfigApplied()
}, },
{ error -> { error ->
val errorMessage = if(error is retrofit2.HttpException){
// error.response()?.errorBody()?.toString() ?: error.message.toString()
val body = error.response()?.errorBody()?.toString()
JSONObject(body ?: "").getString("message")
}else{
error.message.toString()
}
_uiState.update { _uiState.update {
it.copy( it.copy(
isLoading = false,
isError = true, isError = true,
statusText = "Download failed", isLoading = false,
errorMessage = formatNetworkError(error) statusText = "Download failed!",
errorMessage = errorMessage
) )
} }
} }
@ -100,12 +114,22 @@ class TmsSetupViewModel @Inject constructor(
_uiState.update { it.copy(isLoading = true, statusText = "Starting hardware...") } _uiState.update { it.copy(isLoading = true, statusText = "Starting hardware...") }
var elapsed = 0 var elapsed = 0
while (BaseApplication.basicOptV2 == null && elapsed < 10_000) { while (BaseApplication.basicOptV2 == null && elapsed < 10_000) {
delay(500) delay(500.milliseconds)
elapsed += 500 elapsed += 500
} }
} }
private fun onConfigApplied() { private fun onConfigApplied() {
val ops = SystemParamsOperation.getInstance()
if (!ops.isActive) {
val msg = ops.disabledMsg.takeIf { it.isNotEmpty() }
?: "This terminal has been disabled. Please contact your administrator."
_uiState.update { it.copy(isLoading = false) }
viewModelScope.launch { _navigateToDisable.emit(msg) }
return
}
val validity = TMSUtil.getInstance().checkParams() val validity = TMSUtil.getInstance().checkParams()
if (validity.status == ValidityStatus.SUCCESS) { if (validity.status == ValidityStatus.SUCCESS) {
_uiState.update { it.copy(isLoading = false, statusText = "Ready.") } _uiState.update { it.copy(isLoading = false, statusText = "Ready.") }
@ -126,26 +150,26 @@ class TmsSetupViewModel @Inject constructor(
viewModelScope.launch { _navigateToDashboard.emit(Unit) } viewModelScope.launch { _navigateToDashboard.emit(Unit) }
} }
private fun formatNetworkError(error: Throwable): String { // private fun formatNetworkError(error: Throwable): String {
return when (error) { // return when (error) {
is javax.net.ssl.SSLHandshakeException -> // is javax.net.ssl.SSLHandshakeException ->
"SSL handshake failed: ${error.message ?: "Certificate or protocol mismatch"}" // "SSL handshake failed: ${error.message ?: "Certificate or protocol mismatch"}"
is javax.net.ssl.SSLException -> // is javax.net.ssl.SSLException ->
"SSL/TLS error: ${error.message ?: "Secure connection could not be established"}" // "SSL/TLS error: ${error.message ?: "Secure connection could not be established"}"
is java.security.cert.CertificateException -> // is java.security.cert.CertificateException ->
"Server certificate error: ${error.message ?: "Certificate is invalid or untrusted"}" // "Server certificate error: ${error.message ?: "Certificate is invalid or untrusted"}"
is java.net.UnknownHostException -> // is java.net.UnknownHostException ->
"Host not found: ${error.message ?: "Check server URL and network connection"}" // "Host not found: ${error.message ?: "Check server URL and network connection"}"
is java.net.ConnectException -> // is java.net.ConnectException ->
"Connection refused: ${error.message ?: "Server is unreachable"}" // "Connection refused: ${error.message ?: "Server is unreachable"}"
is java.net.SocketTimeoutException -> // is java.net.SocketTimeoutException ->
"Connection timed out: ${error.message ?: "Server did not respond in time"}" // "Connection timed out: ${error.message ?: "Server did not respond in time"}"
is retrofit2.HttpException -> // is retrofit2.HttpException ->
"HTTP ${error.code()} ${error.message()}" // "HTTP ${error.code()} ${error.message()}"
else -> // else ->
error.message ?: "Unknown network error" // error.message ?: "Unknown network error"
} // }
} // }
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
private fun buildRequest(): SiriusRequest { private fun buildRequest(): SiriusRequest {

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="800dp"
android:height="800dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group>
<clip-path
android:pathData="M0,0h24v24h-24z"/>
<path
android:pathData="M9.827,2.229C10.794,0.59 13.206,0.59 14.174,2.229L23.66,18.302C24.658,19.993 23.364,22 21.486,22H2.514C0.636,22 -0.658,19.993 0.34,18.302L9.827,2.229ZM10.059,7.055C10.027,6.482 10.483,6 11.057,6H12.943C13.517,6 13.973,6.482 13.941,7.055L13.552,14.056C13.523,14.585 13.085,15 12.554,15H11.446C10.915,15 10.477,14.585 10.448,14.056L10.059,7.055ZM14,18C14,19.105 13.105,20 12,20C10.895,20 10,19.105 10,18C10,16.895 10.895,16 12,16C13.105,16 14,16.895 14,18Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</group>
</vector>

View File

@ -3,15 +3,10 @@ package com.utsmyanmar.baselib;
import android.content.ContentValues; import android.content.ContentValues;
import android.os.RemoteException; import android.os.RemoteException;
import com.sunmi.pay.hardware.aidl.AidlConstants;
import com.sunmi.pay.hardware.aidlv2.AidlConstantsV2; import com.sunmi.pay.hardware.aidlv2.AidlConstantsV2;
import com.sunmi.pay.hardware.aidlv2.security.SecurityOptV2; import com.sunmi.pay.hardware.aidlv2.security.SecurityOptV2;
import com.utsmyanmar.paylibs.Constant;
import com.utsmyanmar.paylibs.utils.core_utils.ByteUtil; import com.utsmyanmar.paylibs.utils.core_utils.ByteUtil;
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation; import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation;
import com.utsmyanmar.paylibs.utils.secure.TriDes;
import java.security.GeneralSecurityException;
import sunmi.sunmiui.utils.LogUtil; import sunmi.sunmiui.utils.LogUtil;
@ -28,7 +23,7 @@ public final class TerminalKeyUtil {
SecurityOptV2 mSecurityOptV2 = BaseApplication.getInstance().mSecurityOptV2; SecurityOptV2 mSecurityOptV2 = BaseApplication.getInstance().mSecurityOptV2;
byte[] cvByte = ByteUtil.hexStr2Bytes("B7B520"); byte[] cvByte = ByteUtil.hexStr2Bytes("B7B520");
byte[] dataByte = ByteUtil.hexStr2Bytes("e121249099a677e8b7d4f6a9d49fe8d1".toUpperCase()); byte[] dataByte = ByteUtil.hexStr2Bytes("05FFB893726A5F1E59692D24E72E36AC".toUpperCase());
byte[] makBytes = ByteUtil.hexStr2Bytes("250738083EC15BD3BA67D66B8A7AA13B"); byte[] makBytes = ByteUtil.hexStr2Bytes("250738083EC15BD3BA67D66B8A7AA13B");
// byte[] makCvBytes = ByteUtil.hexStr2Bytes("204E449B97"); // byte[] makCvBytes = ByteUtil.hexStr2Bytes("204E449B97");