emv first upload

This commit is contained in:
kizzy 2026-07-08 13:59:26 +07:00
parent 665bf1d5df
commit 7c0e9c4e66
8 changed files with 888 additions and 95 deletions

View File

@ -0,0 +1,248 @@
package com.mob.utsmyanmar.ui.emv
import android.text.TextUtils
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.livedata.observeAsState
import com.mob.utsmyanmar.viewmodel.EmvTransactionProcessViewModel
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.utsmyanmar.baselib.util.enums.EmvResultStatus
import com.utsmyanmar.baselib.util.enums.PinUtil
import com.utsmyanmar.baselib.ui.CustomPinPadKeyboard
import com.utsmyanmar.checkxread.util.CardTypeX
import com.utsmyanmar.paylibs.utils.POSUtil
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
import sunmi.sunmiui.utils.LogUtil
private const val TAG = "EmvTransactionRoute"
private const val MAX_PIN_RETRY = 3
@Composable
fun EmvTransactionRoute(
emvViewModel: EmvTransactionProcessViewModel,
sharedViewModel: SharedViewModel,
onNavigateProcessing: () -> Unit,
onNavigateTransactionResult: () -> Unit,
onNavigateInputRrn: () -> Unit,
onNavigateCardWaiting: () -> Unit,
onNavigateError: (String) -> Unit,
onNavigateMain: () -> Unit
) {
val emvResultStatus by emvViewModel.emvResultStatus.observeAsState()
val candiList by emvViewModel.candiList.observeAsState()
val cardNum by emvViewModel.cardNum.observeAsState("")
val expDate by emvViewModel.expDate.observeAsState("")
val emvCardType by emvViewModel.emvCardType.observeAsState("")
val pinText by emvViewModel.pinText.observeAsState("")
val pinRemaining by emvViewModel.pinRemainingCount.observeAsState("")
val pinMode by emvViewModel.emvPinMode.observeAsState("")
val pinPadVisibility by emvViewModel.pinPadVisibility.observeAsState(8)
val errorCodeMsg by emvViewModel.errorCodeMsg.observeAsState()
var loadingMessage by remember { mutableStateOf<String?>(null) }
var pinRetryCount by remember { mutableIntStateOf(MAX_PIN_RETRY) }
val canGoBack = sharedViewModel.transactionsType.value != TransactionsType.SALE
fun goToMain() {
emvViewModel.onCancelEmvTransaction()
onNavigateMain()
}
BackHandler(enabled = canGoBack) {
goToMain()
}
fun startEmvProcess(keyboard: CustomPinPadKeyboard) {
emvViewModel.customPinPadKeyboard = keyboard
if (sharedViewModel.transactionsType.value == TransactionsType.PIN_CHANGE) {
emvViewModel.pinUtil.postValue(PinUtil.OLD_PIN)
}
val tradeData = emvViewModel.getTradeData()
if (tradeData == null) {
onNavigateError("Please try again")
return
}
val payDetail = tradeData.payDetail
val cardType = payDetail.cardType
payDetail.amount = POSUtil.getInstance().convertAmount(sharedViewModel.amount.value)
val processCode = sharedViewModel.processCode.value
if (!TextUtils.equals(processCode, "")) {
payDetail.processCode = processCode
}
if (cardType != CardTypeX.MANUAL.value) {
emvViewModel.startProcess()
} else {
onNavigateProcessing()
}
}
LaunchedEffect(emvResultStatus) {
val status = emvResultStatus ?: return@LaunchedEffect
LogUtil.d(TAG, "EMV Result Status : $status")
when (status) {
EmvResultStatus.SELECT_APP -> {
// Candidate list dialog is driven by candiList observer.
}
EmvResultStatus.READ_CARD -> {
sharedViewModel.setIsFallback(true)
onNavigateCardWaiting()
}
EmvResultStatus.ON_NEXT_SCREEN -> onNavigateInputRrn()
EmvResultStatus.READ_CARD_RETRY,
EmvResultStatus.READ_CARD_INTERFACE_NOT_FOUND,
EmvResultStatus.READ_CARD_NO_APP,
EmvResultStatus.READ_CARD_REFER_INSTRUCTION -> {
onNavigateCardWaiting()
}
EmvResultStatus.TRY_AGAIN -> {
sharedViewModel.set_errorFragmentMsg("Card Not Supported!")
onNavigateError("Card Not Supported!")
}
EmvResultStatus.OFFLINE_DECLINE -> {
sharedViewModel.set_errorFragmentMsg("Offline Denial")
onNavigateError("Offline Denial")
}
EmvResultStatus.CONFIRM_CODE_VERIFY -> onNavigateCardWaiting()
EmvResultStatus.SECONDARY -> {
loadingMessage = "Sending secondary....."
}
EmvResultStatus.PIN_MISMATCH -> {
sharedViewModel.set_errorFragmentMsg("PIN MISMATCH!")
onNavigateError("PIN MISMATCH!")
}
EmvResultStatus.PIN_EMPTY -> {
pinRetryCount--
if (pinRetryCount > 0) {
emvViewModel.pinRemainingCount.value = "$pinRetryCount LEFT"
emvViewModel.startPinProcess()
} else {
emvViewModel.setPinPadCancel()
LogUtil.d(TAG, "Pin Try limit reached")
}
}
EmvResultStatus.SUCCESS -> onNavigateProcessing()
EmvResultStatus.SUCCESS_ONLINE,
EmvResultStatus.FAIL_ONLINE -> {
sharedViewModel.payDetail.value = emvViewModel.payDetailResult.value
loadingMessage = null
onNavigateTransactionResult()
}
EmvResultStatus.REVERSAL_PROCESS -> {
loadingMessage = "Sending reversal....."
}
EmvResultStatus.REVERSAL_SECONDARY -> {
loadingMessage = "Sending reversal secondary..."
}
EmvResultStatus.REVERSAL_FAIL -> {
sharedViewModel.set_errorFragmentMsg("Reversal failed!")
onNavigateError("Reversal failed!")
}
EmvResultStatus.REVERSAL_SUCCESS -> {
sharedViewModel.set_successFragmentMsg("Reversal success!")
onNavigateMain()
}
EmvResultStatus.FAIL -> {
onNavigateError("Transaction failed")
}
EmvResultStatus.NETWORK_ERROR -> {
onNavigateError("Com Error!")
}
EmvResultStatus.ERROR -> {
loadingMessage = null
val mapped = mapEmvError(errorCodeMsg)
sharedViewModel.set_errorFragmentMsg(mapped)
onNavigateError(mapped)
}
else -> LogUtil.d(TAG, "Unhandled EmvResult Status : $status")
}
}
DisposableEffect(Unit) {
onDispose {
emvViewModel.resetTransactionStatus()
}
}
val state = EmvTransactionState(
cardNumber = cardNum.orEmpty(),
expDate = expDate.orEmpty(),
cardScheme = emvCardType.orEmpty(),
amount = formatAmount(sharedViewModel.amount.value),
pinText = pinText.orEmpty(),
pinRemaining = pinRemaining.orEmpty(),
pinMode = pinMode.orEmpty(),
showPinPad = pinPadVisibility == 0,
loadingMessage = loadingMessage,
candidateList = candiList?.toList()
)
EmvTransactionScreen(
state = state,
onCandidateSelected = { index ->
emvViewModel.setEmvAppSelect(index)
emvViewModel.candiList.value = null
},
onKeyboardReady = { keyboard ->
startEmvProcess(keyboard)
}
)
}
private fun mapEmvError(pair: String?): String {
if (pair.isNullOrEmpty()) {
return "Emv process timeout!"
}
if (!pair.contains(":")) {
return pair
}
val code = pair.substringBefore(":")
val msg = pair.substringAfter(":")
return if (code == "-4100" || code == "-4125") {
"Card read failed!"
} else {
"$code\n$msg"
}
}
private fun formatAmount(amount: String?): String {
val normalized = amount.orEmpty()
val value = normalized.toLongOrNull() ?: return normalized.ifBlank { "0" }
return "%,d".format(value)
}

