signature pad

This commit is contained in:
moon 2026-07-01 11:39:55 +06:30
parent f7510c9bad
commit 665bf1d5df
7 changed files with 69 additions and 84 deletions

View File

@ -55,7 +55,6 @@ 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.TradeData import com.utsmyanmar.paylibs.model.TradeData
import com.utsmyanmar.paylibs.model.enums.TransCVM
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
@ -90,8 +89,9 @@ fun AppNavGraph(
val sharedViewModel: SharedViewModel = hiltViewModel(activity) val sharedViewModel: SharedViewModel = hiltViewModel(activity)
SignaturePad( SignaturePad(
onBack = { navController.popBackStack() }, onBack = { navController.popBackStack() },
onConfirm = { _, bitmap -> onConfirm = { _, bitmap, signatureBlob ->
sharedViewModel.signBitmap = bitmap sharedViewModel.signBitmap = bitmap
sharedViewModel.updateSignatureBlob(signatureBlob)
navController.navigate(Routes.TransactionResult.route) { navController.navigate(Routes.TransactionResult.route) {
popUpTo(Routes.Signature.route) { popUpTo(Routes.Signature.route) {
inclusive = true inclusive = true

View File

@ -9,6 +9,7 @@ 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.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.utils.iso_utils.TransactionsType import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
private const val MOCK_HOST_DELAY_MS = 2000L private const val MOCK_HOST_DELAY_MS = 2000L
@ -83,6 +84,6 @@ fun ProcessingRoute(
} }
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 == false return payDetail?.transCVM == TransCVM.SIGNATURE && !payDetail.isFreeSign
return true; // return true;
} }

View File

@ -29,6 +29,7 @@ import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
@ -39,8 +40,10 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
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
import androidx.core.graphics.createBitmap
import com.mob.utsmyanmar.ui.components.appbar.AppBar import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.theme.MOBPOSTheme import com.mob.utsmyanmar.ui.theme.MOBPOSTheme
import java.io.ByteArrayOutputStream
import android.graphics.Canvas as AndroidCanvas import android.graphics.Canvas as AndroidCanvas
import android.graphics.Path as AndroidPath import android.graphics.Path as AndroidPath
import com.mob.utsmyanmar.ui.theme.Color as AppColor import com.mob.utsmyanmar.ui.theme.Color as AppColor
@ -49,39 +52,39 @@ import com.mob.utsmyanmar.ui.theme.Color as AppColor
fun SignaturePad( fun SignaturePad(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
signatureColor: Color = Color.Black, signatureColor: Color = Color.Black,
onBack: () -> Unit = {}, onBack: () -> Unit = {},
onConfirm: (SignatureMode, Bitmap?) -> Unit = { _, _ -> } // signatureBlob is null for Digital mode (using stored signature) or when nothing was drawn
onConfirm: (SignatureMode, Bitmap?, signatureBlob: ByteArray?) -> Unit = { _, _, _ -> }
) { ) {
val paths = remember { mutableStateListOf<List<Offset>>() } val paths = remember { mutableStateListOf<List<Offset>>() }
val currentPath = remember { mutableStateListOf<Offset>() } val currentPath = remember { mutableStateListOf<Offset>() }
var selectedMode by remember { mutableStateOf(SignatureMode.Manual) } var selectedMode by remember { mutableStateOf(SignatureMode.Manual) }
var canvasSize by remember { mutableStateOf(Size.Zero) } var canvasSize by remember { mutableStateOf(Size.Zero) }
var strokeWidth: Float = 8f; var strokeWidth = 6f
fun clearSignature() { fun clearSignature() {
paths.clear() paths.clear()
currentPath.clear() currentPath.clear()
} }
fun buildBitmap(): Bitmap? { fun buildBitmap(): Bitmap? {
if (selectedMode == SignatureMode.Manual) return null if (selectedMode == SignatureMode.Digital) return null
if (paths.isEmpty() && currentPath.isEmpty()) return null if (paths.isEmpty() && currentPath.isEmpty()) return null
if (canvasSize.width <= 0f || canvasSize.height <= 0f) return null if (canvasSize.width <= 0f || canvasSize.height <= 0f) return null
val bitmap = Bitmap.createBitmap( val bitmap =
canvasSize.width.toInt(), createBitmap(canvasSize.width.toInt(), canvasSize.height.toInt(), Bitmap.Config.RGB_565)
canvasSize.height.toInt(),
Bitmap.Config.ARGB_8888
)
val canvas = AndroidCanvas(bitmap) val canvas = AndroidCanvas(bitmap)
canvas.drawColor(android.graphics.Color.WHITE) // fill white background first
val paint = Paint().apply { val paint = Paint().apply {
color = android.graphics.Color.BLACK color = android.graphics.Color.BLACK
style = Paint.Style.STROKE style = Paint.Style.STROKE
strokeWidth = strokeWidth
isAntiAlias = true isAntiAlias = true
strokeCap = Paint.Cap.ROUND strokeCap = Paint.Cap.ROUND
strokeJoin = Paint.Join.ROUND strokeJoin = Paint.Join.ROUND
strokeWidth = 10f
} }
fun drawStroke(points: List<Offset>) { fun drawStroke(points: List<Offset>) {
@ -101,6 +104,13 @@ fun SignaturePad(
return bitmap return bitmap
} }
fun bitmapToBlob(bitmap: Bitmap): ByteArray {
ByteArrayOutputStream().use { stream ->
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
return stream.toByteArray()
}
}
Scaffold( Scaffold(
topBar = { topBar = {
AppBar( AppBar(
@ -117,17 +127,18 @@ fun SignaturePad(
.padding(16.dp), .padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.weight(1f) .fillMaxWidth(0.85f) // narrower than full width, adjust as needed
.fillMaxWidth() .height(180.dp) // fixed height instead of weight(1f)
.background(Color.White, RoundedCornerShape(16.dp)) .align(Alignment.CenterHorizontally)
.padding(12.dp) .background(Color.White, RoundedCornerShape(16.dp))
.onSizeChanged { size -> .padding(12.dp)
canvasSize = Size(size.width.toFloat(), size.height.toFloat()) .onSizeChanged { size ->
} canvasSize = Size(size.width.toFloat(), size.height.toFloat())
.pointerInput(Unit) { }
detectDragGestures( .pointerInput(Unit) {
detectDragGestures(
onDragStart = { startOffset -> onDragStart = { startOffset ->
currentPath.add(startOffset) currentPath.add(startOffset)
}, },
@ -175,9 +186,10 @@ fun SignaturePad(
} }
} }
Column ( Column(
modifier = Modifier.fillMaxWidth().height(150.dp), modifier = Modifier
// horizontalArrangement = Arrangement.spacedBy(12.dp) .fillMaxWidth()
.height(150.dp)
) { ) {
SignatureModeOption( SignatureModeOption(
title = "Manual Sign", title = "Manual Sign",
@ -214,10 +226,12 @@ fun SignaturePad(
Button( Button(
onClick = { onClick = {
val bitmap = buildBitmap() val bitmap = buildBitmap()
if (selectedMode == SignatureMode.Digital && bitmap == null) { if (selectedMode == SignatureMode.Manual && bitmap == null) {
// nothing drawn yet on Manual mode — block confirm
return@Button return@Button
} }
onConfirm(selectedMode, bitmap) val signatureBlob = bitmap?.let { bitmapToBlob(it) }
onConfirm(selectedMode, bitmap, signatureBlob)
}, },
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)

View File

@ -4,9 +4,9 @@ import android.graphics.Bitmap
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.mob.utsmyanmar.model.SettlementType import com.mob.utsmyanmar.model.SettlementType
import com.mob.utsmyanmar.ui.processing_card.ProcessingCardState import com.utsmyanmar.baselib.db.dao.PayDetailDao
import com.mob.utsmyanmar.ui.sending_to_host.ProcessingState
import com.utsmyanmar.baselib.network.model.sirius.SiriusRequest import com.utsmyanmar.baselib.network.model.sirius.SiriusRequest
import com.utsmyanmar.baselib.network.model.sirius.SiriusResponse import com.utsmyanmar.baselib.network.model.sirius.SiriusResponse
import com.utsmyanmar.baselib.repo.Repository import com.utsmyanmar.baselib.repo.Repository
@ -21,19 +21,20 @@ import com.utsmyanmar.paylibs.utils.AccountType
import com.utsmyanmar.paylibs.utils.PrintStatus import com.utsmyanmar.paylibs.utils.PrintStatus
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import com.utsmyanmar.paylibs.utils.enums.HostType import com.utsmyanmar.paylibs.utils.enums.HostType
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionType
import com.utsmyanmar.paylibs.utils.enums.TransMenu import com.utsmyanmar.paylibs.utils.enums.TransMenu
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionType
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Observable
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch
import sunmi.sunmiui.utils.LogUtil import sunmi.sunmiui.utils.LogUtil
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class SharedViewModel @Inject constructor( class SharedViewModel @Inject constructor(
private val repository: Repository private val repository: Repository,
private val payDetailDao: PayDetailDao
) : ViewModel() { ) : ViewModel() {
companion object { companion object {
@ -666,4 +667,13 @@ class SharedViewModel @Inject constructor(
SystemParamsOperation.getInstance().wavePayStatus SystemParamsOperation.getInstance().wavePayStatus
) )
} }
fun updateSignatureBlob(signatureBlob: ByteArray?) {
val current = payDetail.value ?: return
current.signatureBlob = signatureBlob
payDetail.value = current
viewModelScope.launch(Dispatchers.IO) {
payDetailDao.updateSignatureBlob(current.voucherNo, signatureBlob)
}
}
} }

View File

@ -5,13 +5,9 @@ import androidx.room.Dao;
import androidx.room.Delete; import androidx.room.Delete;
import androidx.room.Insert; import androidx.room.Insert;
import androidx.room.Query; import androidx.room.Query;
import androidx.room.Transaction;
import androidx.room.Update; import androidx.room.Update;
import com.utsmyanmar.paylibs.model.PayDetail; import com.utsmyanmar.paylibs.model.PayDetail;
import com.utsmyanmar.paylibs.system.SingleLiveEvent;
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionType;
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType;
import java.util.List; import java.util.List;
@ -157,4 +153,7 @@ public interface PayDetailDao {
@Query("SELECT * FROM paydetail ORDER BY PID DESC") @Query("SELECT * FROM paydetail ORDER BY PID DESC")
LiveData<List<PayDetail>> getAllTrans(); LiveData<List<PayDetail>> getAllTrans();
@Query("UPDATE paydetail SET signatureBlob = :blob WHERE voucherNo = :voucherNo")
void updateSignatureBlob(String voucherNo, byte[] blob);
} }

View File

@ -397,7 +397,7 @@ public class DatabaseModule {
@Provides @Provides
@Singleton @Singleton
public static AppDatabase provideAppDatabase(Application application) { public static AppDatabase provideAppDatabase(Application application) {
return Room.databaseBuilder(application, AppDatabase.class, "yoma") return Room.databaseBuilder(application, AppDatabase.class, "mob")
// .fallbackToDestructiveMigration() // .fallbackToDestructiveMigration()
.addMigrations(MIGRATION_7_8) .addMigrations(MIGRATION_7_8)
.addMigrations(MIGRATION_8_9) .addMigrations(MIGRATION_8_9)

View File

@ -3,16 +3,8 @@ package com.utsmyanmar.paylibs.print.printx;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.RemoteException; import android.os.RemoteException;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.utsmyanmar.paylibs.Constant; import com.utsmyanmar.paylibs.Constant;
import com.utsmyanmar.paylibs.PayLibsUtils; import com.utsmyanmar.paylibs.PayLibsUtils;
import com.utsmyanmar.paylibs.R; import com.utsmyanmar.paylibs.R;
@ -31,10 +23,8 @@ import com.utsmyanmar.paylibs.utils.enums.HostType;
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType; import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType;
import com.utsmyanmar.paylibs.utils.print_utils.BitmapUtils; import com.utsmyanmar.paylibs.utils.print_utils.BitmapUtils;
import java.io.ByteArrayOutputStream;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@ -268,7 +258,7 @@ public class PrintXImpl extends BaseXPrint implements PrintX {
if (payDetail.getTradeAnswerCode().equals(Constant.ANSWER_CODE_APPROVED) || payDetail.getTradeAnswerCode().equals(Constant.ANSWER_CODE_ACCEPT)) { if (payDetail.getTradeAnswerCode().equals(Constant.ANSWER_CODE_APPROVED) || payDetail.getTradeAnswerCode().equals(Constant.ANSWER_CODE_ACCEPT)) {
printTransFooter(false,payDetail, isMerchantCopy); printTransFooter(false,payDetail, isMerchantCopy);
printSignatureBlock(payDetail, signBitmap); // printSignatureBlock(payDetail, signBitmap);
} else { } else {
String receiptFooter = SystemParamsOperation.getInstance().getReceiptFooter(); String receiptFooter = SystemParamsOperation.getInstance().getReceiptFooter();
sunmiPrinterService.printText("\n", null); sunmiPrinterService.printText("\n", null);
@ -293,7 +283,7 @@ public class PrintXImpl extends BaseXPrint implements PrintX {
if (payDetail.getTradeAnswerCode().equals(Constant.ANSWER_CODE_APPROVED) || payDetail.getTradeAnswerCode().equals(Constant.ANSWER_CODE_ACCEPT)) { if (payDetail.getTradeAnswerCode().equals(Constant.ANSWER_CODE_APPROVED) || payDetail.getTradeAnswerCode().equals(Constant.ANSWER_CODE_ACCEPT)) {
printTransFooter(false,payDetail, isMerchantCopy); printTransFooter(false,payDetail, isMerchantCopy);
printSignatureBlock(payDetail, signBitmap); // printSignatureBlock(payDetail, signBitmap);
} else { } else {
String receiptFooter = SystemParamsOperation.getInstance().getReceiptFooter(); String receiptFooter = SystemParamsOperation.getInstance().getReceiptFooter();
sunmiPrinterService.setFontSize(textSize, null); sunmiPrinterService.setFontSize(textSize, null);
@ -309,39 +299,10 @@ public class PrintXImpl extends BaseXPrint implements PrintX {
} }
} }
private void printSignatureBlock(PayDetail payDetail, Bitmap signBitmap) {
try {
if (payDetail.getTransCVM() != com.utsmyanmar.paylibs.model.enums.TransCVM.SIGNATURE) {
return;
}
if (signBitmap == null) {
return;
}
Bitmap printableBitmap = signBitmap;
if (printableBitmap.getWidth() > 130) {
double size = 1.0 * printableBitmap.getHeight() * 130 / printableBitmap.getWidth();
int newHeight = (int) size;
printableBitmap = BitmapUtils.scale(printableBitmap, 130, newHeight);
}
sunmiPrinterService.lineWrap(1, innerResultCallback);
sunmiPrinterService.setAlignment(1, innerResultCallback);
sunmiPrinterService.printBitmap(printableBitmap, innerResultCallback);
sunmiPrinterService.lineWrap(1, innerResultCallback);
} catch (RemoteException e) {
e.printStackTrace();
}
}
private void printTransDetailWave(PayDetail payDetail) { private void printTransDetailWave(PayDetail payDetail) {
try { try {
// payDetail.getCustomerMobile() // as channel type // payDetail.getCustomerMobile() // as channel typeိ
if(payDetail.getTransactionType() == TransactionsType.WAVEPAY.value && payDetail.getQrTransStatus() == 1 ) { if(payDetail.getTransactionType() == TransactionsType.WAVEPAY.value && payDetail.getQrTransStatus() == 1 ) {
sunmiPrinterService.printColumnsString(new String[]{"PAYMENT TYPE ",": "+ (payDetail.getCustomerMobile() == null || payDetail.getCustomerMobile().isEmpty() ? "-": payDetail.getCustomerMobile().toUpperCase())}, new int[]{2, 2}, new int[]{0, 0}, null); sunmiPrinterService.printColumnsString(new String[]{"PAYMENT TYPE ",": "+ (payDetail.getCustomerMobile() == null || payDetail.getCustomerMobile().isEmpty() ? "-": payDetail.getCustomerMobile().toUpperCase())}, new int[]{2, 2}, new int[]{0, 0}, null);