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.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)
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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{
|
||||
.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(
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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,13 +35,17 @@ 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()
|
||||
}
|
||||
|
||||
LaunchedEffect(settlementStatus.status) {
|
||||
when(settlementStatus.status) {
|
||||
when (settlementStatus.status) {
|
||||
TransactionStatus.ON_ERROR,
|
||||
TransactionStatus.ON_SUCCESS -> {
|
||||
sharedViewModel.payDetail.value = settlementViewModel.getPayDetail()
|
||||
@ -45,14 +54,27 @@ 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()
|
||||
}
|
||||
|
||||
LaunchedEffect(transStatus) {
|
||||
when(transStatus) {
|
||||
when (transStatus) {
|
||||
TransResultStatus.SUCCESS -> {
|
||||
sharedViewModel.payDetail.value = transProcessViewModel.payDetailResult.value
|
||||
if (requiresSignature(sharedViewModel.payDetail.value)) {
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user