View File

@ -0,0 +1,248 @@
package com.mob.utsmyanmar.ui.emv
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.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import com.mob.utsmyanmar.ui.theme.Black
import com.mob.utsmyanmar.ui.theme.Primary
import com.mob.utsmyanmar.ui.theme.White
import com.utsmyanmar.baselib.ui.CustomPinPadKeyboard
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EmvTransactionScreen(
state: EmvTransactionState,
onCandidateSelected: (Int) -> Unit,
onKeyboardReady: (CustomPinPadKeyboard) -> Unit
) {
var keyboardView by remember { mutableStateOf<CustomPinPadKeyboard?>(null) }
var isStarted by remember { mutableStateOf(false) }
LaunchedEffect(keyboardView) {
val keyboard = keyboardView ?: return@LaunchedEffect
if (!isStarted) {
isStarted = true
onKeyboardReady(keyboard)
}
}
Scaffold(
topBar = {
CenterAlignedTopAppBar(
title = {
Text(
text = "CARD TRANSACTION",
color = White,
style = MaterialTheme.typography.titleMedium
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Primary
)
)
},
containerColor = White
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.background(White)
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
CardInfoPanel(state)
Spacer(modifier = Modifier.height(20.dp))
if (state.loadingMessage != null) {
CircularProgressIndicator(color = Primary)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = state.loadingMessage,
color = Black,
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center
)
} else if (state.showPinPad) {
PinEntryPanel(state)
} else {
Text(
text = state.statusMessage,
color = Black,
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center
)
}
Spacer(modifier = Modifier.height(24.dp))
// The Sunmi SDK draws its secure keypad on top of this view. It is
// always present in the tree but only meaningful during PIN entry.
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
AndroidView(
factory = { context ->
CustomPinPadKeyboard(context).also {
keyboardView = it
}
}
)
}
}
}
val candidates = state.candidateList
if (candidates != null) {
AlertDialog(
onDismissRequest = { },
confirmButton = { },
title = {
Text(text = "Select Application")
},
text = {
Column {
candidates.forEachIndexed { index, name ->
Text(
text = name,
color = Black,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier
.fillMaxWidth()
.clickable { onCandidateSelected(index) }
.padding(vertical = 14.dp)
)
if (index != candidates.lastIndex) {
HorizontalDivider()
}
}
}
}
)
}
}
@Composable
private fun CardInfoPanel(state: EmvTransactionState) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(
color = Primary,
shape = RoundedCornerShape(24.dp)
)
.padding(horizontal = 24.dp, vertical = 24.dp)
) {
Column {
if (state.cardScheme.isNotBlank()) {
Text(
text = state.cardScheme,
color = White,
style = MaterialTheme.typography.labelLarge
)
Spacer(modifier = Modifier.height(10.dp))
}
Text(
text = state.cardNumber.ifBlank { "**** **** **** ****" },
color = White,
style = MaterialTheme.typography.headlineSmall,
letterSpacing = 2.sp
)
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Column {
Text(
text = "EXPIRY",
color = White,
style = MaterialTheme.typography.labelSmall
)
Text(
text = state.expDate.ifBlank { "--/--" },
color = White,
style = MaterialTheme.typography.bodyLarge
)
}
Column(horizontalAlignment = Alignment.End) {
Text(
text = "AMOUNT",
color = White,
style = MaterialTheme.typography.labelSmall
)
Text(
text = state.amount.ifBlank { "0" },
color = White,
style = MaterialTheme.typography.bodyLarge
)
}
}
}
}
}
@Composable
private fun PinEntryPanel(state: EmvTransactionState) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "${state.pinMode}Please Input PIN".trim(),
color = Black,
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = if (state.pinText.isBlank()) "------" else state.pinText,
color = Primary,
style = MaterialTheme.typography.displaySmall,
letterSpacing = 4.sp
)
if (state.pinRemaining.isNotBlank()) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = state.pinRemaining,
color = Black,
style = MaterialTheme.typography.bodyMedium
)
}
}
}

