emv first upload
This commit is contained in:
parent
665bf1d5df
commit
7c0e9c4e66
@ -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)
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
)
|
||||||
@ -23,6 +23,8 @@ import com.mob.utsmyanmar.ui.dashboard.DashboardScreen2
|
|||||||
import com.mob.utsmyanmar.ui.dashboard.DashboardViewModel
|
import com.mob.utsmyanmar.ui.dashboard.DashboardViewModel
|
||||||
import com.mob.utsmyanmar.ui.dashboard.SeeMoreScreen
|
import com.mob.utsmyanmar.ui.dashboard.SeeMoreScreen
|
||||||
import com.mob.utsmyanmar.ui.device_info.DeviceInfoViewModel
|
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.functions.FunctionsScreen
|
||||||
import com.mob.utsmyanmar.ui.history_functions.HistoryRoute
|
import com.mob.utsmyanmar.ui.history_functions.HistoryRoute
|
||||||
import com.mob.utsmyanmar.ui.input_amount.AmountRoute
|
import com.mob.utsmyanmar.ui.input_amount.AmountRoute
|
||||||
@ -574,9 +576,30 @@ fun AppNavGraph(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onNavigateInputAmount = { navController.popBackStack(Routes.Amount.route, false) },
|
onNavigateInputAmount = { navController.popBackStack(Routes.Amount.route, false) },
|
||||||
onNavigateProcessing = {},
|
onNavigateProcessing = {
|
||||||
onNavigateEmvTransaction = {},
|
navController.navigate(Routes.Processing.route) {
|
||||||
onNavigateError = {},
|
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() },
|
onBack = { navController.popBackStack() },
|
||||||
onShowDecline = {}
|
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) {
|
composable(Routes.InputRrn.route) {
|
||||||
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||||
val rrnViewModel: RRNViewModel = hiltViewModel(activity)
|
val rrnViewModel: RRNViewModel = hiltViewModel(activity)
|
||||||
@ -660,10 +741,12 @@ fun AppNavGraph(
|
|||||||
composable(Routes.Processing.route) {
|
composable(Routes.Processing.route) {
|
||||||
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||||
val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity)
|
val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity)
|
||||||
|
val emvTransactionViewModel: EmvTransactionProcessViewModel = hiltViewModel(activity)
|
||||||
val settlementViewModel: SettlementViewModel = hiltViewModel(activity)
|
val settlementViewModel: SettlementViewModel = hiltViewModel(activity)
|
||||||
ProcessingRoute(
|
ProcessingRoute(
|
||||||
sharedViewModel = sharedViewModel,
|
sharedViewModel = sharedViewModel,
|
||||||
transProcessViewModel = transProcessViewModel,
|
transProcessViewModel = transProcessViewModel,
|
||||||
|
emvTransactionViewModel = emvTransactionViewModel,
|
||||||
settlementViewModel = settlementViewModel,
|
settlementViewModel = settlementViewModel,
|
||||||
onNavigateTransactionResult = {
|
onNavigateTransactionResult = {
|
||||||
navController.navigate(Routes.TransactionResult.route) {
|
navController.navigate(Routes.TransactionResult.route) {
|
||||||
@ -681,6 +764,14 @@ fun AppNavGraph(
|
|||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onNavigateError = { message ->
|
||||||
|
navController.navigate(Routes.TransactionError.createRoute(message)) {
|
||||||
|
popUpTo(Routes.Processing.route) {
|
||||||
|
inclusive = true
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
},
|
||||||
onNavigateMain = {
|
onNavigateMain = {
|
||||||
navController.navigate(Routes.Dashboard.route) {
|
navController.navigate(Routes.Dashboard.route) {
|
||||||
popUpTo(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) {
|
composable(Routes.PrintReceipt.route) {
|
||||||
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||||
val transResultViewModel: TransactionResultViewModel = hiltViewModel(activity)
|
val transResultViewModel: TransactionResultViewModel = hiltViewModel(activity)
|
||||||
|
|||||||
@ -24,12 +24,17 @@ sealed class Routes(val route: String) {
|
|||||||
data object CardWaiting : Routes("card_waiting")
|
data object CardWaiting : Routes("card_waiting")
|
||||||
data object ProcessingCard : Routes("processing_card")
|
data object ProcessingCard : Routes("processing_card")
|
||||||
data object PinPad : Routes("pin_pad")
|
data object PinPad : Routes("pin_pad")
|
||||||
|
data object EmvTransaction : Routes("emv_transaction")
|
||||||
data object InputRrn : Routes("input_rrn")
|
data object InputRrn : Routes("input_rrn")
|
||||||
data object Processing : Routes("processing")
|
data object Processing : Routes("processing")
|
||||||
data object ManualEntry : Routes("manual_entry")
|
data object ManualEntry : Routes("manual_entry")
|
||||||
|
|
||||||
|
|
||||||
data object TransactionResult : Routes("transaction_result")
|
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 PrintReceipt : Routes("print_receipt")
|
||||||
data object Version : Routes("version")
|
data object Version : Routes("version")
|
||||||
data object Functions : Routes("functions")
|
data object Functions : Routes("functions")
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
package com.mob.utsmyanmar.ui.print_receipt
|
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.Animatable
|
||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
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.animation.core.tween
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
@ -13,19 +15,24 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@ -33,16 +40,20 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||||
import com.mob.utsmyanmar.ui.theme.Color
|
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.TransactionResultState
|
||||||
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultViewModel
|
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultViewModel
|
||||||
import com.mob.utsmyanmar.viewmodel.SharedViewModel
|
import com.mob.utsmyanmar.viewmodel.SharedViewModel
|
||||||
import com.utsmyanmar.paylibs.print.PrintReceipt
|
|
||||||
import com.utsmyanmar.paylibs.utils.POSUtil
|
import com.utsmyanmar.paylibs.utils.POSUtil
|
||||||
|
|
||||||
|
import kotlin.math.abs
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
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
|
@Composable
|
||||||
fun PrintReceiptScreen(
|
fun PrintReceiptScreen(
|
||||||
transactionResultViewModel: TransactionResultViewModel,
|
transactionResultViewModel: TransactionResultViewModel,
|
||||||
@ -51,12 +62,10 @@ fun PrintReceiptScreen(
|
|||||||
onDone: () -> Unit
|
onDone: () -> Unit
|
||||||
) {
|
) {
|
||||||
val state by transactionResultViewModel.state.collectAsStateWithLifecycle()
|
val state by transactionResultViewModel.state.collectAsStateWithLifecycle()
|
||||||
val scope = rememberCoroutineScope();
|
val scope = rememberCoroutineScope()
|
||||||
val receiptOffsetY = remember {
|
val receiptOffsetY = remember { Animatable(0f) }
|
||||||
Animatable(0f);
|
var slotHeightPx by remember { mutableFloatStateOf(0f) }
|
||||||
}
|
var isPrinting by remember { mutableStateOf(false) }
|
||||||
val animationDuration = 3000;
|
|
||||||
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@ -73,15 +82,41 @@ fun PrintReceiptScreen(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.onSizeChanged { slotHeightPx = it.height.toFloat() }
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clipToBounds()
|
||||||
|
) {
|
||||||
ReceiptPreview(
|
ReceiptPreview(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.fillMaxWidth()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.graphicsLayer{
|
.graphicsLayer {
|
||||||
translationY = receiptOffsetY.value
|
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))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
@ -92,6 +127,7 @@ fun PrintReceiptScreen(
|
|||||||
|
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = onDone,
|
onClick = onDone,
|
||||||
|
enabled = !isPrinting,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.height(50.dp),
|
.height(50.dp),
|
||||||
@ -107,47 +143,55 @@ fun PrintReceiptScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.height(50.dp),
|
.height(50.dp),
|
||||||
|
enabled = !isPrinting,
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = Color.LegacyRed,
|
containerColor = Color.LegacyRed,
|
||||||
contentColor = Color.White,
|
contentColor = Color.White,
|
||||||
disabledContainerColor = Color.LegacyRed.copy(alpha = 0.5f),
|
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),
|
shape = RoundedCornerShape(12.dp),
|
||||||
onClick = {
|
onClick = {
|
||||||
|
if (isPrinting) return@Button
|
||||||
|
|
||||||
|
isPrinting = true
|
||||||
onPrint()
|
onPrint()
|
||||||
// try {
|
|
||||||
// PrintReceipt.getInstance().printNow()
|
scope.launch {
|
||||||
// scope.launch {
|
val targetOffset = -(slotHeightPx + OFFSCREEN_BUFFER_PX)
|
||||||
// launch {
|
|
||||||
// receiptOffsetY.animateTo(
|
receiptOffsetY.animateTo(
|
||||||
// targetValue = -1500f,
|
targetValue = targetOffset,
|
||||||
// animationSpec = tween(
|
animationSpec = tween(
|
||||||
// durationMillis = animationDuration,
|
durationMillis = PRINT_ANIMATION_DURATION_MS,
|
||||||
// easing = FastOutSlowInEasing
|
easing = FastOutSlowInEasing
|
||||||
// )
|
)
|
||||||
// )
|
)
|
||||||
// }
|
|
||||||
// delay(5000)
|
delay(PRINTED_HOLD_DELAY_MS)
|
||||||
// launch {
|
|
||||||
// receiptOffsetY.animateTo(
|
receiptOffsetY.animateTo(
|
||||||
// targetValue = 0f,
|
targetValue = 0f,
|
||||||
// animationSpec = tween(
|
animationSpec = tween(
|
||||||
// durationMillis = 0,
|
durationMillis = RESET_ANIMATION_DURATION_MS,
|
||||||
// easing = FastOutSlowInEasing
|
easing = FastOutSlowInEasing
|
||||||
// )
|
)
|
||||||
// )
|
)
|
||||||
// }
|
|
||||||
// }
|
isPrinting = false
|
||||||
// } catch (e: Exception) {
|
}
|
||||||
//
|
|
||||||
// Log.d("PrintReceipt", "error with $e")
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
if (isPrinting) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
color = Color.White,
|
||||||
|
strokeWidth = 2.dp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text = "Print",
|
text = if (isPrinting) "Printing..." else "Print",
|
||||||
style = MaterialTheme.typography.titleMedium
|
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
|
@Composable
|
||||||
fun ReceiptPreview(
|
fun ReceiptPreview(
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import sunmi.sunmiui.utils.LogUtil
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProcessingCardRoute(
|
fun ProcessingCardRoute(
|
||||||
@ -24,6 +25,7 @@ fun ProcessingCardRoute(
|
|||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.uiEvent.collect { event ->
|
viewModel.uiEvent.collect { event ->
|
||||||
|
LogUtil.d("tag","Processing card : $event")
|
||||||
when (event) {
|
when (event) {
|
||||||
ProcessingCardUiEvent.NavigateToPinPad -> onNavigatePinPad()
|
ProcessingCardUiEvent.NavigateToPinPad -> onNavigatePinPad()
|
||||||
ProcessingCardUiEvent.NavigateToInputAmount -> onNavigateInputAmount()
|
ProcessingCardUiEvent.NavigateToInputAmount -> onNavigateInputAmount()
|
||||||
|
|||||||
@ -3,24 +3,29 @@ package com.mob.utsmyanmar.ui.sending_to_host
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
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 androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.mob.utsmyanmar.model.TransResultStatus
|
import com.mob.utsmyanmar.model.TransResultStatus
|
||||||
import com.mob.utsmyanmar.model.TransactionStatus
|
import com.mob.utsmyanmar.model.TransactionStatus
|
||||||
import com.mob.utsmyanmar.ui.settlement.SettlementViewModel
|
import com.mob.utsmyanmar.ui.settlement.SettlementViewModel
|
||||||
|
import com.mob.utsmyanmar.viewmodel.EmvTransactionProcessViewModel
|
||||||
import com.mob.utsmyanmar.viewmodel.SharedViewModel
|
import com.mob.utsmyanmar.viewmodel.SharedViewModel
|
||||||
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
|
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
|
||||||
import com.utsmyanmar.paylibs.model.enums.TransCVM
|
import com.utsmyanmar.paylibs.model.enums.TransCVM
|
||||||
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||||
|
|
||||||
private const val MOCK_HOST_DELAY_MS = 2000L
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProcessingRoute(
|
fun ProcessingRoute(
|
||||||
sharedViewModel: SharedViewModel,
|
sharedViewModel: SharedViewModel,
|
||||||
transProcessViewModel: TransProcessViewModel,
|
transProcessViewModel: TransProcessViewModel,
|
||||||
|
emvTransactionViewModel: EmvTransactionProcessViewModel,
|
||||||
settlementViewModel: SettlementViewModel,
|
settlementViewModel: SettlementViewModel,
|
||||||
onNavigateTransactionResult: () -> Unit,
|
onNavigateTransactionResult: () -> Unit,
|
||||||
onNavigateSignature: () -> Unit,
|
onNavigateSignature: () -> Unit,
|
||||||
|
onNavigateError: (String) -> Unit,
|
||||||
onNavigateMain: ()-> Unit
|
onNavigateMain: ()-> Unit
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@ -30,13 +35,17 @@ fun ProcessingRoute(
|
|||||||
|
|
||||||
val settlementStatus by settlementViewModel.uiState.collectAsStateWithLifecycle()
|
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) {
|
LaunchedEffect(Unit) {
|
||||||
settlementViewModel.startSettlementProcess()
|
settlementViewModel.startSettlementProcess()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(settlementStatus.status) {
|
LaunchedEffect(settlementStatus.status) {
|
||||||
when(settlementStatus.status) {
|
when (settlementStatus.status) {
|
||||||
TransactionStatus.ON_ERROR,
|
TransactionStatus.ON_ERROR,
|
||||||
TransactionStatus.ON_SUCCESS -> {
|
TransactionStatus.ON_SUCCESS -> {
|
||||||
sharedViewModel.payDetail.value = settlementViewModel.getPayDetail()
|
sharedViewModel.payDetail.value = settlementViewModel.getPayDetail()
|
||||||
@ -45,14 +54,27 @@ fun ProcessingRoute(
|
|||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
|
ProcessingScreen(settlementStatus.processingState)
|
||||||
|
}
|
||||||
|
|
||||||
|
isEmv -> {
|
||||||
|
EmvProcessingContent(
|
||||||
|
sharedViewModel = sharedViewModel,
|
||||||
|
emvTransactionViewModel = emvTransactionViewModel,
|
||||||
|
onNavigateTransactionResult = onNavigateTransactionResult,
|
||||||
|
onNavigateError = onNavigateError,
|
||||||
|
onNavigateMain = onNavigateMain
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
// transProcessViewModel.resetTransactionStatus()
|
|
||||||
transProcessViewModel.startOnlineProcess()
|
transProcessViewModel.startOnlineProcess()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(transStatus) {
|
LaunchedEffect(transStatus) {
|
||||||
when(transStatus) {
|
when (transStatus) {
|
||||||
TransResultStatus.SUCCESS -> {
|
TransResultStatus.SUCCESS -> {
|
||||||
sharedViewModel.payDetail.value = transProcessViewModel.payDetailResult.value
|
sharedViewModel.payDetail.value = transProcessViewModel.payDetailResult.value
|
||||||
if (requiresSignature(sharedViewModel.payDetail.value)) {
|
if (requiresSignature(sharedViewModel.payDetail.value)) {
|
||||||
@ -72,18 +94,71 @@ fun ProcessingRoute(
|
|||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if(sharedViewModel.transactionsType.value == TransactionsType.SETTLEMENT) {
|
|
||||||
ProcessingScreen(settlementStatus.processingState)
|
|
||||||
} else {
|
|
||||||
ProcessingScreen(state)
|
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 {
|
private fun requiresSignature(payDetail: com.utsmyanmar.paylibs.model.PayDetail?): Boolean {
|
||||||
return payDetail?.transCVM == TransCVM.SIGNATURE && !payDetail.isFreeSign
|
return payDetail?.transCVM == TransCVM.SIGNATURE && !payDetail.isFreeSign
|
||||||
// return true;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user