View File

@ -0,0 +1,15 @@
package com.mob.utsmyanmar.ui.emv
data class EmvTransactionState(
val cardNumber: String = "",
val expDate: String = "",
val cardScheme: String = "",
val amount: String = "",
val pinText: String = "",
val pinRemaining: String = "",
val pinMode: String = "",
val showPinPad: Boolean = false,
val statusMessage: String = "Processing card, please wait...",
val loadingMessage: String? = null,
val candidateList: List<String>? = null
)

View File

@ -23,6 +23,8 @@ import com.mob.utsmyanmar.ui.dashboard.DashboardScreen2
import com.mob.utsmyanmar.ui.dashboard.DashboardViewModel
import com.mob.utsmyanmar.ui.dashboard.SeeMoreScreen
import com.mob.utsmyanmar.ui.device_info.DeviceInfoViewModel
import com.mob.utsmyanmar.ui.emv.EmvTransactionRoute
import com.mob.utsmyanmar.ui.error.TransactionErrorScreen
import com.mob.utsmyanmar.ui.functions.FunctionsScreen
import com.mob.utsmyanmar.ui.history_functions.HistoryRoute
import com.mob.utsmyanmar.ui.input_amount.AmountRoute
@ -574,9 +576,30 @@ fun AppNavGraph(
}
},
onNavigateInputAmount = { navController.popBackStack(Routes.Amount.route, false) },
onNavigateProcessing = {},
onNavigateEmvTransaction = {},
onNavigateError = {},
onNavigateProcessing = {
navController.navigate(Routes.Processing.route) {
popUpTo(Routes.ProcessingCard.route) {
inclusive = true
}
launchSingleTop = true
}
},
onNavigateEmvTransaction = {
navController.navigate(Routes.EmvTransaction.route) {
popUpTo(Routes.ProcessingCard.route) {
inclusive = true
}
launchSingleTop = true
}
},
onNavigateError = { message ->
navController.navigate(Routes.TransactionError.createRoute(message)) {
popUpTo(Routes.ProcessingCard.route) {
inclusive = true
}
launchSingleTop = true
}
},
onBack = { navController.popBackStack() },
onShowDecline = {}
)
@ -619,6 +642,64 @@ fun AppNavGraph(
)
}
composable(Routes.EmvTransaction.route) {
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
val emvTransactionViewModel: EmvTransactionProcessViewModel = hiltViewModel(activity)
EmvTransactionRoute(
emvViewModel = emvTransactionViewModel,
sharedViewModel = sharedViewModel,
onNavigateProcessing = {
navController.navigate(Routes.Processing.route) {
popUpTo(Routes.EmvTransaction.route) {
inclusive = true
}
launchSingleTop = true
}
},
onNavigateTransactionResult = {
navController.navigate(Routes.TransactionResult.route) {
popUpTo(Routes.EmvTransaction.route) {
inclusive = true
}
launchSingleTop = true
}
},
onNavigateInputRrn = {
navController.navigate(Routes.InputRrn.route) {
popUpTo(Routes.EmvTransaction.route) {
inclusive = true
}
launchSingleTop = true
}
},
onNavigateCardWaiting = {
navController.navigate(Routes.CardWaiting.route) {
popUpTo(Routes.EmvTransaction.route) {
inclusive = true
}
launchSingleTop = true
}
},
onNavigateError = { message ->
navController.navigate(Routes.TransactionError.createRoute(message)) {
popUpTo(Routes.EmvTransaction.route) {
inclusive = true
}
launchSingleTop = true
}
},
onNavigateMain = {
navController.navigate(Routes.Dashboard.route) {
popUpTo(Routes.Dashboard.route) {
inclusive = false
}
launchSingleTop = true
}
}
)
}
composable(Routes.InputRrn.route) {
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
val rrnViewModel: RRNViewModel = hiltViewModel(activity)
@ -660,10 +741,12 @@ fun AppNavGraph(
composable(Routes.Processing.route) {
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity)
val emvTransactionViewModel: EmvTransactionProcessViewModel = hiltViewModel(activity)
val settlementViewModel: SettlementViewModel = hiltViewModel(activity)
ProcessingRoute(
sharedViewModel = sharedViewModel,
transProcessViewModel = transProcessViewModel,
emvTransactionViewModel = emvTransactionViewModel,
settlementViewModel = settlementViewModel,
onNavigateTransactionResult = {
navController.navigate(Routes.TransactionResult.route) {
@ -681,6 +764,14 @@ fun AppNavGraph(
launchSingleTop = true
}
},
onNavigateError = { message ->
navController.navigate(Routes.TransactionError.createRoute(message)) {
popUpTo(Routes.Processing.route) {
inclusive = true
}
launchSingleTop = true
}
},
onNavigateMain = {
navController.navigate(Routes.Dashboard.route) {
popUpTo(Routes.Dashboard.route) {
@ -720,6 +811,29 @@ fun AppNavGraph(
)
}
composable(
route = Routes.TransactionError.route,
arguments = listOf(
navArgument("message") {
type = NavType.StringType
}
)
) { backStackEntry ->
val message = backStackEntry.arguments?.getString("message").orEmpty().trim()
TransactionErrorScreen(
message = message,
onDone = {
navController.navigate(Routes.Dashboard.route) {
popUpTo(Routes.Dashboard.route) {
inclusive = false
}
launchSingleTop = true
}
}
)
}
composable(Routes.PrintReceipt.route) {
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
val transResultViewModel: TransactionResultViewModel = hiltViewModel(activity)

View File

@ -24,12 +24,17 @@ sealed class Routes(val route: String) {
data object CardWaiting : Routes("card_waiting")
data object ProcessingCard : Routes("processing_card")
data object PinPad : Routes("pin_pad")
data object EmvTransaction : Routes("emv_transaction")
data object InputRrn : Routes("input_rrn")
data object Processing : Routes("processing")
data object ManualEntry : Routes("manual_entry")
data object TransactionResult : Routes("transaction_result")
data object TransactionError : Routes("transaction_error/{message}") {
fun createRoute(message: String): String =
"transaction_error/${Uri.encode(message.ifBlank { " " })}"
}
data object PrintReceipt : Routes("print_receipt")
data object Version : Routes("version")
data object Functions : Routes("functions")

View File

@ -1,9 +1,11 @@
package com.mob.utsmyanmar.ui.print_receipt
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@ -13,19 +15,24 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
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.draw.clipToBounds
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -33,16 +40,20 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.theme.Color
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultEvent
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultState
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultViewModel
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.utsmyanmar.paylibs.print.PrintReceipt
import com.utsmyanmar.paylibs.utils.POSUtil
import kotlin.math.abs
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
private const val PRINT_ANIMATION_DURATION_MS = 2300
private const val RESET_ANIMATION_DURATION_MS = 350
private const val PRINTED_HOLD_DELAY_MS = 500L
private const val OFFSCREEN_BUFFER_PX = 120f
@Composable
fun PrintReceiptScreen(
transactionResultViewModel: TransactionResultViewModel,
@ -51,12 +62,10 @@ fun PrintReceiptScreen(
onDone: () -> Unit
) {
val state by transactionResultViewModel.state.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope();
val receiptOffsetY = remember {
Animatable(0f);
}
val animationDuration = 3000;
val scope = rememberCoroutineScope()
val receiptOffsetY = remember { Animatable(0f) }
var slotHeightPx by remember { mutableFloatStateOf(0f) }
var isPrinting by remember { mutableStateOf(false) }
Scaffold(
topBar = {
@ -73,15 +82,41 @@ fun PrintReceiptScreen(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.onSizeChanged { slotHeightPx = it.height.toFloat() }
) {
Box(
modifier = Modifier
.fillMaxSize()
.clipToBounds()
) {
ReceiptPreview(
state = state,
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.graphicsLayer {
translationY = receiptOffsetY.value
val travel = slotHeightPx + OFFSCREEN_BUFFER_PX
alpha = if (travel > 0f) {
1f - (abs(receiptOffsetY.value) / travel).coerceIn(0f, 1f)
} else {
1f
}
}
)
}
PrinterSlot(
isActive = isPrinting,
modifier = Modifier
.align(Alignment.TopCenter)
.fillMaxWidth()
)
}
Spacer(modifier = Modifier.height(16.dp))
@ -92,6 +127,7 @@ fun PrintReceiptScreen(
OutlinedButton(
onClick = onDone,
enabled = !isPrinting,
modifier = Modifier
.weight(1f)
.height(50.dp),
@ -107,47 +143,55 @@ fun PrintReceiptScreen(
modifier = Modifier
.weight(1f)
.height(50.dp),
enabled = !isPrinting,
colors = ButtonDefaults.buttonColors(
containerColor = Color.LegacyRed,
contentColor = Color.White,
disabledContainerColor = Color.LegacyRed.copy(alpha = 0.5f),
disabledContentColor = Color.White.copy(alpha = 0.5f),
disabledContentColor = Color.White.copy(alpha = 0.7f),
),
shape = RoundedCornerShape(12.dp),
onClick = {
if (isPrinting) return@Button
isPrinting = true
onPrint()
// try {
// PrintReceipt.getInstance().printNow()
// scope.launch {
// launch {
// receiptOffsetY.animateTo(
// targetValue = -1500f,
// animationSpec = tween(
// durationMillis = animationDuration,
// easing = FastOutSlowInEasing
// )
// )
// }
// delay(5000)
// launch {
// receiptOffsetY.animateTo(
// targetValue = 0f,
// animationSpec = tween(
// durationMillis = 0,
// easing = FastOutSlowInEasing
// )
// )
// }
// }
// } catch (e: Exception) {
//
// Log.d("PrintReceipt", "error with $e")
//
// }
scope.launch {
val targetOffset = -(slotHeightPx + OFFSCREEN_BUFFER_PX)
receiptOffsetY.animateTo(
targetValue = targetOffset,
animationSpec = tween(
durationMillis = PRINT_ANIMATION_DURATION_MS,
easing = FastOutSlowInEasing
)
)
delay(PRINTED_HOLD_DELAY_MS)
receiptOffsetY.animateTo(
targetValue = 0f,
animationSpec = tween(
durationMillis = RESET_ANIMATION_DURATION_MS,
easing = FastOutSlowInEasing
)
)
isPrinting = false
}
}
) {
if (isPrinting) {
CircularProgressIndicator(
modifier = Modifier.size(18.dp),
color = Color.White,
strokeWidth = 2.dp
)
Spacer(modifier = Modifier.width(8.dp))
}
Text(
text = "Print",
text = if (isPrinting) "Printing..." else "Print",
style = MaterialTheme.typography.titleMedium
)
}
@ -157,6 +201,48 @@ fun PrintReceiptScreen(
}
}
/**
* Visual "exit slot" pinned to the top of the receipt preview. The receipt
* appears to feed upward through this slot and out of frame while printing,
* mimicking a physical thermal printer.
*/
@Composable
private fun PrinterSlot(
isActive: Boolean,
modifier: Modifier = Modifier
) {
val infiniteTransition = rememberInfiniteTransition(label = "printerSlotGlow")
val glow by infiniteTransition.animateFloat(
initialValue = 0.35f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 550, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
),
label = "glow"
)
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.padding(horizontal = 40.dp)
.fillMaxWidth()
.height(6.dp)
.clip(RoundedCornerShape(50))
.background(
if (isActive) {
Color.LegacyRed.copy(alpha = glow)
} else {
Color.Gray.copy(alpha = 0.25f)
}
)
)
}
}
@Composable
fun ReceiptPreview(

View File

@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import sunmi.sunmiui.utils.LogUtil
@Composable
fun ProcessingCardRoute(
@ -24,6 +25,7 @@ fun ProcessingCardRoute(
LaunchedEffect(Unit) {
viewModel.uiEvent.collect { event ->
LogUtil.d("tag","Processing card : $event")
when (event) {
ProcessingCardUiEvent.NavigateToPinPad -> onNavigatePinPad()
ProcessingCardUiEvent.NavigateToInputAmount -> onNavigateInputAmount()

View File

@ -3,24 +3,29 @@ package com.mob.utsmyanmar.ui.sending_to_host
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.mob.utsmyanmar.model.TransResultStatus
import com.mob.utsmyanmar.model.TransactionStatus
import com.mob.utsmyanmar.ui.settlement.SettlementViewModel
import com.mob.utsmyanmar.viewmodel.EmvTransactionProcessViewModel
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
import com.utsmyanmar.paylibs.model.enums.TransCVM
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
private const val MOCK_HOST_DELAY_MS = 2000L
@Composable
fun ProcessingRoute(
sharedViewModel: SharedViewModel,
transProcessViewModel: TransProcessViewModel,
emvTransactionViewModel: EmvTransactionProcessViewModel,
settlementViewModel: SettlementViewModel,
onNavigateTransactionResult: () -> Unit,
onNavigateSignature: () -> Unit,
onNavigateError: (String) -> Unit,
onNavigateMain: ()-> Unit
) {
@ -30,7 +35,11 @@ fun ProcessingRoute(
val settlementStatus by settlementViewModel.uiState.collectAsStateWithLifecycle()
if(sharedViewModel.transactionsType.value == TransactionsType.SETTLEMENT) {
val isEmv = sharedViewModel.isEmv.value == true
val isSettlement = sharedViewModel.transactionsType.value == TransactionsType.SETTLEMENT
when {
isSettlement -> {
LaunchedEffect(Unit) {
settlementViewModel.startSettlementProcess()
}
@ -45,9 +54,22 @@ fun ProcessingRoute(
else -> {}
}
}
} else {
ProcessingScreen(settlementStatus.processingState)
}
isEmv -> {
EmvProcessingContent(
sharedViewModel = sharedViewModel,
emvTransactionViewModel = emvTransactionViewModel,
onNavigateTransactionResult = onNavigateTransactionResult,
onNavigateError = onNavigateError,
onNavigateMain = onNavigateMain
)
}
else -> {
LaunchedEffect(Unit) {
// transProcessViewModel.resetTransactionStatus()
transProcessViewModel.startOnlineProcess()
}
@ -72,18 +94,71 @@ fun ProcessingRoute(
else -> {}
}
}
}
if(sharedViewModel.transactionsType.value == TransactionsType.SETTLEMENT) {
ProcessingScreen(settlementStatus.processingState)
} else {
ProcessingScreen(state)
}
}
}
@Composable
private fun EmvProcessingContent(
sharedViewModel: SharedViewModel,
emvTransactionViewModel: EmvTransactionProcessViewModel,
onNavigateTransactionResult: () -> Unit,
onNavigateError: (String) -> Unit,
onNavigateMain: () -> Unit
) {
val emvTransStatus by emvTransactionViewModel.getTransStatus().observeAsState()
var title by remember { mutableStateOf("Sending...") }
LaunchedEffect(Unit) {
emvTransactionViewModel.startOnlineProcess()
}
LaunchedEffect(emvTransStatus) {
when (emvTransStatus) {
TransResultStatus.SUCCESS,
TransResultStatus.FAIL,
TransResultStatus.OFFLINE_SUCCESS -> {
sharedViewModel.payDetail.value = emvTransactionViewModel.payDetailResult.value
onNavigateTransactionResult()
}
TransResultStatus.SECONDARY -> title = "Sending Secondary..."
TransResultStatus.REVERSAL_PREPARE -> title = "Preparing Reversal..."
TransResultStatus.REVERSAL_PROCESS -> title = "Sending Reversal..."
TransResultStatus.REVERSAL_SECONDARY -> title = "Sending Secondary Reversal..."
TransResultStatus.REVERSAL_THIRD -> title = "Sending Third Reversal..."
TransResultStatus.REVERSAL_SUCCESS -> {
sharedViewModel.payDetail.value = emvTransactionViewModel.payDetailResult.value
sharedViewModel.set_successFragmentMsg("Reversal success!")
onNavigateMain()
}
TransResultStatus.REVERSAL_FAIL -> {
sharedViewModel.payDetail.value = emvTransactionViewModel.payDetailResult.value
sharedViewModel.set_errorFragmentMsg("Reversal failed!")
onNavigateError("Reversal failed!")
}
TransResultStatus.ERROR,
TransResultStatus.NETWORK_ERROR -> {
val message = emvTransactionViewModel.errorCodeMsg.value
?.substringAfter(":")
?.takeIf { it.isNotBlank() }
?: "Transaction error"
onNavigateError(message)
}
else -> {}
}
}
ProcessingScreen(ProcessingState(title = title))
}
private fun requiresSignature(payDetail: com.utsmyanmar.paylibs.model.PayDetail?): Boolean {
return payDetail?.transCVM == TransCVM.SIGNATURE && !payDetail.isFreeSign
// return true;
}