Compare commits

...

63 Commits
main ... mpu

Author SHA1 Message Date
142716caa4 added signature for all MPU transactions 2026-06-15 14:39:32 +07:00
3b7354fc3f MPU Certification latest 2026-06-15 14:37:22 +07:00
f070ad1c10 MPU still in progress only left preauth test cases 2026-06-11 15:41:30 +07:00
b073e62a20 MPU certification in progress + TMS 2026-06-10 18:37:28 +07:00
5d51f53440 Merge remote-tracking branch 'origin/tms' into dev 2026-06-10 16:47:01 +07:00
4fefae3646 MPU certification in progress 2026-06-10 16:40:02 +07:00
moon
7361078369 function settings screen added 2026-06-10 15:55:40 +06:30
moon
800eeadd22 request body changed 2026-06-10 14:07:11 +06:30
moon
18851cf7a1 tms config download page 2026-06-10 13:25:38 +06:30
moon
8b3fb9a1a5 tms connected 2026-06-10 12:23:21 +06:30
moon
6dc180e791 converted tms configs into kotlin 2026-06-09 20:22:23 +06:30
moon
86998e1fe2 tmsSetupsImpl 2026-06-09 15:00:39 +06:30
8e6fde9856 MPU ready for certification 2026-06-09 12:52:45 +07:00
68dd5eebd7 Merge remote-tracking branch 'origin/dev' into dev
# Conflicts:
#	app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen2.kt
2026-06-08 16:21:57 +07:00
759cd20209 configured for MPU 2026-06-08 16:21:38 +07:00
moon
3301cb15e0 Merge branch 'dev' of https://hub.utsmyanmar.com/Kyaw_Min_Khant/mob_pos into dev 2026-06-08 08:49:54 +06:30
9a87493945 added mock card and integrated partially for MPU 2026-06-04 01:27:48 +07:00
moon
c1c208187b responsive keypad 2026-05-31 22:04:09 +06:30
moon
40ee7c600f responsive size for dashboard 2026-05-31 21:41:43 +06:30
moon
8671d459cf icons changed 2026-05-31 19:52:29 +06:30
000c48bf6b cardzone sign on got approved 2026-05-30 17:55:58 +07:00
moon
f95208aa9d input amount screen responsive screen size 2026-05-28 15:17:57 +06:30
moon
4aebd825d2 version screen 2026-05-27 16:55:59 +06:30
f80c7c5d1b sign on and sale work now 2026-05-25 23:11:26 +07:00
moon
5e2cfa3821 25-may 2026-05-25 11:02:15 +06:30
moon
0ed4113366 local package 2026-05-22 23:24:58 +06:30
moon
6ddf0dda6c changed SendingToHost to Processing 2026-05-22 22:50:28 +06:30
moon
ba24c60058 merge AmountScreen 2026-05-22 22:32:07 +06:30
moon
feb6ea449f navi repair 2026-05-22 19:40:33 +06:30
moon
030eb6f836 refund 2026-05-22 17:32:58 +06:30
moon
af917520b6 Settlement Screen init 2026-05-22 00:30:51 +06:30
moon
767456b29a void flow complete 2026-05-22 00:05:58 +06:30
moon
5c6c8f78c5 auto 0000 added 2026-05-21 23:56:45 +06:30
moon
0f0bd49709 search void tran 2026-05-21 23:53:12 +06:30
moon
dbc5424aab void screen and pinpad fix 2026-05-21 23:31:05 +06:30
moon
675e398aaf sidebar profile added 2026-05-21 21:27:13 +06:30
moon
57dcd13c34 ads screen added 2026-05-21 20:10:02 +06:30
moon
72ecfbe4e7 keypad, modify 2026-05-21 11:25:32 +06:30
moon
0f435192ee Merge branch 'dev' of https://hub.utsmyanmar.com/Kyaw_Min_Khant/mob_pos into dev 2026-05-21 11:05:18 +06:30
moon
fe6c5ad15e sign on page 2026-05-21 11:03:12 +06:30
10f5bcf355 Merge pull request 'dashboard ui changed' (#1) from dev_ptk into dev
Reviewed-on: Dashboard Ui change
2026-05-21 04:32:40 +00:00
e985b37aa2 dashboard ui changed 2026-05-20 16:29:42 +06:30
moon
f4fc2fa730 animation added 2026-05-16 21:35:58 +06:30
moon
ff2858d484 new success screen 2026-05-16 13:45:17 +06:30
moon
9d9ef95140 flow fix 2026-05-16 12:15:59 +06:30
moon
79ab7c0bae card capture renew 2026-05-16 12:08:41 +06:30
moon
283d2d64ce renew amount screen 2026-05-16 10:33:49 +06:30
moon
fb4c832476 new dashboard style 2026-05-16 10:07:00 +06:30
moon
5bbda074a8 New Printer SDK integration success 2026-05-14 11:41:57 +06:30
moon
ee2f9df351 fix navigation to reset goback 2026-05-13 22:58:28 +06:30
moon
35600bda29 refactor routes 2026-05-13 22:38:35 +06:30
moon
44c0fa20af Modified AppBar and add routes 2026-05-13 20:29:37 +06:30
moon
04549321fb Update TransactionResultScreen.kt 2026-05-13 20:08:14 +06:30
moon
42bd592c3e TranscationResultScreen ui 2026-05-13 20:02:22 +06:30
moon
5904b4f69a fix themes 2026-05-13 10:51:10 +06:30
moon
bdd252cda9 replaced the MOB logo 2026-05-13 00:18:05 +06:30
moon
9602d7cf81 removed mock icon 2026-05-12 23:51:22 +06:30
moon
0abf7bc6c9 pin pad success 2026-05-12 23:49:47 +06:30
moon
bcd5634941 pinpad init 2026-05-12 23:22:15 +06:30
moon
b5e2ec8e01 pin pad init oki 2026-05-12 22:24:54 +06:30
moon
d44163b601 MPU card reading data 2026-05-12 20:04:33 +06:30
moon
37a1a2e38d new ViewModels added 2026-05-12 19:17:21 +06:30
moon
d3a6ddbc37 card reader setup 2026-05-12 15:41:41 +06:30
221 changed files with 13439 additions and 4478 deletions

3
.gitignore vendored
View File

@ -14,3 +14,6 @@
.cxx
local.properties
/.idea
/.idea
/sunmiui-lib
/paysdk-lib

View File

@ -57,5 +57,6 @@
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="UsePropertyAccessSyntax" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
</profile>
</component>

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

96
CLAUDE.md Normal file
View File

@ -0,0 +1,96 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Build Commands
```powershell
# Build debug APK
./gradlew assembleDebug
# Build release APK
./gradlew assembleRelease
# Run unit tests
./gradlew test
# Run instrumented tests (requires connected device/emulator)
./gradlew connectedAndroidTest
# Run a single unit test class
./gradlew :app:test --tests "com.mob.utsmyammer.ExampleUnitTest"
# Clean build
./gradlew clean assembleDebug
```
## Architecture Overview
**MOBPOS** is a mobile Point-of-Sale system for Myanmar, targeting Sunmi hardware devices (card readers, PIN pads, thermal printers). It processes EMV card transactions (sale, void, refund, settlement) and QR payments (MMQR, Wave Pay).
### Single-Activity Compose App
`MainActivity` hosts the entire app. Navigation is handled by Compose Navigation (`AppNavGraph.kt`) with typed routes defined in a sealed `Routes` object.
### MVVM with Shared State
- Each screen has a dedicated `ViewModel` (Hilt-injected)
- `SharedViewModel` in `viewmodel/` is the central state holder for cross-screen transaction data (card info, amount, transaction type, approval codes)
- Hardware interaction is abstracted into `CardReaderViewModel` and `EmvTransactionProcessViewModel`
- State is a mix of `LiveData` (older screens) and `StateFlow` (newer screens like card reading)
### Screen Structure Pattern
Every feature follows: `ui/<feature>/` containing:
- `*Screen.kt` — Composable UI
- `*ViewModel.kt` — business logic and state
### Key Transaction Flow
Dashboard → InputAmount → CardWaiting → ProcessingCard → SendingToHost → TransactionResult → PrintReceipt
### Local Modules
The app depends on several local library modules (in `settings.gradle.kts`):
- `:baselib` — Room DB, Retrofit, Hilt base setup
- `:paylibs` / `:paysdk-lib` — Payment SDK wrappers
- `:ecr` / `:ecr-service-lib` — Electronic Cash Register integration
- `:mpulib` / `:mpu-lib` — Mini POS Unit hardware
- `:sunmiui-lib` — Sunmi device UI components
- `:qrgen-lib` — QR code generation
### Key Dependencies
- **UI:** Jetpack Compose + Material 3, Compose Navigation
- **DI:** Hilt 2.59.2 with KSP
- **Async:** RxJava 3 + RxAndroid (primary async pattern)
- **Network:** Retrofit 3 + OkHttp 4 + GSON
- **DB:** Room 2.4.3 with RxJava 3 support
- **Hardware:** Sunmi SDK (card reader, PIN pad, printer APIs)
### Package Layout
```
com.mob.utsmyammer/
├── config/ # Constants, SunmiPayManager
├── model/ # Data models (CardTransactionType, ProcessCode, TransactionStatus)
│ └── ecr/ # ECR-specific models
├── ui/ # All Compose screens (one subdir per feature)
│ ├── components/ # Shared reusables (NumericEntryScreen, SquareButton, AppBar)
│ ├── navigation/ # AppNavGraph, Routes
│ └── theme/ # Compose theme and colors
├── utils/ # Utility functions
└── viewmodel/ # Shared ViewModels (SharedViewModel, CardReaderViewModel, etc.)
```
## Hardware Integration Notes
Sunmi device permissions are declared in `AndroidManifest.xml` (LED, MSR, ICC, PINPAD, PRINTER). When modifying card reading or PIN entry flows, be aware that `CardReaderViewModel` and the Sunmi ECR service handle hardware callbacks via RxJava subjects. Hardware callbacks arrive on background threads — always observe on the main thread before updating UI state.
## Build Configuration
- `compileSdk 36`, `minSdk 24`, `targetSdk 36`
- Java 11 toolchain, Kotlin 2.2.10
- Compose BOM 2026.02.01
- ProGuard/minification is **disabled** for all build types
- Application ID: `com.mob.ustmm`

View File

@ -1,6 +1,8 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
id("com.google.devtools.ksp")
id("com.google.dagger.hilt.android")
}
android {
@ -12,8 +14,9 @@ android {
}
defaultConfig {
applicationId = "com.mob.ustmm"
applicationId = "com.mob.utsmyanmar"
minSdk = 24
//noinspection OldTargetApi
targetSdk = 36
versionCode = 1
versionName = "1.0"
@ -36,6 +39,7 @@ android {
}
buildFeatures {
compose = true
buildConfig = true
}
}
@ -44,10 +48,13 @@ dependencies {
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.runtime)
implementation(libs.androidx.compose.runtime.livedata)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.navigation.compose)
testImplementation(libs.junit)
@ -57,4 +64,29 @@ dependencies {
androidTestImplementation(libs.androidx.junit)
debugImplementation(libs.androidx.compose.ui.test.manifest)
debugImplementation(libs.androidx.compose.ui.tooling)
}
implementation(libs.hilt.android.v2592)
ksp(libs.hilt.android.compiler)
implementation(libs.rxjava)
implementation(libs.rxandroid)
implementation(libs.retrofit)
implementation(libs.converter.gson)
// Core icons (usually included with material library)
implementation( libs.androidx.compose.material.icons.core)
// Extended icons (full set of icons)
implementation(libs.androidx.compose.material.icons.extended)
// splash screen
implementation(libs.androidx.core.splashscreen)
//image lib
implementation("io.coil-kt.coil3:coil-compose:3.4.0")
implementation("io.coil-kt.coil3:coil-network-okhttp:3.4.0")
// local libs
implementation(project(":baselib"))
implementation(project(":mpulib"))
implementation(project(":paylibs"))
implementation(project(":paysdk-lib"))
implementation(project(":qrgen-lib"))
implementation(project(":sunmiui-lib"))
implementation(project(":ecr"))
implementation(project(":xpay"))
implementation(project(":cmhl"))
}

View File

@ -2,7 +2,32 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.hardware.telephony"
android:required="false" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="com.sunmi.perm.LED" />
<uses-permission android:name="com.sunmi.perm.MSR" />
<uses-permission android:name="com.sunmi.perm.ICC" />
<uses-permission android:name="com.sunmi.perm.PINPAD" />
<uses-permission android:name="com.sunmi.perm.SECURITY" />
<uses-permission android:name="com.sunmi.perm.CONTACTLESS_CARD" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.MODIFY_PHONE_STATE"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<application
android:name="com.mob.utsmyanmar.MyApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
@ -12,16 +37,14 @@
android:supportsRtl="true"
android:theme="@style/Theme.MOBPOS">
<activity
android:name=".MainActivity"
android:name="com.mob.utsmyanmar.MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.MOBPOS">
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
</manifest>

View File

@ -0,0 +1,3 @@
## Working agreements
- use Scaffold and AppBar() in every main Screen

View File

@ -1,17 +1,24 @@
package com.mob.utsmyanmar
import android.os.Bundle
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.navigation.compose.rememberNavController
import com.mob.utsmyanmar.ui.navigation.AppNavGraph
import com.mob.utsmyanmar.ui.theme.MOBPOSTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// installSplashScreen()
super.onCreate(savedInstanceState)
enableEdgeToEdge()
// enableEdgeToEdge()
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
setContent {
MOBPOSTheme {
val navController = rememberNavController()

View File

@ -0,0 +1,18 @@
package com.mob.utsmyanmar
import com.mob.utsmyanmar.utils.AppContextHolder
import com.utsmyanmar.baselib.BaseApplication
import com.utsmyanmar.paylibs.network.ISOSocket
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class MyApplication : BaseApplication() {
override fun onCreate() {
super.onCreate()
AppContextHolder.init(this)
ISOSocket.getInstance().initContext(this)
}
}

View File

@ -0,0 +1,13 @@
package com.mob.utsmyanmar.config
import com.mob.utsmyanmar.R
object Constants {
const val WALLET = "WALLET"
const val MPU_CARD_SCHEME = "MPU"
const val TIMEOUT = 30
const val PROCESSING_TIMEOUT = 180
const val PIN_PAD_TIMEOUT = 60
const val REVERSAL = "REVERSAL"
}

View File

@ -0,0 +1,43 @@
package com.mob.utsmyanmar.config
import com.sunmi.pay.hardware.aidl.security.SecurityOpt
import com.sunmi.pay.hardware.aidlv2.emv.EMVOptV2
import com.sunmi.pay.hardware.aidlv2.pinpad.PinPadOptV2
import com.sunmi.pay.hardware.aidlv2.readcard.ReadCardOptV2
import com.sunmi.pay.hardware.aidlv2.security.SecurityOptV2
import com.sunmi.pay.hardware.aidlv2.system.BasicOptV2
import com.utsmyanmar.baselib.BaseApplication
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class SunmiPayManager @Inject constructor() {
private val app: BaseApplication?
get() = BaseApplication.getInstance()
val pinPadOptV2: PinPadOptV2?
get() = app?.mPinPadOptV2
val securityOptV2: SecurityOptV2?
get() = app?.mSecurityOptV2
val readCardOptV2: ReadCardOptV2?
get() = app?.mReadCardOptV2
val emvOptV2: EMVOptV2?
get() = app?.mEMVOptV2
val basicOptV2: BasicOptV2?
get() = BaseApplication.basicOptV2
val securityOpt: SecurityOpt?
get() = app?.securityOpt
fun isReady(): Boolean {
return pinPadOptV2 != null &&
securityOptV2 != null &&
readCardOptV2 != null &&
emvOptV2 != null &&
basicOptV2 != null
}
}

View File

@ -0,0 +1,5 @@
package com.mob.utsmyanmar.model
enum class CardTransactionType {
MPU, EMV, MAG, FALLBACK, MOCK
}

View File

@ -0,0 +1,12 @@
package com.mob.utsmyanmar.model
enum class PinPadStatus {
ON_CONFIRM,
ON_CANCEL ,
ON_EMPTY,
ON_ERROR,
ON_TIMEOUT,
ON_NEXT_SCREEN,
ON_CARD_REMOVED,
ON_ERROR_DUKPT,
}

View File

@ -0,0 +1,39 @@
package com.mob.utsmyanmar.model
object ProcessCode {
const val BALANCE_INQUIRY = "31"
const val SALE_PURCHASE = "00"
const val SALE_VOID = "02"
const val PRE_AUTH_SALE = "30"
const val PRE_AUTH_VOID = "30"
const val PRE_AUTH_COMPLETE = "30"
const val PRE_AUTH_COMPLETE_VOID = "30"
const val CASH_ADVANCE = "01" // 01-MPU 17-TTIP
const val FUND_TRANSFER = "61"
const val PIN_CHANGE = "70"
const val SETTLEMENT = "92"
const val SIGN_ON = "95"
const val CASH_DEPOSIT = "21"
const val REFUND = "20"
const val SMART = "00"
const val SAVING = "10"
const val CURRENT = "20"
const val TO_ACCOUNT = "00"
}

View File

@ -0,0 +1,6 @@
package com.mob.utsmyanmar.model
enum class SettlementType {
NORMAL,
CUT_OVER
}

View File

@ -0,0 +1,29 @@
package com.mob.utsmyanmar.model
enum class TransResultStatus {
CLICK_CONFIRM,
SUCCESS,
FAIL,
OFFLINE_SUCCESS,
OFFLINE_FAILURE,
SECONDARY,
REVERSAL_PROCESS,
REVERSAL_PREPARE,
REVERSAL_SECONDARY,
REVERSAL_THIRD,
REVERSAL_FAIL,
REVERSAL_SUCCESS,
BEFORE_REVERSAL,
PIN_PAD_CANCEL,
PIN_PAD_CONFIRM,
PIN_PAD_ERROR,
PIN_MISMATCH,
PIN_MISMATCH_END,
REMOVED_CARD,
EMV_ERROR,
ERROR,
RETRY_AGAIN,
NEXT_SCREEN,
EMPTY_PIN,
NETWORK_ERROR
}

View File

@ -0,0 +1,11 @@
package com.mob.utsmyanmar.model
enum class TransactionStatus {
ON_SUCCESS,
ON_REVERSAL,
ON_BATCH_UPLOAD,
ON_FAIL,
ON_ERROR,
ON_SECONDARY,
ON_DONE
}

View File

@ -0,0 +1,7 @@
package com.mob.utsmyanmar.model.ecr
enum class ECRResultStatus {
USER_CANCEL,
TIME_OUT,
RESPONSE_RECEIVED
}

View File

@ -0,0 +1,14 @@
package com.mob.utsmyanmar.model.sirius
import com.utsmyanmar.baselib.network.model.sirius.SiriusHost
import com.utsmyanmar.baselib.network.model.sirius.SiriusMerchant
import com.utsmyanmar.baselib.network.model.sirius.SiriusProperty
data class SiriusResponse (
var serial: String,
var ecrKey: String,
var address: String,
var merchant : SiriusMerchant,
var hosts : List<SiriusHost>,
var properties: List<SiriusProperty>
)

View File

@ -0,0 +1,6 @@
package com.mob.utsmyanmar.model.sirius
enum class TMSUpdate {
UPDATE,
CHECK
}

View File

@ -0,0 +1,6 @@
package com.mob.utsmyanmar.model.sirius
data class TMSValidity(
var status: ValidityStatus? = null,
var message: String? = null
)

View File

@ -0,0 +1,6 @@
package com.mob.utsmyanmar.model.sirius
enum class ValidityStatus {
SUCCESS,
FAILURE
}

View File

@ -1,362 +0,0 @@
package com.mob.utsmyanmar.ui.amount
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
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.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.mob.utsmyanmar.ui.theme.MOBPOSTheme
import com.mob.utsmyanmar.ui.theme.Primary
import com.mob.utsmyanmar.ui.theme.White
import com.mob.utsmyanmar.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AmountScreen(
action: String,
onBackClick: () -> Unit,
onCancelClick: () -> Unit = {},
onNextClick: (String) -> Unit = {}
) {
var amount by remember {
mutableStateOf("0")
}
Scaffold(
topBar = {
CenterAlignedTopAppBar(
title = {
Text(
text = action.uppercase(),
color = White,
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold
)
},
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(
painter = painterResource(R.drawable.ic_left_arrow),
contentDescription = "Back",
modifier = Modifier.size(24.dp),
tint = White
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Primary
)
)
},
containerColor = White
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.background(White)
.verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(16.dp))
AmountBox(
amount = formatTypedAmount(amount),
height = 134.dp
)
Spacer(modifier = Modifier.height(24.dp))
Keypad(
buttonHeight = 74.dp,
buttonSpacing = 12.dp,
onKeyClick = { key ->
amount = handleAmountInput(amount, key)
}
)
Spacer(modifier = Modifier.height(18.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(14.dp)
) {
Button(
onClick = onCancelClick,
modifier = Modifier
.weight(1f)
.height(68.dp),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(
containerColor = White,
contentColor = Primary
),
border = ButtonDefaults.outlinedButtonBorder
) {
Text(
text = "Cancel",
fontSize = 20.sp,
fontWeight = FontWeight.Bold
)
}
Button(
onClick = {
onNextClick(normalizeAmount(amount))
},
modifier = Modifier
.weight(1f)
.height(68.dp),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Primary,
contentColor = White
)
) {
Text(
text = "Next",
fontSize = 20.sp,
fontWeight = FontWeight.Bold
)
}
}
Spacer(modifier = Modifier.height(12.dp))
}
}
}
@Composable
private fun AmountBox(
amount: String,
height: Dp
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(height)
.background(
color = Primary,
shape = RoundedCornerShape(20.dp)
)
.padding(horizontal = 24.dp),
contentAlignment = Alignment.CenterEnd
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = amount,
color = White,
fontSize = 30.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.width(14.dp))
Box(
modifier = Modifier
.background(
color = White,
shape = RoundedCornerShape(10.dp)
)
.padding(
horizontal = 16.dp,
vertical = 6.dp
)
) {
Text(
text = "MMK",
color = Primary,
fontSize = 20.sp,
fontWeight = FontWeight.Bold
)
}
}
}
}
@Composable
private fun Keypad(
buttonHeight: Dp,
buttonSpacing: Dp,
onKeyClick: (String) -> Unit
) {
val keys = listOf(
listOf("1", "2", "3"),
listOf("4", "5", "6"),
listOf("7", "8", "9"),
listOf(".", "0", "DEL")
)
Column(
verticalArrangement = Arrangement.spacedBy(buttonSpacing)
) {
keys.forEach { row ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
row.forEach { key ->
Button(
onClick = {
onKeyClick(key)
},
modifier = Modifier
.weight(1f)
.height(buttonHeight),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Primary,
contentColor = White
),
contentPadding = PaddingValues(0.dp)
) {
if (key == "DEL") {
Text(
text = "",
fontSize = 26.sp,
fontWeight = FontWeight.Bold
)
} else {
Text(
text = key,
fontSize = 22.sp,
fontWeight = FontWeight.Bold
)
}
}
}
}
}
}
}
private fun handleAmountInput(
current: String,
key: String
): String {
return when (key.uppercase()) {
"DEL" -> {
current
.dropLast(1)
.ifEmpty { "0" }
}
"." -> {
if (current.contains(".")) {
current
} else {
"${current.ifEmpty { "0" }}."
}
}
in "0".."9" -> {
val sanitizedCurrent = if (current == "0") "" else current
if (sanitizedCurrent.contains(".")) {
val decimal = sanitizedCurrent.substringAfter(".")
if (decimal.length >= 2) {
current
} else {
sanitizedCurrent + key
}
} else {
val nextValue = sanitizedCurrent + key
nextValue.trimStart('0').ifEmpty { "0" }
}
}
else -> current
}
}
private fun normalizeAmount(value: String): String {
if (value.isBlank()) return "0.00"
val normalized = if (value.endsWith(".")) {
value.dropLast(1)
} else {
value
}
return when {
normalized.isBlank() -> "0.00"
normalized.contains(".") -> {
val whole = normalized.substringBefore(".").ifEmpty { "0" }
val decimal = normalized.substringAfter(".")
when (decimal.length) {
0 -> "$whole.00"
1 -> "$whole.${decimal}0"
else -> "$whole.${decimal.take(2)}"
}
}
else -> "${normalized}.00"
}
}
private fun formatTypedAmount(value: String): String {
if (!value.contains(".")) return "$value.00"
val whole = value.substringBefore(".").ifEmpty { "0" }
val decimal = value.substringAfter(".")
return when (decimal.length) {
0 -> "$whole."
1 -> "$whole.${decimal}0"
else -> "$whole.${decimal.take(2)}"
}
}
@Preview(showBackground = true)
@Composable
private fun AmountScreenPreview() {
MOBPOSTheme {
AmountScreen(
action = "Amount",
onBackClick = {}
)
}
}

View File

@ -0,0 +1,9 @@
package com.mob.utsmyanmar.ui.cardwaiting
sealed interface CardWaitingEvent {
data object GoManualEntry : CardWaitingEvent
data object GoProcessingCard : CardWaitingEvent
data object GoTimeout : CardWaitingEvent
data object GoMain : CardWaitingEvent
data object GoBack : CardWaitingEvent
}

View File

@ -0,0 +1,446 @@
package com.mob.utsmyanmar.ui.cardwaiting
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.BorderStroke
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.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowLeft
import androidx.compose.material.icons.rounded.Wifi
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.mob.utsmyanmar.R
import com.mob.utsmyanmar.ui.preview.P2Preview
import com.mob.utsmyanmar.ui.theme.Color
@Composable
fun CardWaitingScreen(
viewModel: CardWaitingViewModel,
amount: String,
onManualEntry: () -> Unit,
onProcessingCard: () -> Unit,
onBack: () -> Unit,
onMain: () -> Unit
) {
val uiState by viewModel.uiState.collectAsState()
LaunchedEffect(viewModel) {
viewModel.events.collect { event ->
when (event) {
CardWaitingEvent.GoManualEntry -> onManualEntry()
CardWaitingEvent.GoProcessingCard -> onProcessingCard()
CardWaitingEvent.GoMain -> onMain()
CardWaitingEvent.GoBack -> onBack()
CardWaitingEvent.GoTimeout -> Unit
}
}
}
LaunchedEffect(viewModel) {
viewModel.onScreenResume()
}
DisposableEffect(viewModel) {
onDispose {
viewModel.onScreenPause()
}
}
BackHandler(enabled = uiState.canGoBack) {
viewModel.onBackPressed()
}
CardWaitingScreenContent(
amount = amount,
uiState = uiState,
onBackClick = viewModel::onBackPressed,
onManualEntryClick = viewModel::onManualEntryClick,
onMockClick = viewModel::onMockClick
)
}
@Composable
private fun CardWaitingScreenContent(
amount: String,
uiState: CardWaitingUiState,
onBackClick: () -> Unit,
onManualEntryClick: () -> Unit,
onMockClick: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.IvoryBeige)
.statusBarsPadding()
.navigationBarsPadding()
.padding(horizontal = 20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(54.dp),
contentAlignment = Alignment.Center
) {
if (uiState.canGoBack) {
IconButton(
onClick = onBackClick,
modifier = Modifier.align(Alignment.CenterStart)
) {
Icon(
imageVector = Icons.Rounded.KeyboardArrowLeft,
contentDescription = "Back",
tint = Color.LegacyRed
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "AMOUNT TO PAY",
color = Color.Black,
fontSize = 10.sp,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(6.dp))
Row(verticalAlignment = Alignment.Bottom) {
Text(
text = amount,
color = Color.LegacyRed,
fontSize = 30.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = "MMK",
color = Color.LegacyRed,
fontSize = 10.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 6.dp)
)
}
Spacer(modifier = Modifier.height(32.dp))
ContactlessCircle(isLoading = uiState.isLoading)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = when {
uiState.isCardCaptured -> "Card Detected"
uiState.isFallback -> "Swipe Your Card"
else -> "Tap Your Card"
},
color = Color.LegacyRed,
fontSize = 13.sp,
fontWeight = FontWeight.Bold
)
Text(
text = if (uiState.isCardCaptured) {
"Reader captured card data"
} else {
"Hold your card near the reader"
},
color = Color.Black,
fontSize = 9.sp
)
Spacer(modifier = Modifier.height(14.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
HorizontalDivider(
modifier = Modifier.weight(1f),
color = Color.Gray.copy(alpha = 0.4f)
)
Text(
text = "OR",
modifier = Modifier.padding(horizontal = 12.dp),
color = Color.Gray,
fontSize = 10.sp
)
HorizontalDivider(
modifier = Modifier.weight(1f),
color = Color.Gray.copy(alpha = 0.4f)
)
}
Spacer(modifier = Modifier.height(12.dp))
InsertCardRow(onMockClick = onMockClick)
Spacer(modifier = Modifier.height(18.dp))
Text(
text = uiState.alertMessage,
color = Color.Black,
fontSize = 11.sp,
textAlign = TextAlign.Center,
lineHeight = 16.sp
)
Spacer(modifier = Modifier.height(18.dp))
// StatusPanel(uiState = uiState)
Spacer(modifier = Modifier.weight(1f))
ManualEntryAction(onManualEntryClick = onManualEntryClick)
Spacer(modifier = Modifier.height(18.dp))
CardLogoRow()
Spacer(modifier = Modifier.height(34.dp))
PoweredByMob()
Spacer(modifier = Modifier.height(12.dp))
}
}
@Composable
private fun ContactlessCircle(isLoading: Boolean) {
Box(
modifier = Modifier.size(190.dp),
contentAlignment = Alignment.Center
) {
CircleBorder(180, 0.1f)
CircleBorder(150, 0.20f)
Surface(
modifier = Modifier.size(108.dp),
shape = CircleShape,
color = Color.White,
shadowElevation = 8.dp
) {
Box(contentAlignment = Alignment.Center) {
if (isLoading) {
CircularProgressIndicator(
color = Color.LegacyRed,
strokeWidth = 3.dp,
modifier = Modifier.size(40.dp)
)
} else {
Icon(
imageVector = Icons.Rounded.Wifi,
contentDescription = null,
tint = Color.LegacyRed,
modifier = Modifier
.size(72.dp)
.rotate(90f)
)
}
}
}
}
}
@Composable
private fun CircleBorder(
size: Int,
alpha: Float
) {
Surface(
modifier = Modifier
.size(size.dp)
.alpha(alpha),
shape = CircleShape,
border = BorderStroke(1.dp, Color.LegacyRed)
) {}
}
@Composable
private fun InsertCardRow(onMockClick: ()->Unit) {
Row(
modifier = Modifier.height(44.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.drawable.ic_insert_card),
contentDescription = null,
tint = Color.LegacyRed,
modifier = Modifier.size(28.dp)
.clickable(onClick = onMockClick)
)
Spacer(modifier = Modifier.width(10.dp))
Column {
Text(
text = "Insert Your Card",
color = Color.LegacyRed,
fontSize = 11.sp,
fontWeight = FontWeight.Bold
)
Text(
text = "Chip facing up",
color = Color.Black,
fontSize = 9.sp
)
}
}
}
@Composable
private fun StatusPanel(uiState: CardWaitingUiState) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = Color.White,
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 14.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = when {
uiState.isCardCaptured -> "Reader status"
uiState.isLoading -> "Reader status"
else -> "Ready for card"
},
color = Color.LegacyRed,
fontSize = 12.sp,
fontWeight = FontWeight.Bold
)
Text(
text = when {
uiState.isCardCaptured -> "Captured"
uiState.isLoading -> "Initializing"
else -> "Waiting"
},
color = Color.Gray,
fontSize = 11.sp
)
}
}
}
@Composable
private fun ManualEntryAction(onManualEntryClick: () -> Unit) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = Color.White,
shape = RoundedCornerShape(12.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(Color.White)
.clickable(onClick = onManualEntryClick)
.padding(vertical = 16.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "Manual Entry",
color = Color.LegacyRed,
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
}
}
}
@Composable
private fun CardLogoRow() {
Surface(
modifier = Modifier.fillMaxWidth(),
color = Color.White,
shape = RoundedCornerShape(3.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(42.dp)
.padding(horizontal = 14.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("VISA", fontSize = 22.sp, fontWeight = FontWeight.Bold)
Text("MC", fontSize = 18.sp, fontWeight = FontWeight.Bold)
Text("MPU", fontSize = 20.sp, fontWeight = FontWeight.Bold)
Text("UnionPay", fontSize = 12.sp, fontWeight = FontWeight.Bold)
}
}
}
@Composable
private fun PoweredByMob() {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "Powered by",
color = Color.Gray,
fontSize = 8.sp
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "MOB",
color = Color.Gray,
fontSize = 9.sp,
fontWeight = FontWeight.Bold
)
}
}
@P2Preview
@Composable
fun PreviewCardWaitingScreen() {
CardWaitingScreenContent(
amount = "50,000",
uiState = CardWaitingUiState(),
onBackClick = {},
onManualEntryClick = {},
onMockClick = {}
)
}

View File

@ -0,0 +1,9 @@
package com.mob.utsmyanmar.ui.cardwaiting
data class CardWaitingUiState(
val alertMessage: String = "Please insert, tap, or swipe card",
val isLoading: Boolean = false,
val isFallback: Boolean = false,
val canGoBack: Boolean = true,
val isCardCaptured: Boolean = false
)

View File

@ -0,0 +1,335 @@
package com.mob.utsmyanmar.ui.cardwaiting
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.mob.utsmyanmar.model.CardTransactionType
import com.mob.utsmyanmar.model.ecr.ECRResultStatus
import com.mob.utsmyanmar.utils.CoreUtils
import com.mob.utsmyanmar.viewmodel.CardReaderViewModel
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.sunmi.pay.hardware.aidl.AidlConstants
import com.utsmyanmar.checkxread.checkcard.CheckCardResultX
import com.utsmyanmar.checkxread.util.CardTypeX
import com.utsmyanmar.ecr.ECRHelper
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import sunmi.sunmiui.utils.LogUtil
class CardWaitingViewModel(
private val cardReadViewModel: CardReaderViewModel,
private val sharedViewModel: SharedViewModel
) : ViewModel() {
companion object {
fun provideFactory(
cardReadViewModel: CardReaderViewModel,
sharedViewModel: SharedViewModel
): ViewModelProvider.Factory {
return object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return CardWaitingViewModel(
cardReadViewModel = cardReadViewModel,
sharedViewModel = sharedViewModel
) as T
}
}
}
}
private val _uiState = MutableStateFlow(CardWaitingUiState())
val uiState = _uiState.asStateFlow()
private val _events = Channel<CardWaitingEvent>()
val events = _events.receiveAsFlow()
private var retryCounter = 0
private var fallbackCounter = 0
private var fallbackEnabled = false
private var readerInitJob: Job? = null
fun onScreenResume() {
retryCounter = 0
fallbackCounter = SystemParamsOperation.getInstance().fallbackCounter
fallbackEnabled = SystemParamsOperation.getInstance().fallbackEnabled
val isSaleTransaction = sharedViewModel.transactionsType.value == TransactionsType.SALE
_uiState.update {
it.copy(canGoBack = !isSaleTransaction)
}
if (sharedViewModel.transactionsType.value == TransactionsType.REFUND) {
sharedViewModel.enableCardStatusIcon(false, false, false, false)
} else {
sharedViewModel.enableCardStatusIcon(true, true, true, false)
}
val isFallback = sharedViewModel.getIsFallback().value == true
if (isFallback) {
_uiState.update {
it.copy(
alertMessage = "Fallback!\nPlease stripe!",
isFallback = true,
isCardCaptured = false
)
}
} else {
_uiState.update {
it.copy(
alertMessage = "Please insert, tap, or swipe card",
isFallback = false,
isLoading = false,
isCardCaptured = false
)
}
}
startCardReadWhenReady(isFallback)
}
fun onScreenPause() {
readerInitJob?.cancel()
stopCardReading()
}
fun onBackPressed() {
if (sharedViewModel.isEcr.value == true) {
sharedViewModel.isEcr.postValue(false)
CoreUtils.getInstance(sharedViewModel).responseRejectMsg("Transaction cancelled")
sharedViewModel.isEcrFinished.postValue(true)
}
stopCardReading()
viewModelScope.launch {
_events.send(CardWaitingEvent.GoBack)
}
}
fun onManualEntryClick() {
viewModelScope.launch {
_events.send(CardWaitingEvent.GoManualEntry)
}
}
fun onMockClick() {
onScreenPause()
cardReadViewModel.setCardTransactionType(CardTransactionType.MOCK)
viewModelScope.launch {
_uiState.update {
it.copy(
alertMessage = "Mock card detected.",
isLoading = false,
isCardCaptured = true
)
}
_events.send(CardWaitingEvent.GoProcessingCard)
}
}
private fun startCardReadWhenReady(isFallback: Boolean) {
readerInitJob?.cancel()
readerInitJob = viewModelScope.launch {
if (cardReadViewModel.isReaderReady()) {
setupCardReadProcess(isFallback)
return@launch
}
_uiState.update {
it.copy(
alertMessage = "Initializing card reader...",
isLoading = true,
isCardCaptured = false
)
}
repeat(10) {
delay(300)
if (cardReadViewModel.isReaderReady()) {
_uiState.update { state ->
state.copy(
alertMessage = if (isFallback) {
"Fallback!\nPlease stripe!"
} else {
"Please insert, tap, or swipe card"
},
isLoading = false,
isCardCaptured = false
)
}
setupCardReadProcess(isFallback)
return@launch
}
}
_uiState.update {
it.copy(
alertMessage = "Card reader unavailable.\nPlease wait and try again.",
isLoading = false
)
}
}
}
private fun setupCardReadProcess(isFallback: Boolean) {
initCheckCard(isFallback)
}
private fun initCheckCard(isFallback: Boolean) {
var allType =
AidlConstants.CardType.NFC.value or
AidlConstants.CardType.IC.value or
AidlConstants.CardType.MAGNETIC.value
if (isFallback) {
allType = AidlConstants.CardType.MAGNETIC.value
} else if (
SystemParamsOperation.getInstance().isMagStripeEnabled &&
!SystemParamsOperation.getInstance().isNfcEnabled
) {
allType =
AidlConstants.CardType.IC.value or
AidlConstants.CardType.MAGNETIC.value
}
cardReadViewModel.startCheckXProcess(
allType,
65,
object : CheckCardResultX {
override fun onSuccess(cardType: CardTypeX, isMPU: Boolean) {
Log.d("CardWaitingViewModel", "on success cardType=$cardType and isMpu=$isMPU")
when {
!isFallback && cardType == CardTypeX.MAG -> {
if (SystemParamsOperation.getInstance().isMagStripeEnabled) {
cardReadViewModel.setCardTransactionType(CardTransactionType.MAG)
} else {
_uiState.update {
it.copy(alertMessage = "Mag stripe not allowed")
}
setupCardReadProcess(false)
return
}
}
isFallback && cardType == CardTypeX.MAG -> {
sharedViewModel.setEmvTrans(false)
cardReadViewModel.setCardTransactionType(CardTransactionType.FALLBACK)
}
cardType == CardTypeX.IC || cardType == CardTypeX.NFC -> {
if (isMPU) {
sharedViewModel.setEmvTrans(false)
cardReadViewModel.setCardTransactionType(CardTransactionType.MPU)
} else {
cardReadViewModel.setCardData(cardType.value)
cardReadViewModel.setCardTransactionType(CardTransactionType.EMV)
}
}
}
viewModelScope.launch {
_uiState.update {
it.copy(
alertMessage = "Card detected.",
isLoading = false,
isCardCaptured = true
)
}
_events.send(CardWaitingEvent.GoProcessingCard)
}
}
override fun onError(code: Int, message: String) {
ecrActionCancel("Transaction cancelled")
viewModelScope.launch {
_uiState.update {
it.copy(
alertMessage = message,
isLoading = false
)
}
_events.send(CardWaitingEvent.GoMain)
}
}
override fun onCommError() {
if (fallbackEnabled && retryCounter < fallbackCounter) {
_uiState.update {
it.copy(
alertMessage = "Card not detected!\nRemain Attempt - ${fallbackCounter - retryCounter}"
)
}
retryCounter++
setupCardReadProcess(false)
return
}
if (retryCounter == fallbackCounter) {
_uiState.update {
it.copy(alertMessage = "Fallback!\nPlease stripe!")
}
setupCardReadProcess(true)
return
}
ecrActionCancel("Transaction cancelled")
viewModelScope.launch {
_uiState.update {
it.copy(
alertMessage = "Chip not detected!",
isLoading = false
)
}
_events.send(CardWaitingEvent.GoMain)
}
}
}
)
}
private fun ecrActionCancel(msg: String) {
if (sharedViewModel.isEcr.value != true) {
return
}
sharedViewModel.isEcr.postValue(false)
sharedViewModel.isEcrFinished.postValue(true)
if (SystemParamsOperation.getInstance().isCMHLEnabled) {
ECRHelper.send(
CoreUtils.getInstance(sharedViewModel)
.generateCMHLResponse(ECRResultStatus.USER_CANCEL)
)
CoreUtils.getInstance(sharedViewModel).responseACKCMHL()
return
}
CoreUtils.getInstance(sharedViewModel).responseRejectMsg(msg)
}
private fun stopCardReading() {
sharedViewModel.setIsFallback(false)
cardReadViewModel.cancelCheckCard()
cardReadViewModel.resetOneTimeFlag()
cardReadViewModel.cancelCheckXProcess()
}
override fun onCleared() {
super.onCleared()
readerInitJob?.cancel()
stopCardReading()
}
}

View File

@ -0,0 +1,242 @@
package com.mob.utsmyanmar.ui.components
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.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.Backspace
import androidx.compose.material.icons.rounded.Backspace
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.mob.utsmyanmar.ui.theme.Color
@Composable
fun NumericEntryScreen(
title: String,
prompt: String,
displayValue: String,
supportingText: String,
confirmText: String,
onBackClick: () -> Unit,
onCancelClick: () -> Unit,
onConfirmClick: () -> Unit,
onKeyClick: (String) -> Unit,
onDeleteClick: () -> Unit,
modifier: Modifier = Modifier,
prefixLabel: String? = null,
errorMessage: String? = null,
confirmEnabled: Boolean = true,
canDelete: Boolean = true,
keys: List<List<String>> = listOf(
listOf("1", "2", "3"),
listOf("4", "5", "6"),
listOf("7", "8", "9"),
listOf(".", "0", "00")
)
) {
Box(
modifier = modifier
.fillMaxSize()
.background(Color.IvoryBeige)
.navigationBarsPadding()
.statusBarsPadding()
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 20.dp)
) {
Text(
text = prompt,
color = Color.Gray,
fontSize = 11.sp,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.align(Alignment.CenterHorizontally),
verticalAlignment = Alignment.Bottom
) {
if (!prefixLabel.isNullOrBlank()) {
Text(
text = prefixLabel,
color = Color.LegacyRed,
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(end = 10.dp, bottom = 6.dp)
)
}
Text(
text = displayValue,
color = Color.LegacyRed,
fontSize = 32.sp,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.height(12.dp))
Text(
text = supportingText,
color = if (errorMessage == null) Color.Gray else Color.Error,
fontSize = 11.sp,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(80.dp))
NumericKeypad(
keys = keys,
onKeyClick = onKeyClick
)
Spacer(modifier = Modifier.weight(1f))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Button(
onClick = onCancelClick,
modifier = Modifier
.weight(1f)
.height(56.dp),
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.White,
contentColor = Color.LegacyRed
)
) {
Text("Cancel")
}
Button(
onClick = onConfirmClick,
modifier = Modifier
.weight(1f)
.height(56.dp),
enabled = confirmEnabled,
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.LegacyRed,
contentColor = Color.White,
disabledContainerColor = Color.LegacyRed.copy(alpha = 0.5f),
disabledContentColor = Color.White
)
) {
Text(
text = confirmText,
fontSize = 14.sp,
fontWeight = FontWeight.Medium
)
}
}
Spacer(modifier = Modifier.height(18.dp))
}
Card(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 25.dp, end = 20.dp)
.clickable(enabled = canDelete) {
onDeleteClick()
},
shape = RoundedCornerShape(18.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.Backspace,
contentDescription = "Delete",
tint = if (canDelete) Color.LegacyRed else Color.Gray,
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
)
}
}
}
@Composable
private fun NumericKeypad(
keys: List<List<String>>,
onKeyClick: (String) -> Unit
) {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
keys.forEach { row ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
row.forEach { key ->
KeypadButton(
text = key,
modifier = Modifier.weight(1f),
onClick = { onKeyClick(key) }
)
}
}
}
}
}
@Composable
private fun KeypadButton(
text: String,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
val enabled = text.isNotBlank()
Box(
modifier = modifier
.height(66.dp)
.shadow(
elevation = 2.dp,
shape = RoundedCornerShape(8.dp),
clip = false
)
.background(
color = Color.White,
shape = RoundedCornerShape(8.dp)
)
.clickable(enabled = enabled) { onClick() },
contentAlignment = Alignment.Center
) {
Text(
text = text,
color = if (enabled) Color.LegacyRed else Color.White,
fontSize = 24.sp,
fontWeight = FontWeight.Normal,
textAlign = TextAlign.Center
)
}
}

View File

@ -0,0 +1,59 @@
package com.mob.utsmyanmar.ui.components.appbar
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import com.mob.utsmyanmar.ui.theme.Color
import com.mob.utsmyanmar.ui.theme.Primary
import com.mob.utsmyanmar.ui.theme.White
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppBar(
title: String,
icon: ImageVector? = null,
onIconClick: (() -> Unit)? = null,
) {
CenterAlignedTopAppBar(
title = {
Text(
text = title,
color = White,
fontWeight = FontWeight.SemiBold
)
},
navigationIcon = {
if (icon != null && onIconClick != null) {
IconButton(
onClick = onIconClick
) {
Icon(
imageVector = icon,
contentDescription = "App bar icon",
tint = White
)
}
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.LegacyRed
)
)
}
@Preview
@Composable
fun PreviewAppBar(){
AppBar(
title = "Title"
)
}

View File

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

View File

@ -1,10 +1,13 @@
package com.mob.utsmyanmar.ui.dashboard
import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
@ -20,23 +23,19 @@ import com.mob.utsmyanmar.ui.theme.MOBPOSTheme
import com.mob.utsmyanmar.ui.theme.*
import kotlinx.coroutines.launch
import com.mob.utsmyanmar.R
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.utsmyanmar.paylibs.print.NewPrintReceipt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DashboardScreen(
settlementEnabled: Boolean,
wavePayEnabled: Boolean,
onAmountClick: (String) -> Unit,
onTransactionClick: () -> Unit,
onSettlementClick: () -> Unit,
onHistoryClick: () -> Unit,
onCardClick: () -> Unit,
onWavePayClick: () -> Unit
onNavigateAmount: (String) -> Unit
) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
@ -68,7 +67,6 @@ fun DashboardScreen(
.fillMaxWidth()
.clickable {
scope.launch { drawerState.close() }
onTransactionClick()
}
.padding(horizontal = 24.dp, vertical = 14.dp),
fontSize = 16.sp
@ -79,7 +77,6 @@ fun DashboardScreen(
.fillMaxWidth()
.clickable {
scope.launch { drawerState.close() }
onHistoryClick()
}
.padding(horizontal = 24.dp, vertical = 14.dp),
fontSize = 16.sp
@ -90,7 +87,6 @@ fun DashboardScreen(
.fillMaxWidth()
.clickable(enabled = settlementEnabled) {
scope.launch { drawerState.close() }
onSettlementClick()
}
.padding(horizontal = 24.dp, vertical = 14.dp),
color = if (settlementEnabled) Black else White,
@ -102,7 +98,6 @@ fun DashboardScreen(
.fillMaxWidth()
.clickable(enabled = wavePayEnabled) {
scope.launch { drawerState.close() }
onWavePayClick()
}
.padding(horizontal = 24.dp, vertical = 14.dp),
color = if (wavePayEnabled) Black else White,
@ -113,28 +108,14 @@ fun DashboardScreen(
) {
Scaffold(
topBar = {
CenterAlignedTopAppBar(
title = {
Text(text = "Dashboard", color = White, fontWeight = FontWeight.SemiBold)
},
navigationIcon = {
IconButton(
onClick = {
scope.launch {
drawerState.open()
}
}
) {
Icon(
painter = painterResource(R.drawable.ic_menu),
contentDescription = "Menu Icon",
tint = White
)
AppBar(
title = "Dashboard",
icon = Icons.Default.Menu,
onIconClick = {
scope.launch {
drawerState.open()
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Primary
)
}
)
}
) { paddingValues ->
@ -175,6 +156,16 @@ fun DashboardScreen(
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Button(
onClick = { try {
NewPrintReceipt.getInstance().testPrint()
println("printing...")
}catch (e: Exception){
println("printing error $e")
} }
) {
Text(text = "test")
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
@ -187,7 +178,7 @@ fun DashboardScreen(
contentColor = Primary,
iconTint = Primary,
border = null,
onClick = { onAmountClick("Sale") },
onClick = { onNavigateAmount("Sale") },
)
SquareButton(
title = "Sign On",
@ -197,7 +188,7 @@ fun DashboardScreen(
contentColor = Primary,
iconTint = Primary,
border = null,
onClick = { onAmountClick("Sign On") }
onClick = { onNavigateAmount("Sign On") }
)
}
@ -214,7 +205,7 @@ fun DashboardScreen(
contentColor = Primary,
iconTint = Primary,
border = null,
onClick = onSettlementClick
onClick = {}
)
SquareButton(
title = "Others",
@ -224,7 +215,7 @@ fun DashboardScreen(
contentColor = Primary,
iconTint = Primary,
border = null,
onClick = onCardClick
onClick = {}
)
}
}
@ -241,12 +232,7 @@ private fun DashboardScreenPreview() {
DashboardScreen(
settlementEnabled = true,
wavePayEnabled = true,
onAmountClick = {},
onTransactionClick = {},
onSettlementClick = {},
onHistoryClick = {},
onCardClick = {},
onWavePayClick = {}
onNavigateAmount = {}
)
}
}

View File

@ -0,0 +1,638 @@
package com.mob.utsmyanmar.ui.dashboard
import android.os.Handler
import android.os.Looper
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import coil3.compose.AsyncImage
import com.mob.utsmyanmar.R
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.device_info.DeviceInfoViewModel
import com.mob.utsmyanmar.ui.preview.P2Preview
import com.mob.utsmyanmar.ui.preview.P3Preview
import com.mob.utsmyanmar.ui.theme.Color
import com.utsmyanmar.paylibs.sign_on.EchoTestProcess
import com.utsmyanmar.paylibs.sign_on.SignOnListener
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
fun DashboardScreen2(
onNavigateAmount: (String) -> Unit = {},
onNavigateSignOn: () -> Unit = {},
onNavigateSeeMore: () -> Unit = {},
onNavigateSettlement: () -> Unit = {},
onNavigateVersion: () -> Unit = {},
onNavigateFunctions: () -> Unit = {},
deviceInfoViewModel: DeviceInfoViewModel = viewModel()
) {
val deviceInfo by deviceInfoViewModel.uiState.collectAsState()
LaunchedEffect(Unit) {
deviceInfoViewModel.loadDeviceInfo()
}
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
val mainHandler = remember { Handler(Looper.getMainLooper()) }
var showHostActionDialog by remember { mutableStateOf(false) }
var activeHostAction by remember { mutableStateOf("Log-On") }
var isHostActionRunning by remember { mutableStateOf(false) }
var dialogMessage by remember { mutableStateOf("") }
val isOnline = true
fun confirmationMessage(action: String) = "Do you want to start ${action.lowercase()}?"
fun processingMessage(action: String) = "Sending ${action.lowercase()} request to host..."
fun successMessage(action: String) = "$action success."
fun failureMessage(action: String, resultCode: Int?) =
"$action failed. Response code: ${resultCode ?: -1}"
fun networkFailureMessage(action: String) = "Network error during $action."
fun openHostActionDialog(action: String) {
activeHostAction = action
isHostActionRunning = false
dialogMessage = confirmationMessage(action)
showHostActionDialog = true
}
if (showHostActionDialog) {
AlertDialog(
onDismissRequest = {
if (!isHostActionRunning) {
showHostActionDialog = false
}
}, title = {
Text(
text = activeHostAction, color = Color.LegacyRed, fontWeight = FontWeight.Bold
)
}, text = {
Text(text = dialogMessage)
}, confirmButton = {
TextButton(
onClick = {
if (isHostActionRunning) return@TextButton
if (dialogMessage != confirmationMessage(activeHostAction)) {
showHostActionDialog = false
dialogMessage = confirmationMessage(activeHostAction)
return@TextButton
}
isHostActionRunning = true
dialogMessage = processingMessage(activeHostAction)
val signOnProcess = EchoTestProcess.getInstance()
val request = when (activeHostAction) {
"Echo Test" -> signOnProcess.enqueue(false)
"Log-Off" -> signOnProcess.enqueueLogOff()
else -> signOnProcess.enqueueLogOn()
}
request.startSignOn(object : SignOnListener {
override fun onSuccessSignOn() {
mainHandler.post {
isHostActionRunning = false
dialogMessage = successMessage(activeHostAction)
}
}
override fun onFailureSignOn(resultCode: Int?) {
mainHandler.post {
isHostActionRunning = false
mainHandler.post {
isHostActionRunning = false
dialogMessage = failureMessage(activeHostAction, resultCode)
}
}
}
override fun onNetworkFailSignOn(message: String?) {
mainHandler.post {
isHostActionRunning = false
dialogMessage = message?.takeIf { it.isNotBlank() }
?: networkFailureMessage(activeHostAction)
}
}
})
}) {
Text(
text = when {
isHostActionRunning -> "Processing"
dialogMessage == confirmationMessage(activeHostAction) -> "Start"
else -> "Close"
}, color = Color.LegacyRed
)
}
}, dismissButton = {
if (!isHostActionRunning) {
TextButton(
onClick = {
showHostActionDialog = false
dialogMessage = confirmationMessage(activeHostAction)
}) {
Text(text = "Cancel", color = Color.Gray)
}
}
}, containerColor = Color.White
)
}
ModalNavigationDrawer(
drawerState = drawerState, drawerContent = {
ModalDrawerSheet(
modifier = Modifier.fillMaxWidth(0.78f),
drawerContainerColor = Color.White,
drawerShape = RoundedCornerShape(topEnd = 24.dp, bottomEnd = 24.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(Color.IvoryBeige)
.padding(horizontal = 20.dp, vertical = 28.dp)
) {
Row {
Box(
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.background(Color.White), contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(R.drawable.logo_mob),
contentDescription = "mob logo",
modifier = Modifier
.size(100.dp)
.padding(16.dp),
contentScale = ContentScale.Fit
)
}
Column {
Text(
text = "MOB Merchant",
color = Color.CrimsonRed,
fontSize = 12.sp,
fontWeight = FontWeight.Bold
)
Text(
text = "S/N:${deviceInfo.serialNumber}",
color = Color.Black,
fontSize = 12.sp,
fontWeight = FontWeight.Bold
)
Text(
text = if (isOnline) "Online" else "Offline",
color = if (isOnline) Color.Success else Color.Error,
fontSize = 12.sp,
fontWeight = FontWeight.Bold
)
}
}
}
Spacer(modifier = Modifier.height(10.dp))
DrawerItem("Log-On", Icons.Default.Dashboard) {
scope.launch { drawerState.close() }
openHostActionDialog("Log-On")
}
DrawerItem("Echo Test", Icons.Default.Sync) {
scope.launch { drawerState.close() }
openHostActionDialog("Echo Test")
}
DrawerItem("Log-Off", Icons.Default.Dashboard) {
scope.launch { drawerState.close() }
openHostActionDialog("Log-Off")
}
Text(
text = "System Management",
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(horizontal = 16.dp)
)
var switchChecked by remember { mutableStateOf(SystemParamsOperation.getInstance().isReversalOn) }
DrawerItem(
title = "Reversal On/Off",
icon = Icons.Default.Sync,
showSwitch = true,
isChecked = switchChecked,
onCheckedChange = { isChecked ->
switchChecked = isChecked
SystemParamsOperation.getInstance().setReversalFlag(isChecked)
},
onClick = {}
)
DrawerItem("Function", Icons.Default.Dashboard) {
scope.launch { drawerState.close() }
onNavigateFunctions()
}
DrawerItem("Version", Icons.Default.Dashboard) {
scope.launch { drawerState.close() }
onNavigateVersion()
}
}
}) {
Scaffold(
containerColor = Color.IvoryBeige, topBar = {
AppBar(
title = "Dashboard",
icon = Icons.Default.Menu,
onIconClick = { scope.launch { drawerState.open() } })
}) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
) {
//top section
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
) {
AdvertisingArea()
}
//center section
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
SummaryCard()
}
//bottom section
Box(
modifier = Modifier
.weight(1.5f)
.fillMaxWidth(),
) {
MenuGrid(
onNavigateAmount = onNavigateAmount,
onNavigateSignOn = onNavigateSignOn,
onNavigateSeeMore = onNavigateSeeMore,
onNavigateSettlement = onNavigateSettlement
)
}
}
}
}
}
@Composable
private fun DrawerItem(
title: String,
icon: ImageVector,
showSwitch: Boolean = false, // New: Flag to enable switch mode
isChecked: Boolean = false, // New: Switch state
onCheckedChange: (Boolean) -> Unit = {}, // New: Switch callback
onClick: () -> Unit
) {
NavigationDrawerItem(
label = {
Text(
text = title,
fontWeight = FontWeight.Medium
)
},
selected = false,
// If it's a switch item, clicking the whole row toggles the switch instead of navigating
onClick = {
if (showSwitch) {
onCheckedChange(!isChecked)
} else {
onClick()
}
},
icon = {
Icon(
imageVector = icon,
contentDescription = title
)
},
badge = {
if (showSwitch) {
Switch(
checked = isChecked,
onCheckedChange = onCheckedChange
)
}
},
modifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp),
colors = NavigationDrawerItemDefaults.colors(
unselectedContainerColor = androidx.compose.ui.graphics.Color.Transparent,
unselectedIconColor = Color.LegacyRed,
unselectedTextColor = Color.Black
)
)
}
@Composable
private fun AdvertisingArea() {
val imageArray = listOf(
"https://i.ytimg.com/vi/eRUVxGRp1Ms/maxresdefault.jpg",
"https://i.ytimg.com/vi/AwvmgTPd7qw/maxresdefault.jpg",
"https://mma.prnewswire.com/media/2080956/SUNMI_3rd_generation_products_T3_PRO_series_V3_MIX.jpg?p=facebook"
)
val pageState = rememberPagerState(pageCount = { imageArray.size })
LaunchedEffect(pageState) {
while (true) {
delay(10000)
val nextPage = (pageState.currentPage + 1) % imageArray.size
pageState.animateScrollToPage(
page = nextPage, animationSpec = tween(durationMillis = 700)
)
}
}
Card(
modifier = Modifier.fillMaxSize(),
shape = RoundedCornerShape(0.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
) {
HorizontalPager(
state = pageState, modifier = Modifier.fillMaxSize()
) { page ->
AsyncImage(
model = imageArray[page],
contentDescription = "ads images",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
}
}
}
@Composable
private fun SummaryCard() {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(4.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Column(modifier = Modifier.weight(1f)) {
SummaryItem(
title = "Sales Today",
value = "MMK 2,000,000",
subtitle = "47 Transactions",
icon = Icons.Default.BarChart,
iconBg = Color.CrimsonRed
)
HorizontalDivider(
color = Color.Gray,
modifier = Modifier.padding(vertical = 6.dp, horizontal = 6.dp)
)
SummaryItem(
title = "Settlement",
value = "Completed",
subtitle = "Today 11:00 PM",
icon = Icons.Default.Check,
iconBg = Color.CrimsonRed
)
}
VerticalDivider(color = Color.Gray, modifier = Modifier.height(160.dp))
Column(
modifier = Modifier
.weight(0.75f)
.padding(start = 18.dp),
horizontalAlignment = Alignment.Start
) {
Row(verticalAlignment = Alignment.Bottom) {
Text(
text = "Last Sync", fontSize = 12.sp
)
IconCircle(Icons.Default.Sync, Color.CrimsonRed)
}
Spacer(Modifier.height(6.dp))
Text(
text = "5 mins ago",
fontSize = 18.sp,
color = Color.LegacyRed,
fontWeight = FontWeight.SemiBold
)
}
}
}
}
@Composable
private fun SummaryItem(
title: String,
value: String,
subtitle: String,
icon: ImageVector,
iconBg: androidx.compose.ui.graphics.Color
) {
Row(
verticalAlignment = Alignment.Top, modifier = Modifier.padding(6.dp)
) {
Column(modifier = Modifier.weight(1f)) {
Text(title, fontSize = 12.sp)
Text(
value, fontSize = 14.sp, color = Color.LegacyRed, fontWeight = FontWeight.Bold
)
Text(subtitle, fontSize = 12.sp)
}
IconCircle(icon, iconBg)
}
}
@Composable
private fun IconCircle(
icon: ImageVector, color: androidx.compose.ui.graphics.Color
) {
Box(
modifier = Modifier
.size(38.dp)
.clip(CircleShape)
.background(color.copy(alpha = 0.1f)),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = color,
modifier = Modifier.size(22.dp)
)
}
}
@Composable
private fun MenuGrid(
onNavigateAmount: (String) -> Unit,
onNavigateSignOn: () -> Unit,
onNavigateSeeMore: () -> Unit,
onNavigateSettlement: () -> Unit
) {
Column(
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.padding(horizontal = 16.dp)
) {
Spacer(Modifier.height(8.dp))
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
MenuCard(title = "Sale", icon = {
Icon(
painterResource(R.drawable.ic_terminal),
contentDescription = "icon",
modifier = Modifier.size(40.dp),
tint = Color.LegacyRed
)
}, modifier = Modifier.weight(1f), onClick = { onNavigateAmount("Sale") })
MenuCard(title = "MMQR", icon = {
Image(
painter = painterResource(R.drawable.ic_mmqr_logo),
contentDescription = "mmqr image",
modifier = Modifier.height(48.dp)
)
}, modifier = Modifier.weight(1f))
MenuCard("History", icon = {
Icon(
painterResource(R.drawable.ic_history),
contentDescription = "icon",
modifier = Modifier.size(32.dp),
tint = Color.LegacyRed
)
}, modifier = Modifier.weight(1f))
}
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
MenuCard(
title = "Sign On", icon = {
Icon(
painterResource(R.drawable.ic_sign_on),
contentDescription = "icon",
modifier = Modifier.size(32.dp),
tint = Color.LegacyRed
)
}, modifier = Modifier.weight(1f), onClick = onNavigateSignOn
)
MenuCard(
title = "Settlement", icon = {
Icon(
painterResource(R.drawable.ic_settlement),
contentDescription = "icon",
modifier = Modifier.size(32.dp),
tint = Color.LegacyRed
)
}, modifier = Modifier.weight(1f), onClick = onNavigateSettlement
)
MenuCard(
title = "See More", icon = {
Icon(
painterResource(R.drawable.ic_see_more),
contentDescription = "icon",
modifier = Modifier.size(32.dp),
tint = Color.LegacyRed
)
}, modifier = Modifier.weight(1f), onClick = onNavigateSeeMore
)
}
}
}
@Composable
private fun MenuCard(
title: String,
icon: @Composable () -> Unit,
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null
) {
Card(
modifier = modifier
.height(100.dp)
.then(
if (onClick != null) {
Modifier.clickable(onClick = onClick)
} else {
Modifier
}
),
shape = RoundedCornerShape(14.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(5.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(6.dp)
) {
Column {
icon()
Spacer(modifier = Modifier.weight(1f))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = title,
fontSize = 12.sp,
color = Color.Black,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
modifier = Modifier.weight(1f)
)
Icon(
imageVector = Icons.Default.ChevronRight,
contentDescription = null,
tint = Color.LegacyRed,
modifier = Modifier.size(28.dp)
)
}
}
}
}
}
@P2Preview
@Composable
fun PreviewDashboardScreen2() {
DashboardScreen2()
}
@P3Preview
@Composable
fun PreviewDashboardScreen3() {
DashboardScreen2()
}

View File

@ -0,0 +1,141 @@
package com.mob.utsmyanmar.ui.dashboard
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.PaddingValues
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.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.AccountBalanceWallet
import androidx.compose.material.icons.filled.CreditCard
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.LockOpen
import androidx.compose.material.icons.filled.Replay
import androidx.compose.material.icons.filled.SwapHoriz
import androidx.compose.material.icons.filled.Undo
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.preview.P2Preview
import com.mob.utsmyanmar.ui.theme.Color
@Composable
fun SeeMoreScreen(
onBack: () -> Unit = {},
onNavigateAmount: (String) -> Unit = {}
) {
val items = listOf(
SeeMoreItem("Void", Icons.Default.Undo),
SeeMoreItem("Refund", Icons.Default.Replay),
SeeMoreItem("Pre-Auth", Icons.Default.Lock),
SeeMoreItem("Pre-Auth Void", Icons.Default.LockOpen),
SeeMoreItem("Pre-Auth Complete", Icons.Default.CreditCard),
SeeMoreItem("Pre-Auth Complete Void", Icons.Default.SwapHoriz),
SeeMoreItem("Cash Out", Icons.Default.AccountBalanceWallet)
)
Scaffold(
containerColor = Color.IvoryBeige,
topBar = {
AppBar(
title = "See More",
icon = Icons.AutoMirrored.Filled.ArrowBack,
onIconClick = onBack
)
}
) { paddingValues ->
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(items) { item ->
MoreMenuCard(
title = item.title,
icon = item.icon,
onClick = { onNavigateAmount(item.title) }
)
}
}
}
}
@Composable
private fun MoreMenuCard(
title: String,
icon: ImageVector,
onClick: () -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.height(132.dp)
.clickable(onClick = onClick),
shape = RoundedCornerShape(18.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(14.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
Box(
modifier = Modifier
.background(Color.CrimsonRed, RoundedCornerShape(14.dp))
.padding(12.dp)
) {
Icon(
imageVector = icon,
contentDescription = title,
tint = Color.White
)
}
Text(
text = title,
color = Color.Black,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
textAlign = TextAlign.Start
)
}
}
}
private data class SeeMoreItem(
val title: String,
val icon: ImageVector
)
@P2Preview
@Composable
private fun PreviewSeeMoreScreen() {
SeeMoreScreen()
}

View File

@ -0,0 +1,13 @@
package com.mob.utsmyanmar.ui.device_info
data class DeviceInfoUiState(
val hardwareVersion: String = "",
val firmwareVersion: String = "",
val serialNumber: String = "",
val deviceModel: String = "",
val finalVersion: String = "",
val payHardwareVersion: String = "",
val romVersion: String = "",
val isLoading: Boolean = false,
val errorMessage: String? = null
)

View File

@ -0,0 +1,79 @@
package com.mob.utsmyanmar.ui.device_info
import androidx.lifecycle.ViewModel
import com.sunmi.pay.hardware.aidl.AidlConstants
import com.utsmyanmar.baselib.BaseApplication
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import sunmi.sunmiui.utils.LogUtil
class DeviceInfoViewModel : ViewModel() {
var TAG = DeviceInfoViewModel::class.simpleName;
private val _uiState = MutableStateFlow(DeviceInfoUiState())
val uiState = _uiState.asStateFlow()
fun loadDeviceInfo() {
_uiState.value = _uiState.value.copy(isLoading = true)
try {
val hardwareVersion = getParams(AidlConstants.SysParam.HARDWARE_VERSION)
val firmwareVersion = getParams(AidlConstants.SysParam.FIRMWARE_VERSION)
val serialNo = getParams(AidlConstants.SysParam.SN)
val deviceModel = getParams(AidlConstants.SysParam.DEVICE_MODEL)
val finalVersion = SystemParamsOperation.getInstance().finalVersion ?: ""
val payHardwareVersion = getPayHardwareVersion()
val romVersion = getRomVersion()
_uiState.value = DeviceInfoUiState(
hardwareVersion = hardwareVersion,
firmwareVersion = firmwareVersion,
serialNumber = serialNo,
deviceModel = deviceModel,
finalVersion = finalVersion,
payHardwareVersion = payHardwareVersion,
romVersion = romVersion,
isLoading = false
)
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isLoading = false,
errorMessage = e.message ?: "Failed to load device info"
)
}
}
private fun getParams(name: String): String {
return try {
BaseApplication.getInstance()
.basicOptBinder
?.getSysParam(name)
?: ""
} catch (e: Exception) {
""
}
}
private fun getPayHardwareVersion():String {
return try {
BaseApplication.getInstance().applicationContext.packageManager.getPackageInfo(
"com.sunmi.pay.hardware_v3",
0
).versionName ?: "PHV?"
} catch (e: Exception) {
e.printStackTrace()
"PHV?"
}
}
private fun getRomVersion():String {
return try {
android.os.Build.VERSION.RELEASE ?: "UNKNOWN"
}catch (e: Exception){
LogUtil.d(TAG, "get rom version error " + e)
"UNKNOWN"
}
}
}

View File

@ -0,0 +1,235 @@
package com.mob.utsmyanmar.ui.functions
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
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.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.OnDeviceTraining
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.preview.P2Preview
import com.mob.utsmyanmar.ui.preview.P3Preview
import com.mob.utsmyanmar.ui.theme.Color
import com.mob.utsmyanmar.R
@Composable
fun FunctionsScreen(
onBack: () -> Unit = {}
) {
Scaffold(
containerColor = Color.IvoryBeige,
topBar = {
AppBar(
title = "Settings",
icon = Icons.AutoMirrored.Filled.ArrowBack,
onIconClick = onBack
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp).verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(text = "General Settings")
FunctionButton(
onClick = {},
title = "App Version",
subTitle = "1.0-uat",
leadingIcon = {
Icon(
modifier = Modifier.size(24.dp),
painter = painterResource(R.drawable.ic_device_info),
contentDescription = "icon",
tint = Color.LegacyRed
)
}
)
FunctionButton(
onClick = {},
title = "Host Config",
subTitle = "Detail for bound hosts",
leadingIcon = {
Icon(
modifier = Modifier.size(24.dp),
painter = painterResource(R.drawable.ic_database_config),
contentDescription = "icon",
tint = Color.LegacyRed
)
},
trailingIcon = {
Icon(
imageVector = Icons.Default.ChevronRight,
contentDescription = "icon",
tint = Color.LegacyRed
)
}
)
Text(text = "System Configuration")
FunctionButton(
onClick = {},
title = "Clear Batch",
subTitle = "Detail for bound hosts",
leadingIcon = {
Icon(
modifier = Modifier.size(24.dp),
painter = painterResource(R.drawable.ic_refresh),
contentDescription = "icon",
tint = Color.LegacyRed
)
},
trailingIcon = {
Icon(
imageVector = Icons.Default.ChevronRight,
contentDescription = "icon",
tint = Color.LegacyRed
)
}
)
FunctionButton(
onClick = {},
title = "Clear Reversal",
subTitle = "Detail for bound hosts",
leadingIcon = {
Icon(
modifier = Modifier.size(24.dp),
painter = painterResource(R.drawable.ic_clear),
contentDescription = "icon",
tint = Color.LegacyRed
)
},
trailingIcon = {
Icon(
imageVector = Icons.Default.ChevronRight,
contentDescription = "icon",
tint = Color.LegacyRed
)
}
)
FunctionButton(
onClick = {},
title = "TMS Server Url",
subTitle = "Detail for bound hosts",
leadingIcon = {
Icon(
modifier = Modifier.size(24.dp),
painter = painterResource(R.drawable.ic_address_global),
contentDescription = "icon",
tint = Color.LegacyRed
)
},
)
FunctionButton(
onClick = {},
title = "Download Config",
subTitle = "Download terminal config from host",
leadingIcon = {
Icon(
modifier = Modifier.size(24.dp),
painter = painterResource(R.drawable.ic_circle_download_arrow),
contentDescription = "icon",
tint = Color.LegacyRed
)
},
trailingIcon = {
Icon(
imageVector = Icons.Default.ChevronRight,
contentDescription = "icon",
tint = Color.LegacyRed
)
}
)
}
}
}
@Composable
fun FunctionButton(
onClick: () -> Unit,
title: String,
subTitle: String,
leadingIcon: (@Composable () -> Unit)? = null,
trailingIcon: (@Composable () -> Unit)? = null,
) {
ElevatedButton(
onClick = onClick,
shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.White,
contentColor = Color.Black
),
modifier = Modifier.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) {
leadingIcon?.invoke()
Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier
.weight(1f)
.padding(horizontal = 8.dp)) {
Text(text = title)
Text(
text = subTitle,
style = MaterialTheme.typography.bodySmall,
color = Color.Gray
)
}
trailingIcon?.invoke()
}
}
}
@P3Preview
@P2Preview
@Composable
fun PreviewFunctionsScreen() {
FunctionsScreen()
}
@Preview
@Composable
fun PreviewFunctionButton() {
FunctionButton(
onClick = {},
title = "title",
subTitle = "sub-title"
)
}

View File

@ -0,0 +1,89 @@
package com.mob.utsmyanmar.ui.input_amount
import androidx.compose.runtime.Composable
import com.mob.utsmyanmar.model.ProcessCode
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
@Composable
fun AmountRoute(
action: String,
sharedViewModel: SharedViewModel,
onBack: () -> Unit,
onNavigateCardWaiting: () -> Unit
) {
InputAmount(
title = action.ifBlank { "Amount" },
onBackClick = onBack,
onChargeClick = { amount ->
sharedViewModel.amount.value = amount
sharedViewModel.setAmountExist(true)
sharedViewModel.setCardDataExist(false)
sharedViewModel.setTransMenu(null)
val config = action.toTransactionConfig()
sharedViewModel.transactionName.value = config.transactionName
sharedViewModel.transactionsType.value = config.transactionType
sharedViewModel.processCode.value = config.processCode
onNavigateCardWaiting()
}
)
}
private data class TransactionConfig(
val transactionName: String,
val transactionType: TransactionsType,
val processCode: String
)
private fun String.toTransactionConfig(): TransactionConfig {
return when (trim().lowercase()) {
"void" -> TransactionConfig(
transactionName = "SALE",
transactionType = TransactionsType.VOID,
processCode = ProcessCode.SALE_VOID + ProcessCode.SMART + ProcessCode.TO_ACCOUNT
)
"refund" -> TransactionConfig(
transactionName = "REFUND",
transactionType = TransactionsType.REFUND,
processCode = ProcessCode.REFUND + ProcessCode.SMART + ProcessCode.TO_ACCOUNT
)
"pre-auth" -> TransactionConfig(
transactionName = "PRE_AUTH",
transactionType = TransactionsType.PRE_AUTH_SALE,
processCode = ProcessCode.PRE_AUTH_SALE + ProcessCode.SMART + ProcessCode.TO_ACCOUNT
)
"pre-auth void" -> TransactionConfig(
transactionName = "PRE_AUTH",
transactionType = TransactionsType.PRE_AUTH_VOID,
processCode = ProcessCode.PRE_AUTH_VOID + ProcessCode.SMART + ProcessCode.TO_ACCOUNT
)
"pre-auth complete" -> TransactionConfig(
transactionName = "PRE_AUTH_COMPLETE",
transactionType = TransactionsType.PRE_AUTH_COMPLETE,
processCode = ProcessCode.PRE_AUTH_COMPLETE + ProcessCode.SMART + ProcessCode.TO_ACCOUNT
)
"pre-auth complete void" -> TransactionConfig(
transactionName = "PRE_AUTH_COMPLETE",
transactionType = TransactionsType.PRE_AUTH_COMPLETE_VOID,
processCode = ProcessCode.PRE_AUTH_COMPLETE_VOID + ProcessCode.SMART + ProcessCode.TO_ACCOUNT
)
"cash out" -> TransactionConfig(
transactionName = "CASH_OUT",
transactionType = TransactionsType.CASH_OUT,
processCode = ProcessCode.CASH_ADVANCE + ProcessCode.SMART + ProcessCode.TO_ACCOUNT
)
else -> TransactionConfig(
transactionName = "SALE",
transactionType = TransactionsType.SALE,
processCode = ProcessCode.SALE_PURCHASE + ProcessCode.SMART + ProcessCode.TO_ACCOUNT
)
}
}

View File

@ -0,0 +1,317 @@
package com.mob.utsmyanmar.ui.input_amount
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.fillMaxHeight
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.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.Backspace
import androidx.compose.material.icons.rounded.Backspace
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.draw.shadow
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.preview.P2Preview
import com.mob.utsmyanmar.ui.preview.P3Preview
import com.mob.utsmyanmar.ui.theme.Color
import kotlin.collections.List
@Composable
fun InputAmount(
title: String = "Amount",
onBackClick: () -> Unit = {},
onChargeClick: (String) -> Unit = {}
){
var amount by remember { mutableStateOf("") }
val prefixLabel = "MMK"
val supportingText = "Enter the amount to continue"
Scaffold(
topBar = { AppBar(title = "Amount") },
containerColor = Color.IvoryBeige
) {paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.padding(16.dp)
) {
Spacer(Modifier.height(8.dp))
//first container
Box(
modifier = Modifier
.fillMaxSize()
.weight(2f),
){
Card(
modifier = Modifier.align(Alignment.CenterEnd)
.clickable(enabled = true) {
amount = amount.dropLast(1)
},
shape = RoundedCornerShape(18.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
) {
Icon(
imageVector = Icons.Rounded.Backspace,
contentDescription = "Delete",
tint = Color.LegacyRed,
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
)
}
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Enter Amount",
color = Color.Gray,
fontSize = 18.sp,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.align(Alignment.CenterHorizontally),
verticalAlignment = Alignment.Bottom
) {
if (prefixLabel.isNotBlank()) {
Text(
text = prefixLabel,
color = Color.LegacyRed,
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(end = 10.dp, bottom = 6.dp)
)
}
Text(
text = formatAmount(amount.ifEmpty { "0" }),
color = Color.LegacyRed,
fontSize = 32.sp,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.height(14.dp))
Text(
text = supportingText,
color = Color.Gray,
fontSize = 14.sp,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
}
}
//second container
Column(
modifier = Modifier
.fillMaxWidth()
.weight(3f),
verticalArrangement = Arrangement.Bottom
){
NumericKeypad(
modifier = Modifier.fillMaxSize(),
onKeyClick = { value ->
amount = appendAmountValue(amount, value)
}
)
}
Spacer(Modifier.height(16.dp))
//third container
Box(
modifier = Modifier
.fillMaxWidth()
.weight(0.5f)
){
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Button(
onClick = {},
modifier = Modifier
.weight(1f)
.height(56.dp),
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.White,
contentColor = Color.LegacyRed
)
) {
Text("Cancel")
}
Button(
onClick = {
if (amount.isNotEmpty()) {
onChargeClick(amount)
} },
modifier = Modifier
.weight(1f)
.height(56.dp),
enabled = amount.isNotEmpty(),
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.LegacyRed,
contentColor = Color.White,
disabledContainerColor = Color.LegacyRed.copy(alpha = 0.5f),
disabledContentColor = Color.White
)
) {
Text(
text = "Next",
fontSize = 14.sp,
fontWeight = FontWeight.Medium
)
}
}
}
}
}
}
@Composable
private fun NumericKeypad(
modifier: Modifier = Modifier,
onKeyClick: (String) -> Unit
) {
val keys : List<List<String>> = listOf(
listOf("1", "2", "3"),
listOf("4", "5", "6"),
listOf("7", "8", "9"),
listOf(".", "0", "00")
)
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
keys.forEach { row ->
Row(
modifier = Modifier.fillMaxWidth().weight(1f),
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
row.forEach { key ->
KeypadButton(
text = key,
modifier = Modifier.weight(1f).fillMaxHeight(),
onClick = { onKeyClick(key) }
)
}
}
}
}
}
@Composable
private fun KeypadButton(
text: String,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
val enabled = text.isNotBlank()
Box(
modifier = modifier
.shadow(
elevation = 2.dp,
shape = RoundedCornerShape(8.dp),
clip = false
)
.background(
color = Color.White,
shape = RoundedCornerShape(8.dp)
)
.clickable(enabled = enabled) { onClick() },
contentAlignment = Alignment.Center
) {
Text(
text = text,
color = if (enabled) Color.LegacyRed else Color.White,
fontSize = 24.sp,
fontWeight = FontWeight.Normal,
textAlign = TextAlign.Center
)
}
}
private fun appendAmountValue(current: String, value: String): String {
if (value == ".") {
if (current.contains(".")) return current
return if (current.isEmpty()) "0." else "$current."
}
val decimalIndex = current.indexOf('.')
return if (decimalIndex >= 0) {
val decimalPart = current.substring(decimalIndex + 1)
val remainingDecimalDigits = 2 - decimalPart.length
if (remainingDecimalDigits <= 0) {
current
} else {
current + value.take(remainingDecimalDigits)
}
} else {
val wholeDigitsCount = current.filter(Char::isDigit).length
val remainingWholeDigits = 9 - wholeDigitsCount
if (remainingWholeDigits <= 0) {
current
} else {
current + value.take(remainingWholeDigits)
}
}
}
private fun formatAmount(value: String): String {
val normalized = value.ifEmpty { "0" }
val wholePart = normalized.substringBefore(".").ifEmpty { "0" }
val groupedWholePart = "%,d".format(wholePart.toLongOrNull() ?: 0L)
if (!normalized.contains(".")) {
return groupedWholePart
}
val decimalPart = normalized.substringAfter(".", "")
return if (normalized.endsWith(".")) {
"$groupedWholePart."
} else {
"$groupedWholePart.$decimalPart"
}
}
@P2Preview
@P3Preview
@Composable
fun PreviewInputAmount(){
InputAmount();
}

View File

@ -1,34 +1,285 @@
package com.mob.utsmyanmar.ui.navigation
import android.annotation.SuppressLint
import androidx.activity.ComponentActivity
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.mob.utsmyanmar.ui.amount.AmountScreen
import com.mob.utsmyanmar.ui.dashboard.DashboardScreen
import com.mob.utsmyanmar.model.ProcessCode
import com.mob.utsmyanmar.ui.input_amount.AmountRoute
import com.mob.utsmyanmar.ui.cardwaiting.CardWaitingScreen
import com.mob.utsmyanmar.ui.cardwaiting.CardWaitingViewModel
import com.mob.utsmyanmar.ui.dashboard.DashboardScreen2
import com.mob.utsmyanmar.ui.dashboard.SeeMoreScreen
import com.mob.utsmyanmar.ui.device_info.DeviceInfoViewModel
import com.mob.utsmyanmar.ui.pinpad.PinPadRoute
import com.mob.utsmyanmar.ui.processing_card.ProcessingCardRoute
import com.mob.utsmyanmar.ui.processing_card.ProcessingCardViewModel
import com.mob.utsmyanmar.ui.print_receipt.PrintReceiptScreen
import com.mob.utsmyanmar.ui.refund_rrn.InputRrnRoute
import com.mob.utsmyanmar.ui.sign_on.SignOnResultScreen
import com.mob.utsmyanmar.ui.sign_on.SignOnRoute
import com.mob.utsmyanmar.ui.sending_to_host.ProcessingRoute
import com.mob.utsmyanmar.ui.settlement.SettlementScreen
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultRoute
import com.mob.utsmyanmar.ui.sale_void.TranDetailPage
import com.mob.utsmyanmar.ui.sale_void.VoidViewModel
import com.mob.utsmyanmar.ui.sale_void.VoidTraceScreen
import com.mob.utsmyanmar.viewmodel.CardReaderViewModel
import com.mob.utsmyanmar.viewmodel.EmvTransactionProcessViewModel
import com.mob.utsmyanmar.ui.pinpad.PinPadViewModel
import com.mob.utsmyanmar.ui.settlement.SettlementViewModel
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultEvent
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultViewModel
import com.mob.utsmyanmar.ui.functions.FunctionsScreen
import com.mob.utsmyanmar.ui.refund_rrn.RRNViewModel
import com.mob.utsmyanmar.ui.tms_setup.TmsSetupRoute
import com.mob.utsmyanmar.ui.tms_setup.TmsSetupViewModel
import com.mob.utsmyanmar.ui.version.VersionScreen
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
import com.utsmyanmar.ecr.data.TransType
import com.utsmyanmar.paylibs.model.TradeData
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
@SuppressLint("ContextCastToActivity")
@Composable
fun AppNavGraph(
navController: NavHostController
) {
val activity = LocalContext.current as ComponentActivity
NavHost(
navController = navController,
startDestination = Routes.Dashboard.route
startDestination = Routes.TmsSetup.route
) {
composable(Routes.TmsSetup.route) {
val tmsSetupViewModel: TmsSetupViewModel = hiltViewModel()
TmsSetupRoute(
viewModel = tmsSetupViewModel,
onNavigateDashboard = {
navController.navigate(Routes.Dashboard.route) {
popUpTo(Routes.TmsSetup.route) {
inclusive = true
}
launchSingleTop = true
}
}
)
}
composable(Routes.Dashboard.route) {
DashboardScreen(
settlementEnabled = true,
wavePayEnabled = true,
onAmountClick = { action ->
navController.navigate(Routes.Amount.createRoute(action))
val sharedViewModel: SharedViewModel = hiltViewModel(activity);
DashboardScreen2(
onNavigateAmount = { action ->
if(action == "Sale"){
sharedViewModel.transactionsType.value = TransactionsType.SALE;
sharedViewModel.processCode.value = ProcessCode.SALE_PURCHASE + ProcessCode.SMART + ProcessCode.TO_ACCOUNT;
}
navController.navigate(Routes.Amount.createRoute(action)) {
launchSingleTop = true
}
},
onTransactionClick = {},
onSettlementClick = {},
onHistoryClick = {},
onCardClick = {},
onWavePayClick = {}
onNavigateSignOn = {
navController.navigate(Routes.SignOn.route) {
launchSingleTop = true
}
},
onNavigateSeeMore = {
navController.navigate(Routes.SeeMore.route) {
launchSingleTop = true
}
},
onNavigateSettlement = {
navController.navigate(Routes.Settlement.route) {
launchSingleTop = true
}
},
onNavigateVersion = {
navController.navigate(Routes.Version.route)
},
onNavigateFunctions = {
navController.navigate(Routes.Functions.route) {
launchSingleTop = true
}
}
)
}
composable(Routes.SeeMore.route) {
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
SeeMoreScreen(
onBack = { navController.popBackStack() },
onNavigateAmount = { action ->
when (action) {
"Void" -> {
sharedViewModel.transactionsType.value = TransactionsType.VOID
navController.navigate(Routes.VoidTrace.route) {
launchSingleTop = true
}
}
"Pre-Auth Complete Void" -> {
sharedViewModel.transactionsType.value = TransactionsType.PRE_AUTH_COMPLETE_VOID
navController.navigate(Routes.VoidTrace.route) {
launchSingleTop = true
}
}
else -> {
navController.navigate(Routes.Amount.createRoute(action)) {
launchSingleTop = true
}
}
}
}
)
}
composable(Routes.Version.route){
val deviceInfoViewModel: DeviceInfoViewModel = hiltViewModel();
VersionScreen(
onBack = {navController.popBackStack()},
deviceInfoViewModel = deviceInfoViewModel
)
}
composable(Routes.Functions.route) {
FunctionsScreen(
onBack = { navController.popBackStack() }
)
}
composable(Routes.VoidTrace.route) {
val voidViewModel: VoidViewModel = hiltViewModel()
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
VoidTraceScreen(
sharedViewModel = sharedViewModel,
voidViewModel = voidViewModel,
onNavigateTranDetail = { trace ->
navController.navigate(Routes.VoidTranDetail.createRoute(trace)) {
launchSingleTop = true
}
},
onBack = { navController.popBackStack() }
)
}
composable(Routes.Settlement.route) {
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
val settlementViewMode: SettlementViewModel = hiltViewModel(activity)
SettlementScreen(
sharedViewModel = sharedViewModel,
settlementViewMode = settlementViewMode,
onBack = { navController.popBackStack(Routes.Dashboard.route,false) },
onStartSettlement = {
sharedViewModel.transactionsType.value = TransactionsType.SETTLEMENT
navController.navigate(Routes.Processing.route) {
popUpTo(Routes.Settlement.route) {
inclusive = true
}
launchSingleTop = true
}
}
)
}
composable(
route = Routes.VoidTranDetail.route,
arguments = listOf(
navArgument("trace") {
type = NavType.StringType
}
)
) { backStackEntry ->
val voidViewModel: VoidViewModel = hiltViewModel()
val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity)
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
val trace = backStackEntry.arguments?.getString("trace").orEmpty()
TranDetailPage(
voidViewModel = voidViewModel,
trace = trace,
onBack = { navController.popBackStack() },
onProceedVoid = { payDetail ->
if(sharedViewModel.transactionsType.value == TransactionsType.VOID) {
transProcessViewModel.setTransType(TransactionsType.VOID)
payDetail.transType = TransactionsType.VOID.name
payDetail.transactionType = TransactionsType.VOID.value
} else {
transProcessViewModel.setTransType(TransactionsType.PRE_AUTH_COMPLETE_VOID)
payDetail.transType = TransactionsType.PRE_AUTH_COMPLETE_VOID.name
payDetail.transactionType = TransactionsType.PRE_AUTH_COMPLETE_VOID.value
}
val tradeData = TradeData()
tradeData.payDetail = payDetail
transProcessViewModel.setTradeData(tradeData)
sharedViewModel.payDetail.value = payDetail
navController.navigate(Routes.Processing.route) {
popUpTo(Routes.VoidTranDetail.route) {
inclusive = true
}
launchSingleTop = true
}
}
)
}
composable(Routes.SignOn.route) {
SignOnRoute(
onBack = { navController.popBackStack() },
onNavigateResult = { isSuccess, message ->
navController.navigate(Routes.SignOnResult.createRoute(isSuccess, message)) {
popUpTo(Routes.SignOn.route) {
inclusive = true
}
launchSingleTop = true
}
}
)
}
composable(
route = Routes.SignOnResult.route,
arguments = listOf(
navArgument("isSuccess") {
type = NavType.BoolType
},
navArgument("message") {
type = NavType.StringType
}
)
) { backStackEntry ->
val isSuccess = backStackEntry.arguments?.getBoolean("isSuccess") ?: false
val message = backStackEntry.arguments?.getString("message").orEmpty()
SignOnResultScreen(
isSuccess = isSuccess,
message = message,
onBack = { navController.popBackStack() },
onDone = {
navController.navigate(Routes.Dashboard.route) {
popUpTo(Routes.Dashboard.route) {
inclusive = false
}
launchSingleTop = true
}
},
onRetry = {
navController.navigate(Routes.SignOn.route) {
popUpTo(Routes.SignOnResult.route) {
inclusive = true
}
launchSingleTop = true
}
}
)
}
@ -40,10 +291,232 @@ fun AppNavGraph(
}
)
) { backStackEntry ->
AmountScreen(
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
AmountRoute(
action = backStackEntry.arguments?.getString("action").orEmpty(),
onBackClick = { navController.popBackStack() }
sharedViewModel = sharedViewModel,
onBack = { navController.popBackStack() },
onNavigateCardWaiting = {
navController.navigate(Routes.CardWaiting.route) {
popUpTo(Routes.Amount.route) {
inclusive = true
}
launchSingleTop = true
}
}
)
}
composable(Routes.CardWaiting.route) {
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
val cardReaderViewModel: CardReaderViewModel = hiltViewModel(activity)
val cardWaitingViewModel: CardWaitingViewModel = viewModel(
factory = CardWaitingViewModel.provideFactory(
cardReadViewModel = cardReaderViewModel,
sharedViewModel = sharedViewModel
)
)
CardWaitingScreen(
viewModel = cardWaitingViewModel,
amount = formatAmountForDisplay(sharedViewModel.amount.value),
onManualEntry = {},
onProcessingCard = {
navController.navigate(Routes.ProcessingCard.route) {
popUpTo(Routes.CardWaiting.route) {
inclusive = true
}
launchSingleTop = true
}
},
onBack = { navController.popBackStack() },
onMain = {
navController.navigate(Routes.Dashboard.route) {
popUpTo(Routes.Dashboard.route) {
inclusive = false
}
launchSingleTop = true
}
}
)
}
composable(Routes.ProcessingCard.route) {
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
val cardReaderViewModel: CardReaderViewModel = hiltViewModel(activity)
val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity)
val pinPadViewModel: PinPadViewModel = hiltViewModel(activity)
val emvTransactionViewModel: EmvTransactionProcessViewModel = hiltViewModel(activity)
val processingCardViewModel: ProcessingCardViewModel = viewModel(
factory = ProcessingCardViewModel.provideFactory(
cardReadViewModel = cardReaderViewModel,
sharedViewModel = sharedViewModel,
transProcessViewModel = transProcessViewModel,
pinPadViewModel = pinPadViewModel,
emvTransactionViewModel = emvTransactionViewModel
)
)
ProcessingCardRoute(
viewModel = processingCardViewModel,
onNavigatePinPad = {
pinPadViewModel.resetSessionState()
navController.navigate(Routes.PinPad.route) {
popUpTo(Routes.ProcessingCard.route) {
inclusive = true
}
launchSingleTop = true
}
},
onNavigateInputAmount = { navController.popBackStack(Routes.Amount.route, false) },
onNavigateProcessing = {},
onNavigateEmvTransaction = {},
onNavigateError = {},
onBack = { navController.popBackStack() },
onShowDecline = {}
)
}
composable(Routes.PinPad.route) {
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
val pinPadViewModel: PinPadViewModel = hiltViewModel(activity)
val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity)
PinPadRoute(
pinPadViewModel = pinPadViewModel,
sharedViewModel = sharedViewModel,
transProcessViewModel = transProcessViewModel,
onNavigateInputRrn = {
navController.navigate(Routes.InputRrn.route) {
popUpTo(Routes.PinPad.route) {
inclusive = true
}
launchSingleTop = true
}
},
onNavigateProcessing = {
navController.navigate(Routes.Processing.route) {
popUpTo(Routes.PinPad.route) {
inclusive = true
}
launchSingleTop = true
}
},
onBack = { navController.popBackStack() },
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)
val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity)
InputRrnRoute(
transProcessViewModel = transProcessViewModel,
rrnViewModel = rrnViewModel,
sharedViewModel = sharedViewModel,
onBack = { navController.popBackStack() },
onNavigateProcessing = {
navController.navigate(Routes.Processing.route) {
popUpTo(Routes.InputRrn.route) {
inclusive = true
}
launchSingleTop = true
}
}
)
}
composable(Routes.Processing.route) {
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity)
val settlementViewModel: SettlementViewModel = hiltViewModel(activity)
ProcessingRoute(
sharedViewModel = sharedViewModel,
transProcessViewModel = transProcessViewModel,
settlementViewModel = settlementViewModel,
onNavigateTransactionResult = {
navController.navigate(Routes.TransactionResult.route) {
popUpTo(Routes.Processing.route) {
inclusive = true
}
launchSingleTop = true
}
},
onNavigateMain = {
navController.navigate(Routes.Dashboard.route) {
popUpTo(Routes.Dashboard.route) {
inclusive = false
}
launchSingleTop = true
}
}
)
}
composable(Routes.TransactionResult.route) {
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
val transResultViewModel: TransactionResultViewModel = hiltViewModel(activity)
TransactionResultRoute(
viewModel = transResultViewModel,
sharedViewModel = sharedViewModel,
onNavigateMain = {
navController.navigate(Routes.Dashboard.route) {
popUpTo(Routes.Dashboard.route) {
inclusive = false
}
launchSingleTop = true
}
},
onNavigatePrintReceipt = {
navController.navigate(Routes.PrintReceipt.route) {
popUpTo(Routes.TransactionResult.route) {
inclusive = false
}
launchSingleTop = true
}
},
onShowError = {},
onShowSuccess = {},
onShowPrinterDialog = {}
)
}
composable(Routes.PrintReceipt.route) {
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
val transResultViewModel: TransactionResultViewModel = hiltViewModel(activity)
PrintReceiptScreen(
sharedViewModel = sharedViewModel,
transactionResultViewModel = transResultViewModel,
onPrint = {
transResultViewModel.onEvent(
TransactionResultEvent.RetryPrint,
sharedViewModel
)
},
onDone = {
navController.navigate(Routes.Dashboard.route) {
popUpTo(Routes.Dashboard.route) {
inclusive = false
}
launchSingleTop = true
}
}
)
}
}
}
private fun formatAmountForDisplay(amount: String?): String {
val normalizedAmount = amount.orEmpty()
val value = normalizedAmount.toLongOrNull() ?: return normalizedAmount.ifBlank { "0" }
return "%,d".format(value)
}

View File

@ -1,8 +1,32 @@
package com.mob.utsmyanmar.ui.navigation
import android.net.Uri
sealed class Routes(val route: String) {
data object TmsSetup : Routes("tms_setup")
data object Dashboard : Routes("dashboard")
data object Amount : Routes("amount/{action}") {
fun createRoute(action: String): String = "amount/$action"
fun createRoute(action: String): String = "amount/${Uri.encode(action)}"
}
data object SeeMore : Routes("see_more")
data object Settlement : Routes("settlement")
data object VoidTrace : Routes("void_trace")
data object VoidTranDetail : Routes("void_tran_detail/{trace}") {
fun createRoute(trace: String): String = "void_tran_detail/${Uri.encode(trace)}"
}
data object SignOn : Routes("sign_on")
data object SignOnResult : Routes("sign_on_result/{isSuccess}/{message}") {
fun createRoute(isSuccess: Boolean, message: String): String {
return "sign_on_result/$isSuccess/${Uri.encode(message)}"
}
}
data object CardWaiting : Routes("card_waiting")
data object ProcessingCard : Routes("processing_card")
data object PinPad : Routes("pin_pad")
data object InputRrn : Routes("input_rrn")
data object Processing : Routes("processing")
data object TransactionResult : Routes("transaction_result")
data object PrintReceipt : Routes("print_receipt")
data object Version : Routes("version")
data object Functions : Routes("functions")
}

View File

@ -0,0 +1,93 @@
package com.mob.utsmyanmar.ui.pinpad
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.lifecycle.compose.collectAsStateWithLifecycle
import com.mob.utsmyanmar.model.PinPadStatus
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
import com.utsmyanmar.paylibs.Constant
import com.utsmyanmar.paylibs.model.TradeData
import com.utsmyanmar.paylibs.utils.POSUtil
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
@Composable
fun PinPadRoute(
pinPadViewModel: PinPadViewModel,
sharedViewModel: SharedViewModel,
transProcessViewModel: TransProcessViewModel,
onNavigateInputRrn: () -> Unit,
onNavigateProcessing: () -> Unit,
onBack: () -> Unit,
onNavigateMain: () -> Unit,
) {
val pinText by pinPadViewModel.pinText.collectAsStateWithLifecycle()
val alertMsg by pinPadViewModel.alertMsg.collectAsStateWithLifecycle()
val pinStatus by pinPadViewModel.pinStatus.collectAsStateWithLifecycle()
val canGoBack = sharedViewModel.transactionsType.value != TransactionsType.SALE
BackHandler(enabled = canGoBack) {
onBack()
}
LaunchedEffect(Unit) {
val tradeData: TradeData = pinPadViewModel.getTradeData()!!
val payDetail = tradeData.getPayDetail()
val processCode = sharedViewModel.processCode.getValue()
val amount = sharedViewModel.amount.getValue()
payDetail.setAmount(POSUtil.getInstance().convertAmount(amount))
if (!TextUtils.equals(processCode, "")) {
payDetail.setProcessCode(processCode)
}
}
LaunchedEffect(pinStatus) {
when (pinStatus) {
PinPadStatus.ON_CONFIRM,
PinPadStatus.ON_NEXT_SCREEN -> {
val payDetail = pinPadViewModel.getPayDetail()
payDetail?.tradeAnswerCode = Constant.ANSWER_CODE_APPROVED
sharedViewModel.payDetail.value = payDetail
transProcessViewModel.resetTransactionStatus()
if (sharedViewModel.transactionsType.value == TransactionsType.REFUND || sharedViewModel.transactionsType.value == TransactionsType.PRE_AUTH_VOID || sharedViewModel.transactionsType.value == TransactionsType.PRE_AUTH_COMPLETE) {
onNavigateInputRrn()
} else {
onNavigateProcessing()
}
}
PinPadStatus.ON_CANCEL,
PinPadStatus.ON_TIMEOUT,
PinPadStatus.ON_CARD_REMOVED -> {
onBack()
}
PinPadStatus.ON_ERROR -> {
onNavigateMain()
}
else -> {}
}
}
DisposableEffect(Unit) {
onDispose {
pinPadViewModel.cancelPinPad()
}
}
PinPadScreen(
pinText = pinText,
alertMessage = alertMsg,
canGoBack = canGoBack,
onBack = onBack,
onKeyboardReady = { keyboard ->
pinPadViewModel.startPinPadProcess(keyboard)
}
)
}

View File

@ -0,0 +1,181 @@
package com.mob.utsmyanmar.ui.pinpad
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import com.mob.utsmyanmar.R
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 PinPadScreen(
pinText: String,
alertMessage: String?,
canGoBack: Boolean,
onBack: () -> 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 = "PIN ENTRY",
color = White,
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold
)
},
navigationIcon = {
if (canGoBack) {
IconButton(onClick = onBack) {
Icon(
painter = painterResource(R.drawable.ic_left_arrow),
contentDescription = "Back",
tint = White
)
}
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Primary
)
)
},
containerColor = White
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.background(White)
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(
color = Primary,
shape = RoundedCornerShape(24.dp)
)
.padding(horizontal = 24.dp, vertical = 28.dp),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "Please Input PIN",
color = White,
fontSize = 22.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(14.dp))
Text(
text = if (pinText.isBlank()) "------" else pinText,
color = White,
fontSize = 30.sp,
fontWeight = FontWeight.Bold,
letterSpacing = 4.sp
)
if (!alertMessage.isNullOrBlank()) {
Spacer(modifier = Modifier.height(12.dp))
Text(
text = alertMessage,
color = White,
fontSize = 15.sp,
textAlign = TextAlign.Center
)
}
}
}
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Enter your PIN on the secured keypad below.",
color = Black,
fontSize = 16.sp,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(24.dp))
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
AndroidView(
factory = { context ->
CustomPinPadKeyboard(context).also {
keyboardView = it
}
}
)
}
Spacer(modifier = Modifier.weight(1f))
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = if (canGoBack) {
"Cancel on device or use back to exit."
} else {
"Cancel on device to exit."
},
color = Black,
fontSize = 14.sp,
textAlign = TextAlign.Center
)
}
}
}
}

View File

@ -0,0 +1,565 @@
package com.mob.utsmyanmar.ui.pinpad
import android.os.Handler
import android.os.Looper
import android.os.RemoteException
import android.util.Log
import android.view.View
import android.view.ViewTreeObserver
import android.widget.TextView
import androidx.lifecycle.ViewModel
import com.mob.utsmyanmar.config.Constants
import com.mob.utsmyanmar.config.SunmiPayManager
import com.mob.utsmyanmar.model.PinPadStatus
import com.sunmi.pay.hardware.aidl.AidlConstants
import com.sunmi.pay.hardware.aidlv2.AidlErrorCodeV2
import com.sunmi.pay.hardware.aidlv2.bean.PinPadConfigV2
import com.sunmi.pay.hardware.aidlv2.bean.PinPadDataV2
import com.sunmi.pay.hardware.aidlv2.pinpad.PinPadListenerV2
import com.sunmi.pay.hardware.aidlv2.pinpad.PinPadOptV2
import com.utsmyanmar.baselib.ui.CustomPinPadKeyboard
import com.utsmyanmar.checkxread.sdk.SunmiSDK
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.model.TradeData
import com.utsmyanmar.paylibs.utils.core_utils.ByteUtil
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import sunmi.sunmiui.utils.LogUtil
import java.nio.charset.StandardCharsets
import javax.inject.Inject
@HiltViewModel
class PinPadViewModel @Inject constructor(
private val sunmiPayManager: SunmiPayManager
) : ViewModel() {
companion object {
private const val TAG = "PinPadViewModel"
private const val PIN_CONFIRM_SUCCESS_STATUS = 0
private const val ON_CONFIRM_CLICK = 2
private const val ON_CANCEL_CLICK = 3
private const val ON_ERROR_PIN_PAD = 4
private const val ON_NUMBER_CLICK = 5
private const val ON_EMPTY_PIN_BLOCK = 6
private const val ON_TIMEOUT_PIN_PAD = 7
private const val ON_ERROR_DUKPT = 8
}
private var mPinPadOptV2: PinPadOptV2? = null
private var mPinType = 0
private var mWidth = 239
private var mHeight = 130
private var mInterval = 1
private var mKeyboardCoordinate = intArrayOf(0, 661)
private var mCancelWidth = 112
private var mCancelHeight = 112
private var mCancelCoordinate = intArrayOf(0, 48)
private var dukptIndex = 0
private var tmkIndex = 9
private val PIK_INDEX = 11
private var tradeData: TradeData? = null
private var payDetail: PayDetail? = null
private var pan: String = ""
/*
* UI States
*/
private val _pinText = MutableStateFlow("")
val pinText = _pinText.asStateFlow()
private val _alertMsg = MutableStateFlow<String?>(null)
val alertMsg = _alertMsg.asStateFlow()
private val _errorCode = MutableStateFlow<Int?>(null)
val errorCode = _errorCode.asStateFlow()
private val _transType = MutableStateFlow<TransactionsType?>(null)
val transType = _transType.asStateFlow()
private val _pinStatus =
MutableStateFlow<PinPadStatus?>(null)
val pinStatus = _pinStatus.asStateFlow()
private var isTerminalStateReached = false
/*
* Trade Data
*/
fun setTradeData(tradeData: TradeData) {
this.tradeData = tradeData
payDetail = tradeData.payDetail
pan = payDetail?.cardNo ?: ""
}
fun getTradeData(): TradeData? {
return tradeData
}
fun setPayDetail(payDetail: PayDetail) {
this.payDetail = payDetail
tradeData = TradeData().apply {
this.payDetail = payDetail
}
}
fun getPayDetail(): PayDetail? {
return payDetail
}
fun setTransType(type: TransactionsType?) {
_transType.value = type
}
fun resetSessionState() {
_pinText.value = ""
_alertMsg.value = null
_errorCode.value = null
_pinStatus.value = null
isTerminalStateReached = false
}
/*
* Pin Pad
*/
fun startPinPadProcess(
customPinPadKeyboard: CustomPinPadKeyboard
) {
resetSessionState()
// dukptIndex = SystemParamsOperation.getInstance().tmkIndex.toInt()
if(!sunmiPayManager.isReady()){
_alertMsg.value =
"Sunmi Pay SDK is not ready"
_pinStatus.value =
PinPadStatus.ON_ERROR
return
}
initData()
// testInjectPIK()
initPinPad(customPinPadKeyboard)
}
private fun initData() {
mPinPadOptV2 = sunmiPayManager.pinPadOptV2
if (mPinPadOptV2 == null) {
Log.d(TAG, "PinPad service are not ready!")
return;
}
try {
val result = mPinPadOptV2?.setAntiExhaustiveProtectionMode(3)
if ((result ?: -1) >= 0) {
LogUtil.d(
TAG,
"Pin anti exhaustive result:$result"
)
} else {
LogUtil.d(
TAG, "Pin Anti Exhaustive failed"
)
}
} catch (e: RemoteException) {
e.printStackTrace()
}
}
fun cancelPinPad() {
try {
if (!isTerminalStateReached) {
mPinPadOptV2?.cancelInputPin()
Log.d(TAG, "PinPad Canceled")
}
} catch (e: RemoteException) {
throw RuntimeException(e)
}
}
/*
* Init Pin Pad
*/
private fun initPinPad(
customPinPadKeyboard: CustomPinPadKeyboard
) {
LogUtil.e(TAG, "Init Pin Pad PAN:$pan")
var timeout = Constants.PIN_PAD_TIMEOUT
val pinPadOrder = !SystemParamsOperation.getInstance().isRandomPinPad
if (SunmiSDK.getInstance().checkCardExist() != 2) {
timeout = Constants.TIMEOUT
}
try {
val config = PinPadConfigV2().apply {
maxInput = 6
minInput = 0
pinPadType = 1 // custom keyboard
algorithmType = 0
pinType = mPinType
this.timeout = timeout * 1000
isOrderNumKey = pinPadOrder
keySystem = AidlConstants.Security.SEC_MKSK
pinKeyIndex = PIK_INDEX
pinblockFormat = AidlConstants.PinBlockFormat.SEC_PIN_BLK_ISO_FMT0
this.pan = getPanBytes(this@PinPadViewModel.pan)
}
val result = mPinPadOptV2?.initPinPad(config, mPinPadListener)
LogUtil.e(TAG, "pinpad result:$result")
if (result.isNullOrEmpty()) {
_alertMsg.value = "PinPad init failed"
_pinStatus.value = PinPadStatus.ON_ERROR
return
}
getKeyboardCoordinate(result, customPinPadKeyboard)
_pinText.value = ""
customPinPadKeyboard.keepScreenOn = true
customPinPadKeyboard.setKeyBoard(result)
customPinPadKeyboard.visibility = View.VISIBLE
} catch (e: Exception) {
e.printStackTrace()
_alertMsg.value = e.message
_pinStatus.value = PinPadStatus.ON_ERROR
}
}
private fun testInjectPIK() {
try {
val securityOptV2 = sunmiPayManager.securityOptV2 ?: return
val pik = ByteUtil.hexStr2Bytes(
"33DD20C9A0B5B861F2914D44BC2AF055"
)
val kcv = ByteUtil.hexStr2Bytes(
"28DBDB489D28BC92"
)
val code = securityOptV2.savePlaintextKey(
AidlConstants.Security.KEY_TYPE_PIK,
pik,
kcv,
AidlConstants.Security.KEY_ALG_TYPE_3DES,
PIK_INDEX
)
LogUtil.e(TAG, "saveTestPIK result:$code")
} catch (e: Exception) {
e.printStackTrace()
}
}
/*
* Keyboard
*/
private fun getKeyboardCoordinate(
keyBoardText: String,
customPinPadKeyboard: CustomPinPadKeyboard
) {
customPinPadKeyboard
.viewTreeObserver
.addOnGlobalLayoutListener(
object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
customPinPadKeyboard
.viewTreeObserver
.removeOnGlobalLayoutListener(this)
val textView: TextView =
customPinPadKeyboard.key_0
textView.getLocationOnScreen(
mKeyboardCoordinate
)
mWidth = textView.width
mHeight = textView.height
mInterval = 1
importPinPadData(keyBoardText)
}
}
)
}
private fun importPinPadData(text: String) {
val pinPadData = PinPadDataV2()
pinPadData.numX = mKeyboardCoordinate[0]
pinPadData.numY = mKeyboardCoordinate[1]
pinPadData.numW = mWidth
pinPadData.numH = mHeight
pinPadData.lineW = mInterval
pinPadData.cancelX = mCancelCoordinate[0]
pinPadData.cancelY = mCancelCoordinate[1]
pinPadData.cancelW = mCancelWidth
pinPadData.cancelH = mCancelHeight
pinPadData.lineW = 0
pinPadData.rows = 5
pinPadData.clos = 3
keyMap(text, pinPadData)
try {
mPinPadOptV2?.importPinPadData(pinPadData)
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun keyMap(
str: String,
data: PinPadDataV2
) {
data.keyMap = ByteArray(64)
var j = 0
for (i in 0 until 15) {
when (i) {
9, 12 -> {
data.keyMap[i] = 0x1B
}
13 -> {
data.keyMap[i] = 0x0C
}
11, 14 -> {
data.keyMap[i] = 0x0D
}
else -> {
data.keyMap[i] =
str[j].code.toByte()
j++
}
}
}
}
/*
* Password View
*/
private fun showPasswordView(len: Int) {
val sb = StringBuilder()
repeat(len) {
sb.append("*")
}
_pinText.value = sb.toString()
}
/*
* Handler
*/
private val handler =
Handler(Looper.getMainLooper()) { msg ->
when (msg.what) {
ON_NUMBER_CLICK -> {
showPasswordView(msg.arg1)
}
ON_CONFIRM_CLICK -> {
LogUtil.d(TAG, "ON CLICK CONFIRM")
if (
transType.value == TransactionsType.PRE_AUTH_COMPLETE ||
transType.value == TransactionsType.PRE_AUTH_VOID ||
transType.value == TransactionsType.REFUND
) {
isTerminalStateReached = true
_pinStatus.value = PinPadStatus.ON_NEXT_SCREEN
} else {
isTerminalStateReached = true
_pinStatus.value = PinPadStatus.ON_CONFIRM
}
}
ON_CANCEL_CLICK -> {
LogUtil.d(TAG, "ON CLICK CANCEL")
if (!isTerminalStateReached) {
_pinStatus.value = PinPadStatus.ON_CANCEL
}
}
ON_ERROR_PIN_PAD -> {
LogUtil.d(TAG, "ON ERROR CODE: ${msg.arg1}")
_errorCode.value = msg.arg1
isTerminalStateReached = true
_pinStatus.value = PinPadStatus.ON_ERROR
}
ON_EMPTY_PIN_BLOCK -> {
_pinStatus.value = PinPadStatus.ON_EMPTY
}
ON_TIMEOUT_PIN_PAD -> {
isTerminalStateReached = true
_pinStatus.value = PinPadStatus.ON_TIMEOUT
}
ON_ERROR_DUKPT -> {
_alertMsg.value = "Try Again!"
isTerminalStateReached = true
_pinStatus.value = PinPadStatus.ON_ERROR_DUKPT
}
}
true
}
/*
* Listener
*/
private val mPinPadListener =
object : PinPadListenerV2.Stub() {
override fun onPinLength(len: Int) {
handler.obtainMessage(
ON_NUMBER_CLICK,
len,
0
).sendToTarget()
}
override fun onConfirm(
status: Int,
pinBlock: ByteArray?
) {
LogUtil.e(
TAG,
"onConfirm status:$status and pinblock :${ByteUtil.bytes2HexStr(pinBlock)}"
)
if (status != PIN_CONFIRM_SUCCESS_STATUS) {
_alertMsg.value = "PinPad confirm failed: $status"
_pinStatus.value = PinPadStatus.ON_ERROR
return
}
// val isCardValid = SunmiSDK.getInstance().checkCardExist() == 2 ||
// payDetail?.cardType == AidlConstants.CardType.MAGNETIC.getValue() ||
// payDetail?.cardType == -9
//
// if (!isCardValid) {
// isTerminalStateReached = true
// _pinStatus.value = PinPadStatus.ON_CARD_REMOVED
// return
// }
if (pinBlock != null) {
payDetail?.pinCipher = ByteUtil.bytes2HexStr(pinBlock)
} else {
payDetail?.pinCipher = ""
}
handler.obtainMessage(
ON_CONFIRM_CLICK
).sendToTarget()
}
override fun onCancel() {
if (!isTerminalStateReached) {
handler.obtainMessage(
ON_CANCEL_CLICK
).sendToTarget()
}
}
override fun onError(code: Int) {
val msg =
AidlErrorCodeV2
.valueOf(code)
.msg
LogUtil.d(
TAG,
"error code:$code - message :$msg"
)
when (code) {
-60001 -> {
handler.obtainMessage(
ON_TIMEOUT_PIN_PAD
).sendToTarget()
}
-3025 -> {
handler.obtainMessage(
ON_ERROR_DUKPT
).sendToTarget()
}
else -> {
handler.obtainMessage(
ON_ERROR_PIN_PAD,
code,
code,
code
).sendToTarget()
}
}
}
}
private fun getPanBytes(pan: String): ByteArray {
if (pan.length < 13) {
return ByteArray(0)
}
return pan
.substring(pan.length - 13, pan.length - 1)
.toByteArray(StandardCharsets.US_ASCII)
}
}

View File

@ -0,0 +1,20 @@
package com.mob.utsmyanmar.ui.preview
import androidx.compose.ui.tooling.preview.Preview
@Preview(
name = "P2",
device = "spec:width=720px,height=1440px,dpi=350",
showBackground = true,
showSystemUi = true
)
annotation class P2Preview
@Preview(
name = "P3",
device = "spec:width=720px,height=1600px,dpi=270",
showBackground = true,
showSystemUi = true
)
annotation class P3Preview

View File

@ -0,0 +1,281 @@
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.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
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.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.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
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 kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
fun PrintReceiptScreen(
transactionResultViewModel: TransactionResultViewModel,
sharedViewModel: SharedViewModel,
onPrint: () -> Unit,
onDone: () -> Unit
) {
val state by transactionResultViewModel.state.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope();
val receiptOffsetY = remember {
Animatable(0f);
}
val animationDuration = 3000;
Scaffold(
topBar = {
AppBar(title = "Receipt")
},
containerColor = Color.IvoryBeige
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
ReceiptPreview(
state = state,
modifier = Modifier
.weight(1f)
.verticalScroll(rememberScrollState())
.graphicsLayer{
translationY = receiptOffsetY.value
}
)
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
OutlinedButton(
onClick = onDone,
modifier = Modifier
.weight(1f)
.height(50.dp),
shape = RoundedCornerShape(12.dp)
) {
Text(
text = "Cancel",
style = MaterialTheme.typography.bodyLarge.copy(
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold
)
)
}
Button(
modifier = Modifier
.weight(1f)
.height(50.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.LegacyRed,
contentColor = Color.White,
disabledContainerColor = Color.LegacyRed.copy(alpha = 0.5f),
disabledContentColor = Color.White.copy(alpha = 0.5f),
),
shape = RoundedCornerShape(12.dp),
onClick = {
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")
//
// }
}
) {
Text(
text = "Print",
style = MaterialTheme.typography.bodyLarge.copy(
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold
)
)
}
}
}
}
}
@Composable
fun ReceiptPreview(
state: TransactionResultState,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(Color.White)
.border(
width = 1.dp,
color = Color.Gray,
shape = RoundedCornerShape(12.dp)
)
.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "MOB POS",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Transaction Receipt",
fontSize = 14.sp,
fontFamily = FontFamily.Monospace
)
Spacer(modifier = Modifier.height(16.dp))
ReceiptDivider()
ReceiptRow("Merchant", state.payDetail?.merchantName.toString() )
ReceiptRow("Terminal ID", state.payDetail?.terminalNo.toString() )
ReceiptRow("Trace No", state.payDetail?.voucherNo.toString() )
ReceiptRow("Transaction", state.payDetail?.transType.toString())
ReceiptRow("Amount", POSUtil.getInstance().getDecimalAmountSeparatorFormat(state.payDetail?.amount?:0))
ReceiptRow("Card Type", state.payDetail?.accountType.toString())
ReceiptRow("Card No", state.payDetail?.cardNo.toString())
ReceiptRow("Status", POSUtil.getInstance().getResponse(state.payDetail?.tradeAnswerCode?: "-"))
ReceiptRow("Date", state.payDetail?.transDate.toString() )
ReceiptRow("Time", state.payDetail?.transTime.toString())
ReceiptRow("Ref No", state.payDetail?.referNo.toString())
ReceiptDivider()
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Thank You",
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
fontFamily = FontFamily.Monospace
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Please keep this receipt",
fontSize = 12.sp,
fontFamily = FontFamily.Monospace
)
}
}
@Composable
fun ReceiptRow(
label: String,
value: String
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 6.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = label,
fontSize = 13.sp,
fontFamily = FontFamily.Monospace,
color = Color.Gray
)
Text(
text = value,
fontSize = 13.sp,
fontWeight = FontWeight.SemiBold,
fontFamily = FontFamily.Monospace
)
}
}
@Composable
fun ReceiptDivider() {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 10.dp),
text = "--------------------------------",
fontFamily = FontFamily.Monospace,
color = Color.Gray
)
}
@Preview(showBackground = true, showSystemUi = true)
@Composable
fun PreviewPrintReceiptScreen() {
MaterialTheme {
PrintReceiptScreen(
sharedViewModel = hiltViewModel(),
transactionResultViewModel = hiltViewModel(),
onPrint = {},
onDone = {}
)
}
}

View File

@ -0,0 +1,5 @@
package com.mob.utsmyanmar.ui.processing_card
sealed interface ProcessingCardEvent {
data object StartProcess : ProcessingCardEvent
}

View File

@ -0,0 +1,43 @@
package com.mob.utsmyanmar.ui.processing_card
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@Composable
fun ProcessingCardRoute(
viewModel: ProcessingCardViewModel,
onNavigatePinPad: () -> Unit,
onNavigateInputAmount: () -> Unit,
onNavigateProcessing: () -> Unit,
onNavigateEmvTransaction: () -> Unit,
onNavigateError: (String) -> Unit,
onBack: () -> Unit,
onShowDecline: (String) -> Unit
) {
val state by viewModel.state.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
viewModel.onEvent(ProcessingCardEvent.StartProcess)
}
LaunchedEffect(Unit) {
viewModel.uiEvent.collect { event ->
when (event) {
ProcessingCardUiEvent.NavigateToPinPad -> onNavigatePinPad()
ProcessingCardUiEvent.NavigateToInputAmount -> onNavigateInputAmount()
ProcessingCardUiEvent.NavigateToProcessing -> onNavigateProcessing()
ProcessingCardUiEvent.NavigateToEmvTransaction -> onNavigateEmvTransaction()
is ProcessingCardUiEvent.NavigateToError -> onNavigateError(event.message)
is ProcessingCardUiEvent.ShowDeclineAndBack -> {
onShowDecline(event.message)
onBack()
}
ProcessingCardUiEvent.Back -> onBack()
}
}
}
ProcessingCardScreen(state = state)
}

View File

@ -0,0 +1,27 @@
package com.mob.utsmyanmar.ui.processing_card
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@Composable
fun ProcessingCardScreen(
state: ProcessingCardState
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
if (state.isLoading) {
CircularProgressIndicator()
}
Text(text = state.message)
}
}

View File

@ -0,0 +1,7 @@
package com.mob.utsmyanmar.ui.processing_card
data class ProcessingCardState(
val title: String = "Processing Card",
val message: String = "Please wait...",
val isLoading: Boolean = true
)

View File

@ -0,0 +1,11 @@
package com.mob.utsmyanmar.ui.processing_card
sealed interface ProcessingCardUiEvent {
data object NavigateToPinPad : ProcessingCardUiEvent
data object NavigateToInputAmount : ProcessingCardUiEvent
data object NavigateToProcessing : ProcessingCardUiEvent
data object NavigateToEmvTransaction : ProcessingCardUiEvent
data class NavigateToError(val message: String) : ProcessingCardUiEvent
data class ShowDeclineAndBack(val message: String) : ProcessingCardUiEvent
data object Back : ProcessingCardUiEvent
}

View File

@ -0,0 +1,285 @@
package com.mob.utsmyanmar.ui.processing_card
import android.text.TextUtils
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.mob.utsmyanmar.model.CardTransactionType
import com.mob.utsmyanmar.ui.pinpad.PinPadViewModel
import com.mob.utsmyanmar.utils.MockCardData
import com.mob.utsmyanmar.utils.MockData
import com.mob.utsmyanmar.utils.TransactionUtil
import com.mob.utsmyanmar.viewmodel.CardReaderViewModel
import com.mob.utsmyanmar.viewmodel.EmvTransactionProcessViewModel
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
import com.utsmyanmar.checkxread.model.CardDataX
import com.utsmyanmar.checkxread.readcard.MAGXReadCard
import com.utsmyanmar.checkxread.readcard.MPUXReadCard
import com.utsmyanmar.checkxread.readcard.ReadCardResultX
import com.utsmyanmar.checkxread.util.CardTypeX
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.model.TradeData
import com.utsmyanmar.paylibs.model.enums.TransCVM
import com.utsmyanmar.paylibs.utils.POSUtil
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import com.utsmyanmar.paylibs.utils.enums.TransMenu
import com.utsmyanmar.paylibs.utils.params.Params
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import sunmi.sunmiui.utils.LogUtil
class ProcessingCardViewModel(
private val cardReadViewModel: CardReaderViewModel,
private val sharedViewModel: SharedViewModel,
private val transProcessViewModel: TransProcessViewModel,
private val pinPadViewModel: PinPadViewModel,
private val emvTransactionViewModel: EmvTransactionProcessViewModel
) : ViewModel() {
companion object {
private val TAG = ProcessingCardViewModel::class.java.simpleName
fun provideFactory(
cardReadViewModel: CardReaderViewModel,
sharedViewModel: SharedViewModel,
transProcessViewModel: TransProcessViewModel,
pinPadViewModel: PinPadViewModel,
emvTransactionViewModel: EmvTransactionProcessViewModel
): ViewModelProvider.Factory {
return object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ProcessingCardViewModel(
cardReadViewModel = cardReadViewModel,
sharedViewModel = sharedViewModel,
transProcessViewModel = transProcessViewModel,
pinPadViewModel = pinPadViewModel,
emvTransactionViewModel = emvTransactionViewModel
) as T
}
}
}
}
private val _state = MutableStateFlow(ProcessingCardState())
val state = _state.asStateFlow()
private val _uiEvent = Channel<ProcessingCardUiEvent>()
val uiEvent = _uiEvent.receiveAsFlow()
fun onEvent(event: ProcessingCardEvent) {
when (event) {
ProcessingCardEvent.StartProcess -> {
resetEmvTransResult()
checkCardTransactionType()
}
}
}
private fun resetEmvTransResult() {
emvTransactionViewModel.resetTransactionStatus()
}
private fun checkCardTransactionType() {
when (cardReadViewModel.getCardTransactionType()) {
CardTransactionType.MOCK -> mockCardData()
CardTransactionType.MPU -> readMPUCard()
CardTransactionType.EMV -> handlePreEmvProcess()
CardTransactionType.FALLBACK -> readMAGStripe(
isFallback = true,
isNeedPin = true
)
CardTransactionType.MAG -> readMAGStripe(
isFallback = false,
isNeedPin = true
)
else -> {
Log.d(
"ProcessingCardViewModel",
"error on CardTransactionType : ${cardReadViewModel.getCardTransactionType()}"
)
}
}
}
private fun mockCardData() {
sharedViewModel.setEmvTrans(false)
transProcessViewModel.setTransType(sharedViewModel.transactionsType.value)
pinPadViewModel.setTransType(sharedViewModel.transactionsType.value)
val cardDataX = MockData.generateMPUCard();
val tradeData = TransactionUtil.initMPUTransaction(cardDataX, CardTypeX.IC)
transProcessViewModel.setTradeData(tradeData)
pinPadViewModel.setTradeData(tradeData)
sharedViewModel.setCardDataExist(true)
sendUiEvent(ProcessingCardUiEvent.NavigateToPinPad)
}
private fun readMPUCard() {
cardReadViewModel.startReadXProcess(
MPUXReadCard.getInstance(),
object : ReadCardResultX {
override fun onSuccess(cardDataX: CardDataX) {
transProcessViewModel.setTransType(sharedViewModel.transactionsType.value)
pinPadViewModel.setTransType(sharedViewModel.transactionsType.value)
val tradeData = TransactionUtil.initMPUTransaction(cardDataX, CardTypeX.IC)
transProcessViewModel.setTradeData(tradeData)
pinPadViewModel.setTradeData(tradeData)
when {
sharedViewModel._transMenu.value == TransMenu.PRE_AUTH_PARTIAL_VOID -> {
sendUiEvent(
ProcessingCardUiEvent.NavigateToError(
"Function not supported"
)
)
return
}
}
sharedViewModel.setCardDataExist(true)
if (sharedViewModel.getAmountExist().value == false) {
sendUiEvent(ProcessingCardUiEvent.NavigateToInputAmount)
} else {
sendUiEvent(ProcessingCardUiEvent.NavigateToPinPad)
}
}
override fun onError(code: Int, message: String) {
LogUtil.d(TAG, "Failure at $code message: $message")
sharedViewModel.setCardDataExist(false)
sendUiEvent(
ProcessingCardUiEvent.ShowDeclineAndBack(
"FAILURE :$message"
)
)
}
}
)
}
private fun readMAGStripe(
isFallback: Boolean,
isNeedPin: Boolean
) {
cardReadViewModel.startReadXProcess(
MAGXReadCard.getInstance(),
object : ReadCardResultX {
override fun onSuccess(cardDataX: CardDataX) {
sharedViewModel.setEmvTrans(false)
if (isNeedPin) {
transProcessViewModel.setTransType(sharedViewModel.transactionsType.value)
pinPadViewModel.setTransType(sharedViewModel.transactionsType.value)
val tradeData = TransactionUtil.initMagStripeTransaction(cardDataX, isFallback)
transProcessViewModel.setTradeData(tradeData)
pinPadViewModel.setTradeData(tradeData)
} else {
transProcessViewModel.setTransType(sharedViewModel.transactionsType.value)
val tradeData = TransactionUtil.initMagStripeTransaction(cardDataX, isFallback)
val processCode = sharedViewModel.processCode.value
val amount = sharedViewModel.amount.value
val payDetail: PayDetail = tradeData.payDetail
payDetail.amount = POSUtil.getInstance().convertAmount(amount)
if (!TextUtils.equals(processCode, "")) {
payDetail.processCode = processCode
}
payDetail.transCVM = TransCVM.SIGNATURE
transProcessViewModel.setTradeData(tradeData)
}
when {
sharedViewModel.getTransMenu().value == TransMenu.PRE_AUTH_PARTIAL_VOID -> {
sharedViewModel.setTransMenu(null)
sendUiEvent(
ProcessingCardUiEvent.NavigateToError(
"Function not supported"
)
)
return
}
}
sharedViewModel.setCardDataExist(true)
if (
sharedViewModel.getTransMenu().value != TransMenu.PRE_AUTH_FULL_VOID &&
sharedViewModel.getAmountExist().value == false
) {
sendUiEvent(ProcessingCardUiEvent.NavigateToInputAmount)
} else {
if (isNeedPin) {
sendUiEvent(ProcessingCardUiEvent.NavigateToPinPad)
} else {
sendUiEvent(ProcessingCardUiEvent.NavigateToProcessing)
}
}
}
override fun onError(code: Int, message: String) {
LogUtil.d(TAG, "Failure at $code message: $message")
sendUiEvent(
ProcessingCardUiEvent.ShowDeclineAndBack(
"FAILURE :$message"
)
)
}
}
)
}
private fun handlePreEmvProcess() {
emvTransactionViewModel.transType.value =
sharedViewModel.transactionsType.value
if (SystemParamsOperation.getInstance().isEmvEnabled) {
prepareEmvTransaction()
sendUiEvent(ProcessingCardUiEvent.NavigateToEmvTransaction)
} else {
sendUiEvent(
ProcessingCardUiEvent.NavigateToError(
"Please enable EMV"
)
)
}
}
private fun prepareEmvTransaction() {
sharedViewModel.setEmvTrans(true)
val cardType = cardReadViewModel.cardTypeData.value ?: return
val tradeData: TradeData = Params.newTrade(false)
val payDetail = tradeData.payDetail
payDetail.cardType = cardType
emvTransactionViewModel.setTradeData(tradeData)
}
private fun sendUiEvent(event: ProcessingCardUiEvent) {
viewModelScope.launch {
_uiEvent.send(event)
}
}
}

View File

@ -0,0 +1,175 @@
package com.mob.utsmyanmar.ui.refund_rrn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.mob.utsmyanmar.ui.components.NumericEntryScreen
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.model.TradeData
import com.utsmyanmar.paylibs.utils.POSUtil
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
import sunmi.sunmiui.utils.LogUtil
private const val RRN_MAX_LENGTH = 12
@Composable
fun InputRrnRoute(
transProcessViewModel: TransProcessViewModel,
rrnViewModel: RRNViewModel,
sharedViewModel: SharedViewModel,
onBack: () -> Unit,
onNavigateProcessing: () -> Unit
) {
var rrn by rememberSaveable {
mutableStateOf(sharedViewModel.rrNNo.value.orEmpty())
}
var errorMessage by rememberSaveable { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
rrn = ""
errorMessage = null
}
val payDetailState by rrnViewModel.payDetailState.collectAsStateWithLifecycle()
fun processPreAuthComp(payDetail: PayDetail) {
val newPay = transProcessViewModel.payDetail
newPay?.setReferNo(payDetail.referNo)
newPay?.setApprovalCode(payDetail.getApprovalCode())
newPay?.setTradeTime(payDetail.getTradeTime())
newPay?.setTradeDate(payDetail.getTradeDate())
val tradeData = TradeData()
tradeData.payDetail = newPay
transProcessViewModel.setOldTransPayDetail(payDetail)
transProcessViewModel.setTradeData(tradeData)
rrnViewModel.resetState()
onNavigateProcessing()
}
fun processPreAuthVoid(payDetail: PayDetail) {
val newPay = transProcessViewModel.payDetail
newPay?.setReferNo(payDetail.referNo)
val tradeData = TradeData()
tradeData.payDetail = newPay
transProcessViewModel.setOldTransPayDetail(payDetail)
transProcessViewModel.setTradeData(tradeData)
rrnViewModel.resetState()
onNavigateProcessing()
}
LaunchedEffect(payDetailState) {
LogUtil.d("RRN","paydetail state: $payDetailState")
when (val state = payDetailState) {
is RRNViewModel.PayDetailState.Success -> {
sharedViewModel.payDetail.value = state.data
sharedViewModel.rrNNo.value = state.data.referNo
if(sharedViewModel.transactionsType.value == TransactionsType.PRE_AUTH_COMPLETE) {
processPreAuthComp(state.data)
} else if(sharedViewModel.transactionsType.value == TransactionsType.PRE_AUTH_VOID) {
processPreAuthVoid(state.data)
}
}
is RRNViewModel.PayDetailState.Error -> {
errorMessage = state.message
rrnViewModel.resetState()
// show dialog
// navigate to home
}
else -> Unit
}
}
NumericEntryScreen(
title = "Input RRN",
prompt = "Enter RRN",
displayValue = rrn.ifEmpty { "0" },
supportingText = errorMessage ?: "Enter the retrieval reference number to continue",
confirmText = "Next",
onBackClick = onBack,
onCancelClick = onBack,
onConfirmClick = {
val trimmedRrn = rrn.trim()
if (POSUtil.getInstance().checkNumberField(trimmedRrn)) {
errorMessage = "Invalid RRN"
return@NumericEntryScreen
}
sharedViewModel.rrNNo.value = trimmedRrn
if(sharedViewModel.transactionsType.value == TransactionsType.REFUND ) {
val newPay = transProcessViewModel.payDetail
newPay?.setReferNo(trimmedRrn)
val tradeData = TradeData()
tradeData.payDetail = newPay
transProcessViewModel.setTradeData(tradeData)
rrnViewModel.resetState()
onNavigateProcessing()
} else{
rrnViewModel.searchPayDetail(
trimmedRrn,
sharedViewModel.payDetail.value?.cardNo?:""
)
}
},
onKeyClick = { value ->
rrn = appendRrnValue(rrn, value)
errorMessage = null
},
onDeleteClick = {
rrn = rrn.dropLast(1)
errorMessage = null
},
confirmEnabled = rrn.isNotEmpty(),
canDelete = rrn.isNotEmpty(),
keys = listOf(
listOf("1", "2", "3"),
listOf("4", "5", "6"),
listOf("7", "8", "9"),
listOf("", "0", "00")
)
)
}
private fun appendRrnValue(current: String, value: String): String {
if (value.isBlank()) {
return current
}
val remainingDigits = RRN_MAX_LENGTH - current.length
if (remainingDigits <= 0) {
return current
}
return current + value.take(remainingDigits)
}

View File

@ -0,0 +1,56 @@
package com.mob.utsmyanmar.ui.refund_rrn
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope
import com.utsmyanmar.baselib.repo.Repository
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.system.SingleLiveEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class RRNViewModel @Inject constructor(
private val repository: Repository
) : ViewModel() {
sealed class PayDetailState {
object Idle : PayDetailState()
object Loading : PayDetailState()
data class Success(val data: PayDetail) : PayDetailState()
data class Error(val message: String) : PayDetailState()
}
private val _payDetailState = MutableStateFlow<PayDetailState>(PayDetailState.Idle)
val payDetailState: StateFlow<PayDetailState> = _payDetailState
fun searchPayDetail(refNo: String, cardNo: String) {
_payDetailState.value = PayDetailState.Loading
viewModelScope.launch {
try {
repository.searchPayDetail(cardNo, refNo)
.asFlow()
.filterNotNull()
.first()
.let { result ->
_payDetailState.value = PayDetailState.Success(result)
}
} catch (e: Exception) {
_payDetailState.value = PayDetailState.Error(e.message ?: "Search failed")
}
}
}
fun resetState() {
_payDetailState.value = PayDetailState.Idle
}
}

View File

@ -0,0 +1,168 @@
package com.mob.utsmyanmar.ui.sale_void
import androidx.compose.foundation.background
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.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.theme.Color
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.utils.POSUtil
@Composable
fun TranDetailPage(
voidViewModel: VoidViewModel,
trace: String,
onBack: () -> Unit,
onProceedVoid: (PayDetail) -> Unit
) {
val transaction by voidViewModel.searchTransaction(trace).observeAsState()
Scaffold(
containerColor = Color.IvoryBeige,
topBar = {
AppBar(
title = "Transaction Detail",
icon = Icons.AutoMirrored.Filled.ArrowBack,
onIconClick = onBack
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
) {
transaction?.let { payDetail ->
TransactionDetailsCard(transaction = payDetail)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Review the transaction before proceeding to void.",
color = Color.Gray,
fontSize = 13.sp
)
Spacer(modifier = Modifier.weight(1f))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Button(
onClick = onBack,
modifier = Modifier
.weight(1f)
.height(56.dp),
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.White,
contentColor = Color.LegacyRed
)
) {
Text("Back")
}
Button(
onClick = { onProceedVoid(payDetail) },
modifier = Modifier
.weight(1f)
.height(56.dp),
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.LegacyRed,
contentColor = Color.White
)
) {
Text("Void")
}
}
} ?: Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "Transaction not found.",
color = Color.Gray,
fontSize = 14.sp
)
}
}
}
}
@Composable
private fun TransactionDetailsCard(transaction: PayDetail) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color.White)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text(
text = "Previous Sale",
color = Color.LegacyRed,
fontWeight = FontWeight.Bold,
fontSize = 16.sp
)
DetailRow("Trace", transaction.voucherNo)
DetailRow("Amount", POSUtil.getInstance().formatAmount(transaction.amount))
DetailRow("Card No", transaction.cardNo)
DetailRow("Reference", transaction.referNo)
DetailRow("Approval", transaction.approvalCode.orEmpty())
DetailRow("Date", transaction.tradeDate)
DetailRow("Time", transaction.tradeTime)
}
}
}
@Composable
private fun DetailRow(
label: String,
value: String
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = label,
color = Color.Gray,
fontSize = 13.sp
)
Text(
text = value.ifBlank { "-" },
color = Color.Black,
fontSize = 13.sp,
fontWeight = FontWeight.Medium
)
}
}

View File

@ -0,0 +1,322 @@
package com.mob.utsmyanmar.ui.sale_void
import android.util.Log
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.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.rounded.Backspace
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.livedata.observeAsState
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.draw.shadow
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.theme.Color
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
import sunmi.sunmiui.utils.LogUtil
@Composable
fun VoidTraceScreen(
sharedViewModel: SharedViewModel,
voidViewModel: VoidViewModel,
onNavigateTranDetail: (String) -> Unit,
onBack: () -> Unit = {}
) {
val tag = "VoidTraceScreen"
var traceNumber by remember { mutableStateOf("") }
var searchedTrace by remember { mutableStateOf("") }
var lastNavigatedTrace by remember { mutableStateOf<String?>(null) }
val displayTraceNumber = traceNumber.padStart(6, '0').ifEmpty { "000000" }
val recentTransactions by voidViewModel.getLastThreeTransactions().observeAsState(emptyList())
val transactionSource: LiveData<PayDetail?> = remember(searchedTrace) {
val transType = if(sharedViewModel.transactionsType.value == TransactionsType.VOID) {
TransactionsType.SALE.value
} else {
TransactionsType.PRE_AUTH_COMPLETE.value;
}
LogUtil.d("void screen","transaction type : $transType and trace number : $searchedTrace")
if (searchedTrace.isBlank()) {
MutableLiveData<PayDetail?>(null)
} else {
@Suppress("UNCHECKED_CAST")
voidViewModel.getVoidTrans(
transType,
searchedTrace
) as LiveData<PayDetail?>
}
}
val transaction by transactionSource.observeAsState()
val hasSearched = searchedTrace.isNotBlank()
val headerMessage = if (hasSearched && transaction == null) {
"Warning: no previous sale transaction found for trace $searchedTrace."
} else {
"Input trace number to find previous sale transaction."
}
val headerColor = if (hasSearched && transaction == null) {
Color.LegacyRed
} else {
Color.Black
}
LaunchedEffect(searchedTrace) {
if (searchedTrace != lastNavigatedTrace) {
lastNavigatedTrace = null
}
}
LaunchedEffect(transaction?.voucherNo) {
val matchedTrace = transaction?.voucherNo ?: return@LaunchedEffect
if (lastNavigatedTrace == matchedTrace) return@LaunchedEffect
lastNavigatedTrace = matchedTrace
onNavigateTranDetail(matchedTrace)
}
LaunchedEffect(searchedTrace, transaction) {
Log.d(
tag,
"Recent DB traces: ${
recentTransactions.joinToString { detail ->
"${detail.voucherNo}:${detail.transType}:${detail.tradeAnswerCode}"
}
}"
)
if (searchedTrace.isNotBlank()) {
Log.d(
tag,
"Search trace=$searchedTrace result=${transaction?.voucherNo ?: "NOT_FOUND"}"
)
}
}
Scaffold(
containerColor = Color.IvoryBeige,
topBar = {
AppBar(
title = "Void",
icon = Icons.AutoMirrored.Filled.ArrowBack,
onIconClick = onBack
)
}
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
Text(
text = headerMessage,
color = headerColor,
fontSize = 14.sp
)
Spacer(modifier = Modifier.height(28.dp))
Text(
text = "Trace Number",
color = Color.Gray,
fontSize = 11.sp,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = displayTraceNumber,
color = Color.LegacyRed,
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Enter 6-digit trace number",
color = Color.Gray,
fontSize = 11.sp,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(24.dp))
TraceNumberKeypad(
onNumberClick = { value ->
if (traceNumber.length < 6) {
traceNumber = (traceNumber + value).take(6)
}
}
)
Spacer(modifier = Modifier.weight(1f))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 18.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Button(
onClick = onBack,
modifier = Modifier
.weight(1f)
.height(56.dp),
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.White,
contentColor = Color.LegacyRed
)
) {
Text("Cancel")
}
Button(
onClick = {
searchedTrace = traceNumber.padStart(6, '0')
},
modifier = Modifier
.weight(1f)
.height(56.dp),
enabled = traceNumber.isNotBlank(),
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.LegacyRed,
contentColor = Color.White,
disabledContainerColor = Color.LegacyRed.copy(alpha = 0.5f),
disabledContentColor = Color.White
)
) {
Text("Enter")
}
}
}
Card(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 110.dp, end = 16.dp)
.clickable(enabled = traceNumber.isNotEmpty()) {
traceNumber = traceNumber.dropLast(1)
},
shape = RoundedCornerShape(18.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
) {
androidx.compose.material3.Icon(
imageVector = Icons.Rounded.Backspace,
contentDescription = "Delete",
tint = if (traceNumber.isNotEmpty()) Color.LegacyRed else Color.Gray,
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
)
}
}
}
}
@Composable
private fun TraceNumberKeypad(
onNumberClick: (String) -> Unit
) {
val keys = listOf(
listOf("1", "2", "3"),
listOf("4", "5", "6"),
listOf("7", "8", "9"),
listOf("", "0", "00")
)
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
keys.forEach { row ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
row.forEach { key ->
if (key.isBlank()) {
Spacer(
modifier = Modifier
.weight(1f)
.height(66.dp)
)
} else {
TraceKeypadButton(
text = key,
modifier = Modifier.weight(1f),
onClick = { onNumberClick(key) }
)
}
}
}
}
}
}
@Composable
private fun TraceKeypadButton(
text: String,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
Box(
modifier = modifier
.height(66.dp)
.shadow(
elevation = 2.dp,
shape = RoundedCornerShape(8.dp),
clip = false
)
.background(
color = Color.White,
shape = RoundedCornerShape(8.dp)
)
.clickable { onClick() },
contentAlignment = Alignment.Center
) {
Text(
text = text,
color = Color.LegacyRed,
fontSize = 24.sp,
fontWeight = FontWeight.Normal,
textAlign = TextAlign.Center
)
}
}

View File

@ -0,0 +1,53 @@
package com.mob.utsmyanmar.ui.sale_void
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel
import com.utsmyanmar.baselib.repo.Repository
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.system.SingleLiveEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class VoidViewModel @Inject constructor(
private val repository: Repository
) : ViewModel() {
val inputTrace = SingleLiveEvent<String>()
val lists = SingleLiveEvent<List<PayDetail>>()
fun getVoidTransactions(transType: Int): LiveData<List<PayDetail>> {
return repository.getVoidableTransactions(transType)
}
fun getGenericVoidTransactions(
transType: Int,
voucherNo: String,
isEmv: Boolean
): LiveData<PayDetail> {
return repository.getGenericVoidTransaction(transType, voucherNo, isEmv)
}
fun getVoidTrans(transType: Int, voucherNo: String): LiveData<PayDetail> {
return repository.getVoidTransaction(transType, voucherNo)
}
fun observeVoidTrans(transType: Int) {
getVoidTransactions(transType).observeForever(object : Observer<List<PayDetail>> {
override fun onChanged(payDetails: List<PayDetail>) {
if (lists.value == null) {
lists.postValue(payDetails)
}
}
})
}
fun searchTransaction(voucherNo: String): LiveData<PayDetail> {
return repository.searchTransaction(voucherNo)
}
fun getLastThreeTransactions(): LiveData<List<PayDetail>> {
return repository.getLastThreeTransactions()
}
}

View File

@ -0,0 +1,5 @@
package com.mob.utsmyanmar.ui.sending_to_host
data class ProcessingState(
val title: String = "Processing",
)

View File

@ -0,0 +1,90 @@
package com.mob.utsmyanmar.ui.sending_to_host
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.mob.utsmyanmar.model.TransResultStatus
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
import kotlinx.coroutines.delay
import androidx.compose.runtime.getValue
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultState
import com.mob.utsmyanmar.viewmodel.ProcessingTransaction
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import androidx.compose.runtime.collectAsState
import com.mob.utsmyanmar.model.TransactionStatus
import com.mob.utsmyanmar.ui.settlement.SettlementViewModel
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
import kotlinx.coroutines.channels.Channel
private const val MOCK_HOST_DELAY_MS = 2000L
@Composable
fun ProcessingRoute(
sharedViewModel: SharedViewModel,
transProcessViewModel: TransProcessViewModel,
settlementViewModel: SettlementViewModel,
onNavigateTransactionResult: () -> Unit,
onNavigateMain: ()-> Unit
) {
val transStatus by transProcessViewModel.transResultStatus.collectAsStateWithLifecycle()
val state by transProcessViewModel.state.collectAsStateWithLifecycle()
val settlementStatus by settlementViewModel.uiState.collectAsStateWithLifecycle()
if(sharedViewModel.transactionsType.value == TransactionsType.SETTLEMENT) {
LaunchedEffect(Unit) {
settlementViewModel.startSettlementProcess()
}
LaunchedEffect(settlementStatus.status) {
when(settlementStatus.status) {
TransactionStatus.ON_ERROR,
TransactionStatus.ON_SUCCESS -> {
sharedViewModel.payDetail.value = settlementViewModel.getPayDetail()
onNavigateTransactionResult()
}
else -> {}
}
}
} else {
LaunchedEffect(Unit) {
transProcessViewModel.resetTransactionStatus()
transProcessViewModel.startOnlineProcess()
}
LaunchedEffect(transStatus) {
when(transStatus) {
TransResultStatus.SUCCESS -> {
sharedViewModel.payDetail.value = transProcessViewModel.payDetailResult.value
onNavigateTransactionResult()
}
TransResultStatus.FAIL -> {
sharedViewModel.payDetail.value = transProcessViewModel.payDetailResult.value
onNavigateTransactionResult()
}
TransResultStatus.REVERSAL_SUCCESS -> onNavigateMain()
TransResultStatus.REVERSAL_FAIL -> onNavigateMain()
else -> {}
}
}
}
if(sharedViewModel.transactionsType.value == TransactionsType.SETTLEMENT) {
ProcessingScreen(settlementStatus.processingState)
} else {
ProcessingScreen(state)
}
}

View File

@ -0,0 +1,88 @@
package com.mob.utsmyanmar.ui.sending_to_host
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.CircularProgressIndicator
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.mob.utsmyanmar.ui.processing_card.ProcessingCardState
import com.mob.utsmyanmar.ui.theme.Black
import com.mob.utsmyanmar.ui.theme.Primary
import com.mob.utsmyanmar.ui.theme.White
@Composable
fun ProcessingScreen(state: ProcessingState) {
Scaffold(
containerColor = White
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.background(White)
.padding(horizontal = 24.dp, vertical = 32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(
color = Primary,
shape = RoundedCornerShape(24.dp)
)
.padding(horizontal = 24.dp, vertical = 32.dp),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
CircularProgressIndicator(color = White)
Spacer(modifier = Modifier.height(20.dp))
Text(
text = state.title,
color = White,
fontSize = 24.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(10.dp))
Text(
text = "Please wait while your transaction is being processed.",
color = White,
fontSize = 15.sp,
textAlign = TextAlign.Center
)
}
}
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Transaction processing in progress.",
color = Black,
fontSize = 14.sp,
textAlign = TextAlign.Center
)
}
}
}

View File

@ -0,0 +1,197 @@
package com.mob.utsmyanmar.ui.settlement
import androidx.compose.foundation.layout.Arrangement
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
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.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.theme.Color
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.utils.POSUtil
@Composable
fun SettlementScreen(
sharedViewModel: SharedViewModel,
settlementViewMode: SettlementViewModel,
onBack: () -> Unit,
onStartSettlement: () -> Unit
) {
val records by settlementViewMode.getSettlementPOS().observeAsState(emptyList())
val totalAmount = records.sumOf { it.amount }
Scaffold(
containerColor = Color.IvoryBeige,
topBar = {
AppBar(
title = "Settlement",
icon = Icons.AutoMirrored.Filled.ArrowBack,
onIconClick = onBack
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color.White)
) {
Column(modifier = Modifier.padding(18.dp)) {
Text(
text = "Settlement Summary",
color = Color.LegacyRed,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(12.dp))
SummaryRow("Record Count", records.size.toString())
SummaryRow("Total Amount", POSUtil.getInstance().formatAmount(totalAmount))
}
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Unsettled Records",
color = Color.Black,
fontSize = 15.sp,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(8.dp))
Card(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
colors = CardDefaults.cardColors(containerColor = Color.White)
) {
if (records.isEmpty()) {
Text(
text = "No records available for settlement.",
color = Color.Gray,
fontSize = 14.sp,
modifier = Modifier.padding(18.dp)
)
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(0.dp)
) {
items(records) { record ->
SettlementRecordRow(record = record)
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onStartSettlement,
enabled = records.isNotEmpty(),
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.LegacyRed,
contentColor = Color.White
)
) {
Text("Start Settlement")
}
}
}
}
@Composable
private fun SummaryRow(
label: String,
value: String
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = label,
color = Color.Gray,
fontSize = 13.sp
)
Text(
text = value,
color = Color.Black,
fontSize = 13.sp,
fontWeight = FontWeight.Medium
)
}
}
@Composable
private fun SettlementRecordRow(record: PayDetail) {
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = record.transType.ifBlank { "UNKNOWN" },
color = Color.LegacyRed,
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
Text(
text = POSUtil.getInstance().formatAmount(record.amount),
color = Color.Black,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold
)
}
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Trace ${record.voucherNo.ifBlank { "-" }}",
color = Color.Black,
fontSize = 12.sp
)
Text(
text = "Card ${record.cardNo.ifBlank { "-" }}",
color = Color.Gray,
fontSize = 12.sp
)
Spacer(modifier = Modifier.height(12.dp))
HorizontalDivider(color = Color.Gray.copy(alpha = 0.25f))
}
}

View File

@ -0,0 +1,504 @@
package com.mob.utsmyanmar.ui.settlement
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope
import com.mob.utsmyanmar.config.Constants
import com.mob.utsmyanmar.model.SettlementType
import com.mob.utsmyanmar.model.TransactionStatus
import com.mob.utsmyanmar.ui.sending_to_host.ProcessingState
import com.utsmyanmar.baselib.repo.Repository
import com.utsmyanmar.paylibs.Constant
import com.utsmyanmar.paylibs.batch_upload.BatchListener
import com.utsmyanmar.paylibs.batch_upload.BatchUploadProcess
import com.utsmyanmar.paylibs.isobuilder.ISOMode
import com.utsmyanmar.paylibs.isobuilder.builderx.ISOMsgX
import com.utsmyanmar.paylibs.isobuilder.builderx.ISOVersion
import com.utsmyanmar.paylibs.model.MsgField
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.model.SettleData
import com.utsmyanmar.paylibs.model.TradeData
import com.utsmyanmar.paylibs.network.ISOCallback
import com.utsmyanmar.paylibs.network.ISOSocket
import com.utsmyanmar.paylibs.utils.MessageType
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import com.utsmyanmar.paylibs.utils.enums.HostName
import com.utsmyanmar.paylibs.utils.iso_utils.BitmapConfig
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionType
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
import com.utsmyanmar.paylibs.utils.params.Params
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import sunmi.sunmiui.utils.LogUtil
import java.util.Locale
import javax.inject.Inject
data class SettlementUiState(
val saleCount: Int = 0,
val saleAmount: Long = 0L,
val preCount: Int = 0,
val preAmount: Long = 0L,
val refundCount: Int = 0,
val refundAmount: Long = 0L,
val caCount: Int = 0,
val caAmount: Long = 0L,
val settlementType: SettlementType? = null,
val isNoData: Boolean = false,
val isSentData: Boolean = false,
val bottomLayout: Int = 0,
val status: TransactionStatus? = null,
val isLoading: Boolean = false,
val processingState: ProcessingState = ProcessingState("Processing")
)
sealed interface SettlementEvent {
data class ShowStatus(val status: TransactionStatus) : SettlementEvent
}
@HiltViewModel
class SettlementViewModel @Inject constructor(
private val repository: Repository
) : ViewModel() {
companion object {
private val TAG = SettlementViewModel::class.java.simpleName
}
private var payDetail: PayDetail? = null
private var payDetails: List<PayDetail>? = null
private val deleteTrans = arrayListOf<PayDetail>()
private var flag = false
private var errorFlag = false
private var isSecondCall = false
private var batchIndex = 0
private var bitmap = ""
val records: LiveData<List<PayDetail>> = getSettlementPOS()
init {
viewModelScope.launch {
records.asFlow().collect { list ->
setPayDetails(list)
setSettlementData(list)
}
}
}
private val isoMsgX: ISOMsgX =
ISOMsgX.ISOMsgXBuilder(
ISOVersion.VERSION_1993,
ISOMode.ONLY_HEADER,
HostName.MPU
).build()
private val _uiState = MutableStateFlow(SettlementUiState())
val uiState = _uiState.asStateFlow()
private val _events = Channel<SettlementEvent>(Channel.BUFFERED)
val events = _events.receiveAsFlow()
fun getLastSettlement(voucherNo: String): LiveData<List<PayDetail>> {
return repository.getLastSettlement(voucherNo)
}
fun getSettlement(): LiveData<List<PayDetail>> {
return repository.getSettlement()
}
fun getSettlementPOS(): LiveData<List<PayDetail>> {
return repository.getSettlementPOS()
}
fun getDeleteTrans(batchNo: String): LiveData<List<PayDetail>> {
return repository.getDeleteTrans(batchNo)
}
fun getAdditionalSettlementPOS(): LiveData<List<PayDetail>> {
return repository.getAdditionalSettlementPOS(
SystemParamsOperation.getInstance().getCurrentBatchNum()
)
}
fun setPayDetails(list: List<PayDetail>) {
payDetails = list
}
fun setSettlementData(list: List<PayDetail>) {
val newState = list.fold(_uiState.value) { state, payDetail ->
val transType = payDetail.transactionType
val transName = payDetail.transType
when {
(transType == TransactionType.SALE || transName.equals("SALE", ignoreCase = true))
&& !payDetail.isCanceled ->
state.copy(
saleCount = state.saleCount + 1,
saleAmount = state.saleAmount + payDetail.amount
)
(transType == TransactionType.PRE_SALE_COMPLETE
|| transName.equals("PREAUTH COMPLETION", ignoreCase = true)
|| transName.equals("PRE_AUTH_COMPLETE", ignoreCase = true))
&& !payDetail.isCanceled ->
state.copy(
preCount = state.preCount + 1,
preAmount = state.preAmount + payDetail.amount
)
transType == TransactionType.REFUND
|| transName.equals("REFUND", ignoreCase = true) ->
state.copy(
refundCount = state.refundCount + 1,
refundAmount = state.refundAmount + payDetail.amount
)
transType == TransactionType.CASH_ADVANCE
|| transName.equals("CASH_OUT", ignoreCase = true)
|| transName.equals("CASH_ADVANCE", ignoreCase = true) ->
state.copy(
caCount = state.caCount + 1,
caAmount = state.caAmount + payDetail.amount
)
else -> state
}
}
LogUtil.d(TAG,"sale count ${newState.saleCount} and sale amount ${newState.saleAmount}")
_uiState.update { newState }
}
fun setSettlementSummary(
saleCount: Int,
saleAmount: Long,
preCount: Int,
preAmount: Long,
refundCount: Int,
refundAmount: Long,
caCount: Int,
caAmount: Long
) {
_uiState.update {
it.copy(
saleCount = saleCount,
saleAmount = saleAmount,
preCount = preCount,
preAmount = preAmount,
refundCount = refundCount,
refundAmount = refundAmount,
caCount = caCount,
caAmount = caAmount
)
}
}
fun getPayDetail() = payDetail
fun setSettlementType(type: SettlementType) {
_uiState.update {
it.copy(settlementType = type)
}
}
fun updatePayDetail(payDetail: PayDetail) {
repository.updatePayDetail(payDetail)
}
private fun insertPayDetail(payDetail: PayDetail) {
repository.insertPayDetail(payDetail)
}
private fun updateDB() {
payDetails?.forEach { pay ->
repository.deletePayDetail(pay)
}
deleteTrans.forEach { pay ->
repository.deletePayDetail(pay)
}
payDetails = emptyList()
deleteTrans.clear()
}
fun startSettlementProcess() {
_uiState.update {
it.copy(isLoading = true)
}
SystemParamsOperation.getInstance().getIncrementBatchNo()
requestOnlineProcessSettlement()
}
fun startPrintSettlementProcess() {
// PrintReceipt.getInstance().printSettlementReceiptPOS(...)
}
fun testServiceClass() {
LogUtil.d(TAG, "SettlementViewModel works!")
}
private fun requestOnlineProcessSettlement() {
val state = _uiState.value
val hostName = HostName.MPU
val field60 = SystemParamsOperation.getInstance().getCurrentBatchNum()
val sale2Count = state.saleCount + state.preCount
val sale2Amount = state.saleAmount + state.preAmount
LogUtil.d(TAG,"Sale count :$sale2Count and Sale amount :$sale2Amount")
val totalSaleCount = String.format(Locale.getDefault(), "%03d", sale2Count)
val totalSaleAmount = String.format(Locale.getDefault(), "%010d00", sale2Amount)
val totalRefundCount = String.format(Locale.getDefault(), "%03d", state.refundCount)
val totalRefundAmount = String.format(
Locale.getDefault(),
"%010d00",
if (state.refundAmount == 0L) 0 else state.refundAmount
)
val totalDebitSaleCount = String.format(Locale.getDefault(), "%03d", state.caCount)
val totalDebitSaleAmount = String.format(
Locale.getDefault(),
"%010d00",
if (state.caAmount == 0L) 0 else state.caAmount
)
val totalERefundCount = String.format(Locale.getDefault(), "%03d", 0)
val totalERefundAmount = String.format(Locale.getDefault(), "%010d00", 0)
val tradeData = Params.newTrade(true)
val currentPayDetail = tradeData.payDetail
payDetail = currentPayDetail
bitmap = BitmapConfig.MPU_NEW_SETTLE
currentPayDetail.transType = TransactionsType.SETTLEMENT.name
currentPayDetail.transactionType = TransactionType.SETTLEMENT
if (!flag) {
currentPayDetail.processCode = TransactionsType.SETTLEMENT.processCode
} else {
bitmap = BitmapConfig.MPU_NEW_SETTLE
currentPayDetail.processCode = "960000"
}
currentPayDetail.batchNo = SystemParamsOperation.getInstance().getCurrentBatchNum()
currentPayDetail.settleList =
"${state.saleCount}:${state.saleAmount}-" +
"${state.caCount}:${state.caAmount}-" +
"${state.refundCount}:${state.refundAmount}-" +
"${state.preCount}:${state.preAmount}"
val settleData = SettleData(
state.saleCount,
state.saleAmount,
state.preCount,
state.preAmount,
state.refundCount,
state.refundAmount,
state.caCount,
state.caAmount
)
currentPayDetail.settleDataObj = settleData
if (hostName == HostName.BPC) {
val totalAmount =
state.saleAmount + state.preAmount + state.refundAmount + state.caAmount
val settlementData = if (state.refundAmount != 0L) {
val creditTotal = state.saleAmount + state.preAmount + state.caAmount
val subTotal = creditTotal - state.refundAmount
if (subTotal < 0L) {
"D" + String.format(Locale.getDefault(), "%012d", kotlin.math.abs(subTotal))
} else {
"C" + String.format(Locale.getDefault(), "%012d", subTotal)
}
} else {
"C" + String.format(Locale.getDefault(), "%012d", totalAmount)
}
currentPayDetail.settleData = settlementData
currentPayDetail.amount = totalAmount
} else {
currentPayDetail.settleData =
totalSaleCount +
totalSaleAmount +
totalRefundCount +
totalRefundAmount +
totalDebitSaleCount +
totalDebitSaleAmount +
totalERefundCount +
totalERefundAmount
}
payDetail = currentPayDetail
tradeData.payDetail = currentPayDetail
tradeData.field60 = field60
val sendBytes = isoMsgX.buildISOPackets(
tradeData,
bitmap,
MessageType.SETTLEMENT
)
LogUtil.d(TAG, "Starting SETTLEMENT process...")
ISOSocket.getInstance().enqueue(
sendBytes,
sendBytes.size,
false,
object : ISOCallback {
override fun onReceive(bytes: ByteArray, length: Int) {
val responseMap: Map<String, MsgField>? =
isoMsgX.parseISOPackets(bytes, length)
if (responseMap != null) {
val resultStr = try {
responseMap["F039"]?.dataStr.orEmpty()
} catch (e: NullPointerException) {
e.printStackTrace()
currentPayDetail.isNeedReversal = true
return
}
currentPayDetail.tradeAnswerCode = resultStr
when {
resultStr == Constant.ANSWER_CODE_ACCEPT ||
resultStr == Constant.ANSWER_CODE_APPROVED -> {
currentPayDetail.isNeedReversal = false
}
resultStr == "95" || resultStr == "095" -> {
currentPayDetail.isNeedReversal = !flag
}
}
} else {
errorFlag = true
}
}
override fun onError(msg: String) {
if (msg != Constants.REVERSAL) {
if (!isSecondCall) {
ISOSocket.getInstance().switchIp()
postStatus(TransactionStatus.ON_SECONDARY)
isSecondCall = true
requestOnlineProcessSettlement()
} else {
postStatus(TransactionStatus.ON_ERROR)
currentPayDetail.isNeedReversal = true
isSecondCall = false
}
} else {
postStatus(TransactionStatus.ON_ERROR)
}
}
override fun onComplete() {
if (currentPayDetail.isNeedReversal) {
flag = true
batchUploadProcess()
} else {
flag = false
batchIndex = 0
updateDB()
insertPayDetail(currentPayDetail)
if (errorFlag) {
postStatus(TransactionStatus.ON_ERROR)
} else {
postStatus(TransactionStatus.ON_SUCCESS)
}
_uiState.update {
it.copy(isLoading = false)
}
}
}
}
)
}
private fun batchUploadProcess() {
_uiState.value = _uiState.value.copy(processingState = ProcessingState("Processing Batch Upload"))
postStatus(TransactionStatus.ON_BATCH_UPLOAD)
val currentPayDetails = payDetails
if (currentPayDetails.isNullOrEmpty()) {
requestOnlineProcessSettlement()
return
}
val uploadPayDetail = currentPayDetails[batchIndex]
val tradeData = TradeData().apply {
payDetail = uploadPayDetail
}
BatchUploadProcess.getInstance()
.enqueue(tradeData)
.startBatchUpload(object : BatchListener {
override fun onSuccessBatch() {
if (batchIndex < currentPayDetails.size - 1) {
LogUtil.d(TAG, "Pay detail size: ${currentPayDetails.size}")
LogUtil.d(TAG, "Count value: $batchIndex")
batchIndex++
batchUploadProcess()
} else {
requestOnlineProcessSettlement()
}
LogUtil.e(TAG, "Batch Upload Success")
}
override fun onFailBatch() {
LogUtil.e(TAG, "Batch Upload Fail")
postStatus(TransactionStatus.ON_ERROR)
_uiState.update {
it.copy(isLoading = false)
}
}
})
}
private fun postStatus(status: TransactionStatus) {
_uiState.update {
it.copy(status = status)
}
viewModelScope.launch {
_events.send(SettlementEvent.ShowStatus(status))
}
}
}

View File

@ -0,0 +1,262 @@
package com.mob.utsmyanmar.ui.sign_on
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.ErrorOutline
import androidx.compose.material.icons.filled.Sync
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
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.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.theme.Color as AppColor
@Composable
fun SignOnRoute(
onBack: () -> Unit,
onNavigateResult: (Boolean, String) -> Unit,
signOnViewModel: SignOnViewModel = viewModel()
) {
val state by signOnViewModel.uiState.collectAsState()
LaunchedEffect(signOnViewModel) {
signOnViewModel.resultEvents.collect { result ->
onNavigateResult(result.isSuccess, result.message)
}
}
SignOnScreen(
state = state,
onBack = onBack,
onStartSignOn = signOnViewModel::startSignOn
)
}
@Composable
fun SignOnScreen(
state: SignOnUiState,
onBack: () -> Unit,
onStartSignOn: () -> Unit
) {
Scaffold(
containerColor = AppColor.IvoryBeige,
topBar = {
AppBar(
title = "Sign On",
icon = Icons.Default.ArrowBack,
onIconClick = onBack
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = AppColor.White),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
imageVector = Icons.Default.Sync,
contentDescription = null,
tint = AppColor.LegacyRed,
modifier = Modifier.height(42.dp)
)
Text(
text = "Host Sign On",
fontSize = 22.sp,
fontWeight = FontWeight.Bold,
color = AppColor.LegacyRed
)
Text(
text = "Run sign on and verify immediately whether the host accepted or rejected the terminal.",
style = MaterialTheme.typography.bodyMedium,
color = AppColor.Gray
)
}
}
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = AppColor.White)
) {
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Current Status",
style = MaterialTheme.typography.titleMedium,
color = AppColor.Black
)
Box(
modifier = Modifier
.fillMaxWidth()
.background(
color = if (state.isLoading) AppColor.GoldenGlow.copy(alpha = 0.18f) else AppColor.SkylineBlue.copy(alpha = 0.12f),
shape = RoundedCornerShape(16.dp)
)
.padding(16.dp)
) {
if (state.isLoading) {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
CircularProgressIndicator(color = AppColor.LegacyRed)
Text(text = state.statusText, color = AppColor.Black)
}
} else {
Text(text = state.statusText, color = AppColor.Black)
}
}
Button(
onClick = onStartSignOn,
enabled = !state.isLoading,
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = AppColor.CrimsonRed,
disabledContainerColor = AppColor.Gray
)
) {
Text(text = if (state.isLoading) "Processing..." else "Start Sign On")
}
if (state.canRetry) {
OutlinedButton(
onClick = onStartSignOn,
enabled = !state.isLoading,
modifier = Modifier.fillMaxWidth()
) {
Text(text = "Retry")
}
}
}
}
}
}
}
@Composable
fun SignOnResultScreen(
isSuccess: Boolean,
message: String,
onBack: () -> Unit,
onDone: () -> Unit,
onRetry: () -> Unit
) {
val accent = if (isSuccess) AppColor.Success else AppColor.LegacyRed
val background = if (isSuccess) AppColor.Success.copy(alpha = 0.12f) else AppColor.CrimsonRed.copy(alpha = 0.12f)
Scaffold(
containerColor = AppColor.IvoryBeige,
topBar = {
AppBar(
title = "Sign On Result",
icon = Icons.Default.ArrowBack,
onIconClick = onBack
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(containerColor = AppColor.White)
) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Box(
modifier = Modifier
.background(background, RoundedCornerShape(50))
.padding(18.dp)
) {
Icon(
imageVector = if (isSuccess) Icons.Default.CheckCircle else Icons.Default.ErrorOutline,
contentDescription = null,
tint = accent
)
}
Text(
text = if (isSuccess) "Sign On Success" else "Sign On Failed",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = accent
)
Text(
text = message,
style = MaterialTheme.typography.bodyLarge,
color = Color.Black
)
}
}
Spacer(modifier = Modifier.weight(1f))
if (!isSuccess) {
Button(
onClick = onRetry,
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(containerColor = AppColor.CrimsonRed)
) {
Text(text = "Try Again")
}
}
OutlinedButton(
onClick = onDone,
modifier = Modifier.fillMaxWidth()
) {
Text(text = "Back To Dashboard")
}
}
}
}

View File

@ -0,0 +1,92 @@
package com.mob.utsmyanmar.ui.sign_on
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.utsmyanmar.paylibs.sign_on.SignOnListener
import com.utsmyanmar.paylibs.sign_on.SignOnProcess
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
data class SignOnUiState(
val isLoading: Boolean = false,
val statusText: String = "Ready to start sign on.",
val canRetry: Boolean = false
)
data class SignOnResult(
val isSuccess: Boolean,
val message: String
)
class SignOnViewModel : ViewModel() {
private val _uiState = MutableStateFlow(SignOnUiState())
val uiState: StateFlow<SignOnUiState> = _uiState.asStateFlow()
private val _resultEvents = MutableSharedFlow<SignOnResult>()
val resultEvents: SharedFlow<SignOnResult> = _resultEvents.asSharedFlow()
fun startSignOn() {
if (_uiState.value.isLoading) return
_uiState.update {
it.copy(
isLoading = true,
statusText = "Signing on to host...",
canRetry = false
)
}
SignOnProcess.getInstance()
.enqueue()
.startSignOn(object : SignOnListener {
override fun onSuccessSignOn() {
dispatchResult(
SignOnResult(
isSuccess = true,
message = "Sign on completed successfully."
)
)
}
override fun onFailureSignOn(resultCode: Int?) {
dispatchResult(
SignOnResult(
isSuccess = false,
message = "Sign on failed. Response code: ${resultCode ?: -1}"
)
)
}
override fun onNetworkFailSignOn(message: String?) {
dispatchResult(
SignOnResult(
isSuccess = false,
message = message?.takeIf { it.isNotBlank() } ?: "Network error during sign on."
)
)
}
})
}
private fun dispatchResult(result: SignOnResult) {
viewModelScope.launch {
_uiState.update {
it.copy(
isLoading = false,
statusText = result.message,
canRetry = !result.isSuccess
)
}
_resultEvents.emit(result)
}
}
}

View File

@ -5,13 +5,22 @@ import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
val Primary = Color(0xFFCA2027)
val White = Color(0xFFFFFFFF)
val Black = Color(0xFF000000)
val Black = Color(0xFF000000)
object Color {
val IvoryBeige = Color(0xFFFFF5E6)
val LegacyRed = Color(0xFF6F0D1E)
val CrimsonRed = Color(0xFFCD2029)
val GoldenGlow = Color(0xFFFFCF66)
val SkylineBlue = Color(0xFF47A2DA)
val White = Color(0xFFFFFFFF)
val Black = Color(0xFF000000)
val Gray = Color(0xFF898989)
val Success = Color(0xFF007E33)
val Error = Color(0xFFCD2029)
}

View File

@ -0,0 +1,173 @@
package com.mob.utsmyanmar.ui.tms_setup
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CloudDownload
import androidx.compose.material.icons.filled.ErrorOutline
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.mob.utsmyanmar.ui.theme.Color as AppColor
@Composable
fun TmsSetupRoute(
viewModel: TmsSetupViewModel,
onNavigateDashboard: () -> Unit
) {
val state by viewModel.uiState.collectAsState()
LaunchedEffect(viewModel) {
viewModel.navigateToDashboard.collect { onNavigateDashboard() }
}
TmsSetupScreen(
state = state,
onRetry = viewModel::downloadConfigs,
onSkip = viewModel::skipDownload
)
}
@Composable
fun TmsSetupScreen(
state: TmsSetupUiState,
onRetry: () -> Unit,
onSkip: () -> Unit
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(AppColor.IvoryBeige),
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
Icon(
imageVector = if (state.isError) Icons.Default.ErrorOutline else Icons.Default.CloudDownload,
contentDescription = null,
tint = if (state.isError) AppColor.Error else AppColor.LegacyRed,
modifier = Modifier.size(72.dp)
)
Text(
text = if (state.isError) "Configuration Error" else "Setting Up Terminal",
fontSize = 22.sp,
fontWeight = FontWeight.Bold,
color = AppColor.LegacyRed,
textAlign = TextAlign.Center
)
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = AppColor.White),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
if (state.isLoading) {
CircularProgressIndicator(
color = AppColor.CrimsonRed,
modifier = Modifier.size(40.dp)
)
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth(),
color = AppColor.CrimsonRed,
trackColor = AppColor.GoldenGlow.copy(alpha = 0.3f),
strokeCap = StrokeCap.Round
)
}
Text(
text = state.statusText,
fontSize = 14.sp,
color = AppColor.Black,
textAlign = TextAlign.Center
)
if (state.isError) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(
AppColor.Error.copy(alpha = 0.08f),
RoundedCornerShape(12.dp)
)
.padding(12.dp)
) {
Text(
text = state.errorMessage,
fontSize = 13.sp,
color = AppColor.Error,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
}
}
}
if (state.isError) {
Button(
onClick = onRetry,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors(containerColor = AppColor.CrimsonRed)
) {
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = null,
modifier = Modifier
.padding(end = 8.dp)
.size(18.dp)
)
Text(text = "Retry", fontSize = 16.sp)
}
OutlinedButton(
onClick = onSkip,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.outlinedButtonColors(contentColor = AppColor.LegacyRed)
) {
Text(text = "Skip", fontSize = 16.sp)
}
}
}
}
}

View File

@ -0,0 +1,169 @@
package com.mob.utsmyanmar.ui.tms_setup
import android.annotation.SuppressLint
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.mob.utsmyanmar.model.sirius.SiriusResponse
import com.mob.utsmyanmar.model.sirius.TMSUpdate
import com.mob.utsmyanmar.model.sirius.ValidityStatus
import com.mob.utsmyanmar.utils.tms.TMSSetupsImpl
import com.mob.utsmyanmar.utils.tms.TMSUtil
import com.utsmyanmar.baselib.BaseApplication
import com.utsmyanmar.baselib.emv.EmvParamOperation
import com.utsmyanmar.baselib.network.model.sirius.SiriusRequest
import com.utsmyanmar.baselib.repo.Repository
import dagger.hilt.android.lifecycle.HiltViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import sunmi.sunmiui.utils.LogUtil
import javax.inject.Inject
data class TmsSetupUiState(
val isLoading: Boolean = false,
val statusText: String = "Initializing...",
val isError: Boolean = false,
val errorMessage: String = ""
)
@HiltViewModel
class TmsSetupViewModel @Inject constructor(
private val repository: Repository,
private val emvParamOperation: EmvParamOperation
) : ViewModel() {
private val _uiState = MutableStateFlow(TmsSetupUiState())
val uiState: StateFlow<TmsSetupUiState> = _uiState.asStateFlow()
private val _navigateToDashboard = MutableSharedFlow<Unit>()
val navigateToDashboard: SharedFlow<Unit> = _navigateToDashboard.asSharedFlow()
private val disposables = CompositeDisposable()
private val tmsSetups = TMSSetupsImpl()
init {
viewModelScope.launch {
waitForHardware()
TMSUtil.getInstance().generateFinalVersion()
downloadConfigs()
}
}
fun downloadConfigs() {
_uiState.update {
it.copy(isLoading = true, isError = false, statusText = "Connecting to TMS server...")
}
val disposable = repository.getParams(buildRequest())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response ->
_uiState.update { it.copy(statusText = "Applying configuration...") }
val appResponse = SiriusResponse(
serial = response.serial.orEmpty(),
ecrKey = response.ecrKey.orEmpty(),
address = response.address.orEmpty(),
merchant = response.merchant,
hosts = response.hosts,
properties = response.properties
)
tmsSetups.initParams(appResponse, TMSUpdate.UPDATE, emvParamOperation)
onConfigApplied()
},
{ error ->
_uiState.update {
it.copy(
isLoading = false,
isError = true,
statusText = "Download failed",
errorMessage = formatNetworkError(error)
)
}
}
)
disposables.add(disposable)
}
private suspend fun waitForHardware() {
_uiState.update { it.copy(isLoading = true, statusText = "Starting hardware...") }
var elapsed = 0
while (BaseApplication.basicOptV2 == null && elapsed < 10_000) {
delay(500)
elapsed += 500
}
}
private fun onConfigApplied() {
val validity = TMSUtil.getInstance().checkParams()
if (validity.status == ValidityStatus.SUCCESS) {
_uiState.update { it.copy(isLoading = false, statusText = "Ready.") }
viewModelScope.launch { _navigateToDashboard.emit(Unit) }
} else {
_uiState.update {
it.copy(
isLoading = false,
isError = true,
statusText = "Configuration incomplete",
errorMessage = validity.message ?: "Invalid TMS configuration"
)
}
}
}
fun skipDownload() {
viewModelScope.launch { _navigateToDashboard.emit(Unit) }
}
private fun formatNetworkError(error: Throwable): String {
return when (error) {
is javax.net.ssl.SSLHandshakeException ->
"SSL handshake failed: ${error.message ?: "Certificate or protocol mismatch"}"
is javax.net.ssl.SSLException ->
"SSL/TLS error: ${error.message ?: "Secure connection could not be established"}"
is java.security.cert.CertificateException ->
"Server certificate error: ${error.message ?: "Certificate is invalid or untrusted"}"
is java.net.UnknownHostException ->
"Host not found: ${error.message ?: "Check server URL and network connection"}"
is java.net.ConnectException ->
"Connection refused: ${error.message ?: "Server is unreachable"}"
is java.net.SocketTimeoutException ->
"Connection timed out: ${error.message ?: "Server did not respond in time"}"
is retrofit2.HttpException ->
"HTTP ${error.code()} ${error.message()}"
else ->
error.message ?: "Unknown network error"
}
}
@SuppressLint("MissingPermission")
private fun buildRequest(): SiriusRequest {
return try {
val tranTime: Long = System.currentTimeMillis()
TMSUtil.getInstance().generateRequestParams("...", tranTime)
} catch (e: Exception) {
LogUtil.e("TmsSetupViewModel", e.message)
SiriusRequest()
}
}
override fun onCleared() {
super.onCleared()
disposables.clear()
}
}

View File

@ -0,0 +1,8 @@
package com.mob.utsmyanmar.ui.transaction_result
sealed interface TransactionResultEvent {
data object Start: TransactionResultEvent
data object BackClick: TransactionResultEvent
data object RetryPrint: TransactionResultEvent
data object PrintLater: TransactionResultEvent
}

View File

@ -0,0 +1,51 @@
package com.mob.utsmyanmar.ui.transaction_result
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
@Composable
fun TransactionResultRoute(
viewModel: TransactionResultViewModel,
sharedViewModel: SharedViewModel,
onNavigateMain: () -> Unit,
onNavigatePrintReceipt: () -> Unit,
onShowError: (String) -> Unit,
onShowSuccess: (String) -> Unit,
onShowPrinterDialog: (String) -> Unit
) {
val state by viewModel.state.collectAsStateWithLifecycle()
val canGoBack = sharedViewModel.transactionsType.value != TransactionsType.SALE
LaunchedEffect(Unit) {
viewModel.onEvent(
TransactionResultEvent.Start,
sharedViewModel
)
}
LaunchedEffect(Unit) {
viewModel.uiEvent.collect { event ->
when (event) {
TransactionResultUiEvent.NavigateMain -> onNavigateMain()
TransactionResultUiEvent.NavigatePrintReceipt -> onNavigatePrintReceipt()
is TransactionResultUiEvent.ShowError -> onShowError(event.message)
is TransactionResultUiEvent.ShowSuccess -> onShowSuccess(event.message)
is TransactionResultUiEvent.ShowPrinterDialog -> onShowPrinterDialog(event.message)
}
}
}
TransactionResultScreen(
state = state,
canGoBack = canGoBack,
onEvent = {
viewModel.onEvent(it, sharedViewModel)
}
)
}

View File

@ -0,0 +1,332 @@
package com.mob.utsmyanmar.ui.transaction_result
import android.R
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CalendarMonth
import androidx.compose.material.icons.filled.Numbers
import androidx.compose.material.icons.filled.Print
import androidx.compose.material.icons.rounded.Check
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.mob.utsmyanmar.ui.preview.P2Preview
import com.mob.utsmyanmar.ui.theme.Color
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.print.PrintReceipt
import com.utsmyanmar.paylibs.utils.POSUtil
@Composable
fun TransactionResultScreen(
state: TransactionResultState,
canGoBack: Boolean,
onEvent: (TransactionResultEvent) -> Unit,
) {
BackHandler(enabled = canGoBack) {
onEvent(TransactionResultEvent.BackClick)
}
Scaffold(
containerColor = Color.IvoryBeige
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.IvoryBeige)
.padding(paddingValues)
.statusBarsPadding()
.navigationBarsPadding()
.padding(horizontal = 20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(8.dp))
if(state.payDetail?.tradeAnswerCode.equals("00")) SuccessIcon()
else FailIcon()
Spacer(modifier = Modifier.height(16.dp))
Text(
text = state.title,
color = Color.LegacyRed,
fontSize = 24.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(6.dp))
Text(
text = state.message.ifBlank { "Your payment has been processed successfully." },
color = Color.Gray,
fontSize = 13.sp,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(28.dp))
AmountCard(amount = POSUtil.getInstance().getDecimalAmountSeparatorFormat(state.payDetail?.amount?:0))
Spacer(modifier = Modifier.height(16.dp))
InfoCard(
date = "26 May 2026",
time = "12:06 PM",
transactionId = state.payDetail?.referNo.toString()
)
}
}
}
@Composable
private fun SuccessIcon() {
Box(
modifier = Modifier.size(132.dp),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.size(132.dp)
.background(
color = Color.Success.copy(alpha = 0.12f),
shape = CircleShape
)
)
Box(
modifier = Modifier
.size(96.dp)
.background(
color = Color.Success,
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Rounded.Check,
contentDescription = "Success",
tint = Color.White,
modifier = Modifier.size(64.dp)
)
}
}
}
@Composable
private fun FailIcon() {
Box(
modifier = Modifier.size(132.dp),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.size(132.dp)
.background(
color = Color.Error.copy(alpha = 0.12f),
shape = CircleShape
)
)
Box(
modifier = Modifier
.size(96.dp)
.background(
color = Color.Error,
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Rounded.Close,
contentDescription = "Failed",
tint = Color.White,
modifier = Modifier.size(64.dp)
)
}
}
}
@Composable
private fun AmountCard(
amount: String
) {
Card(
modifier = Modifier
.fillMaxWidth()
.height(130.dp)
.shadow(
elevation = 6.dp,
shape = RoundedCornerShape(22.dp),
clip = false
),
shape = RoundedCornerShape(22.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White
)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 18.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "PAID AMOUNT",
color = Color.Gray,
fontSize = 15.sp,
fontWeight = FontWeight.SemiBold
)
Text(
text = amount,
color = Color.LegacyRed,
fontSize = 30.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(6.dp))
Text(
text = "MMK",
color = Color.LegacyRed,
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
}
}
}
@Composable
private fun InfoCard(
date: String,
time: String,
transactionId: String
) {
Card(
modifier = Modifier
.fillMaxWidth()
.height(130.dp)
.shadow(
elevation = 4.dp,
shape = RoundedCornerShape(18.dp),
clip = false
),
shape = RoundedCornerShape(18.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White
)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 18.dp),
verticalArrangement = Arrangement.SpaceAround
) {
InfoItem(
icon = Icons.Default.CalendarMonth,
title = "Date & Time",
value = date,
subValue = time,
// modifier = Modifier.weight(1f)
)
InfoItem(
icon = Icons.Default.Numbers,
title = "Transaction ID",
value = transactionId,
subValue = "",
// modifier = Modifier.weight(1f)
)
}
}
}
@Composable
private fun InfoItem(
icon: ImageVector,
title: String,
value: String,
subValue: String,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(46.dp)
.background(
color = Color.LegacyRed.copy(alpha = 0.1f),
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = Color.LegacyRed,
modifier = Modifier.size(25.dp)
)
}
Spacer(modifier = Modifier.width(10.dp))
Column {
Text(
text = title,
color = Color.Gray,
fontSize = 11.sp
)
Text(
text = value,
color = Color.Black,
fontSize = 13.sp,
fontWeight = FontWeight.Bold
)
if (subValue.isNotEmpty()) {
Text(
text = subValue,
color = Color.Black,
fontSize = 11.sp
)
}
}
}
}
@P2Preview
@Composable
fun PreviewTransactionResultScreen() {
TransactionResultScreen(
state = TransactionResultState(
"Success",
payDetail = PayDetail()
),
canGoBack = true,
onEvent = {},
)
}

View File

@ -0,0 +1,10 @@
package com.mob.utsmyanmar.ui.transaction_result
import com.utsmyanmar.paylibs.model.PayDetail
data class TransactionResultState(
val title: String = "Transaction Result",
val message: String = "",
val isLoading: Boolean = false,
val payDetail: PayDetail? = null
)

View File

@ -0,0 +1,9 @@
package com.mob.utsmyanmar.ui.transaction_result
sealed interface TransactionResultUiEvent {
data object NavigateMain : TransactionResultUiEvent
data object NavigatePrintReceipt : TransactionResultUiEvent
data class ShowError(val message: String) : TransactionResultUiEvent
data class ShowSuccess(val message: String) : TransactionResultUiEvent
data class ShowPrinterDialog(val message: String) : TransactionResultUiEvent
}

View File

@ -0,0 +1,287 @@
package com.mob.utsmyanmar.ui.transaction_result
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.mob.utsmyanmar.model.ecr.ECRResultStatus
import com.mob.utsmyanmar.utils.CoreUtils
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.utsmyanmar.baselib.emv.EmvParamOperation
import com.utsmyanmar.baselib.network.model.sirius.SiriusRequest
import com.utsmyanmar.ecr.ECRHelper
import com.utsmyanmar.ecr.ECRProcess
import com.utsmyanmar.ecr.data.model.TransactionsResp
import com.utsmyanmar.paylibs.Constant
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.print.PaperRollStatusCallback
import com.utsmyanmar.paylibs.print.PrintHelper
import com.utsmyanmar.paylibs.utils.PrintStatus
import com.utsmyanmar.paylibs.utils.core_utils.ByteUtil
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import sunmi.sunmiui.utils.LogUtil
import javax.inject.Inject
@HiltViewModel
class TransactionResultViewModel @Inject constructor(
private val emvParamOperation: EmvParamOperation
) : ViewModel() {
companion object {
private const val TAG = "TransactionResultVM"
private const val RESULT_TIMEOUT = 3000L
}
private val _state = MutableStateFlow(TransactionResultState())
val state = _state.asStateFlow()
private val _uiEvent = Channel<TransactionResultUiEvent>()
val uiEvent = _uiEvent.receiveAsFlow()
private var started = false
fun onEvent(
event: TransactionResultEvent,
sharedViewModel: SharedViewModel
) {
when (event) {
TransactionResultEvent.Start -> {
started = false
start(sharedViewModel)
}
TransactionResultEvent.BackClick -> isCardInside()
TransactionResultEvent.RetryPrint -> startPrintProcess(sharedViewModel, false)
TransactionResultEvent.PrintLater -> isCardInside()
}
}
private fun start(sharedViewModel: SharedViewModel) {
if (started) return
started = true
_state.value = _state.value.copy(payDetail = sharedViewModel.payDetail.value)
sharedViewModel.setPrintStatus(PrintStatus.FIRST_PRINT)
sharedViewModel.printXStatus.value = null
updateTitle(sharedViewModel)
observePrintStatus(sharedViewModel)
if (sharedViewModel.isEcr.value == true) {
ecrAction(sharedViewModel)
}
startResultTimeout(sharedViewModel)
}
private fun updateTitle(sharedViewModel: SharedViewModel) {
val payDetail = sharedViewModel.payDetail.value
val resultCode = payDetail?.tradeAnswerCode.orEmpty()
val qrStatus = payDetail?.qrTransStatus ?: -100
val isDemoMode = SystemParamsOperation.getInstance().demoStatus
val isSuccess = isDemoMode ||
resultCode == Constant.ANSWER_CODE_ACCEPT ||
resultCode == Constant.ANSWER_CODE_APPROVED
val title = when {
isSuccess -> "Transaction Success"
qrStatus == 1 || qrStatus == -1 || qrStatus == 2 || qrStatus == 3 -> "QR Pay"
else -> "Error"
}
val message = when {
isSuccess -> "Transaction approved"
payDetail?.tradeAnswerCode?.isNotBlank() == true -> payDetail.tradeAnswerCode
else -> "Transaction failed"
}
_state.value = _state.value.copy(
title = title,
message = message
)
}
private fun startResultTimeout(sharedViewModel: SharedViewModel) {
viewModelScope.launch {
delay(RESULT_TIMEOUT)
val payDetail = sharedViewModel.payDetail.value
if (payDetail == null) {
navigateMain()
return@launch
}
val transactionType = sharedViewModel.transactionsType.value
when {
isNonApprovedTrade(payDetail) &&
isNonWavepayTransaction(transactionType) -> {
startPrintProcess(sharedViewModel, false)
isCardInside()
}
transactionType == TransactionsType.SETTLEMENT -> {
startPrintProcess(sharedViewModel, true)
sendUiEvent(
TransactionResultUiEvent.ShowSuccess(
"Configs are updated"
)
)
navigateMain()
}
isWavePayNonSuccessTransaction(transactionType, payDetail) -> {
startPrintProcess(sharedViewModel, false)
navigateMain()
}
else -> {
SystemParamsOperation.getInstance()
.setLastSuccessTrnx(payDetail.voucherNo)
emvParamOperation.loadEmvTerminalParam()
navigatePrint()
}
}
}
}
private fun isNonApprovedTrade(payDetail: PayDetail): Boolean {
return payDetail.tradeAnswerCode != Constant.ANSWER_CODE_APPROVED &&
payDetail.tradeAnswerCode != Constant.ANSWER_CODE_ACCEPT
}
private fun isNonWavepayTransaction(type: TransactionsType?): Boolean {
return type != TransactionsType.WAVEPAY &&
type != TransactionsType.WAVE_INQUIRY_STATUS &&
type != TransactionsType.WAVEPAY_REFUND
}
private fun isWavePayNonSuccessTransaction(
type: TransactionsType?,
payDetail: PayDetail
): Boolean {
return (type == TransactionsType.WAVEPAY && payDetail.qrTransStatus != 1) ||
(type == TransactionsType.WAVE_INQUIRY_STATUS && payDetail.qrTransStatus != 1) ||
(type == TransactionsType.WAVEPAY_REFUND && payDetail.qrTransStatus != 1)
}
private fun observePrintStatus(sharedViewModel: SharedViewModel) {
sharedViewModel.printXStatus.observeForever { status ->
when (status) {
PrintStatus.EMPTY_PAPER_ROLL -> {
startPrintProcess(sharedViewModel, false)
}
PrintStatus.DONE_PRINT -> {
isCardInside()
}
else -> Unit
}
}
}
private fun startPrintProcess(
sharedViewModel: SharedViewModel,
isSettlement: Boolean
) {
PrintHelper.getInstance().checkPaperRollStatus(
object : PaperRollStatusCallback {
override fun paperRollIsReady() {
if (isSettlement) {
sharedViewModel.startPrintProcessSettlement()
} else {
sharedViewModel.startPrintProcess()
}
}
override fun paperRollIsEmpty() {
sendUiEvent(
TransactionResultUiEvent.ShowPrinterDialog(
"Paper roll not ready"
)
)
}
override fun paperRollLipIsOpened() {
sendUiEvent(
TransactionResultUiEvent.ShowPrinterDialog(
"Printer lip is opened. Please close lip."
)
)
}
override fun unknownStatusOccur(code: Int) {
sendUiEvent(
TransactionResultUiEvent.ShowError(
"Check printer status: $code"
)
)
}
}
)
}
private fun getECRResponseMessage(
sharedViewModel: SharedViewModel
): String {
val resp: TransactionsResp =
CoreUtils.getInstance(sharedViewModel).generateResponseMsg()
return ECRProcess.generateECRResponse(resp)
}
private fun getECRResponseCMHL(
sharedViewModel: SharedViewModel
): ByteArray {
return CoreUtils.getInstance(sharedViewModel)
.generateCMHLResponse(ECRResultStatus.RESPONSE_RECEIVED)
}
private fun ecrAction(sharedViewModel: SharedViewModel) {
if (SystemParamsOperation.getInstance().isCMHLEnabled) {
val response = getECRResponseCMHL(sharedViewModel)
LogUtil.d(TAG, "ECR Response: ${ByteUtil.bytes2HexStr(response)}")
ECRHelper.send(response)
CoreUtils.getInstance(sharedViewModel).responseACKCMHL()
} else {
val response = getECRResponseMessage(sharedViewModel)
LogUtil.d(TAG, "ECR Response: $response")
ECRHelper.send(response.toByteArray())
}
}
private fun isCardInside() {
navigateMain()
}
private fun navigateMain() {
sendUiEvent(TransactionResultUiEvent.NavigateMain)
}
private fun navigatePrint() {
sendUiEvent(TransactionResultUiEvent.NavigatePrintReceipt)
}
private fun sendUiEvent(event: TransactionResultUiEvent) {
viewModelScope.launch {
_uiEvent.send(event)
}
}
}

View File

@ -0,0 +1,207 @@
package com.mob.utsmyanmar.ui.version
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.*
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.*
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.device_info.DeviceInfoViewModel
import com.mob.utsmyanmar.ui.preview.P2Preview
import com.mob.utsmyanmar.ui.preview.P3Preview
import com.mob.utsmyanmar.ui.theme.Color
import com.mob.utsmyanmar.R
@Composable
fun VersionScreen(
onBack: () -> Unit,
deviceInfoViewModel: DeviceInfoViewModel
) {
val deviceInfo by deviceInfoViewModel.uiState.collectAsState();
val itemSpace = 20.dp;
LaunchedEffect(Unit) {
deviceInfoViewModel.loadDeviceInfo();
}
Scaffold(
topBar = {
AppBar(
title = "Version",
onIconClick = onBack,
icon = Icons.Default.ChevronLeft
)
},
containerColor = Color.IvoryBeige
)
{ paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.padding(16.dp)
) {
Text("Device Information")
Spacer(Modifier.height(16.dp))
Card(
colors = CardDefaults.cardColors(
containerColor = Color.White
)
) {
Column(
modifier = Modifier.padding(16.dp),
) {
Item(
title = "PROD NAME",
value = deviceInfo.deviceModel,
icon = {
Icon(
painter = painterResource(R.drawable.ic_function),
contentDescription = "icon",
modifier = Modifier.size(24.dp),
tint = Color.LegacyRed
)
}
)
Spacer(Modifier.height(itemSpace))
Item(
title = "TERMINAL SERIAL",
value = deviceInfo.serialNumber,
icon = {
Icon(
painter = painterResource(R.drawable.ic_four_vertical_bars),
contentDescription = "icon",
modifier = Modifier.size(24.dp),
tint = Color.LegacyRed
)
}
)
Spacer(Modifier.height(itemSpace))
Item(
title = "HARDWARE VER",
value = deviceInfo.hardwareVersion,
icon = {
Icon(
painter = painterResource(R.drawable.ic_gear),
contentDescription = "icon",
modifier = Modifier.size(24.dp),
tint = Color.LegacyRed
)
}
)
Spacer(Modifier.height(itemSpace))
Item(
title = "FIRMWARE VER",
value = deviceInfo.firmwareVersion,
icon = {
Icon(
painter = painterResource(R.drawable.ic_code),
contentDescription = "icon",
modifier = Modifier.size(24.dp),
tint = Color.LegacyRed
)
}
)
Spacer(Modifier.height(itemSpace))
Item(
title = "S/W VER",
value = deviceInfo.finalVersion,
icon = {
Icon(
painter = painterResource(R.drawable.ic_settings),
contentDescription = "icon",
modifier = Modifier.size(24.dp),
tint = Color.LegacyRed
)
}
)
Spacer(Modifier.height(itemSpace))
Item(
title = "PAYHARDWARE SERVICE VER",
value = deviceInfo.payHardwareVersion,
icon = {
Icon(
painter = painterResource(R.drawable.ic_protection_shield),
contentDescription = "icon",
modifier = Modifier.size(24.dp),
tint = Color.LegacyRed
)
}
)
Spacer(Modifier.height(itemSpace))
Item(
title = "ROM VER",
value = deviceInfo.romVersion,
icon = {
Icon(
painter = painterResource(R.drawable.ic_function),
contentDescription = "icon",
modifier = Modifier.size(24.dp),
tint = Color.LegacyRed
)
}
)
}
}
}
}
}
@Composable
fun Item(
title: String,
value: String,
icon: @Composable () -> Unit,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Row(
modifier = Modifier.weight(1f),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(50.dp)
.background(
Color.CrimsonRed.copy(alpha = 0.1f),
shape = RoundedCornerShape(12.dp)
),
contentAlignment = Alignment.Center
) {
icon()
}
Text(text = title, fontSize = 14.sp)
}
Row(
modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = ":")
Text(text = value, fontSize = 14.sp)
}
}
}
@P2Preview
@P3Preview
@Composable
fun P3PreviewVersionScreen() {
VersionScreen(
onBack = {},
deviceInfoViewModel = hiltViewModel()
)
}

View File

@ -0,0 +1,15 @@
package com.mob.utsmyanmar.utils
import android.content.Context
object AppContextHolder {
private lateinit var appContext: Context
fun init(context: Context) {
appContext = context.applicationContext
}
fun get(): Context {
return appContext
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
package com.mob.utsmyanmar.utils
import com.utsmyanmar.ecr.data.model.Transactions
interface ECRSetups {
fun setUpECREchoTest()
fun setUpECRSale(trans: Transactions): Boolean
fun setUpECRQR(trans: Transactions): Boolean
fun setUpECRVoid(trans: Transactions): Boolean
fun setUpECRCashAdvance(trans: Transactions): Boolean
fun setUpECRPreAuth(trans: Transactions): Boolean
fun setUpECRPreAuthVoid(trans: Transactions): Boolean
fun setUpECRPreAuthComplete(trans: Transactions): Boolean
fun setUpECRPreAuthCompleteVoid(trans: Transactions): Boolean
fun setUpECRSettlement()
fun setUpECRRefund(trans: Transactions): Boolean
}

View File

@ -0,0 +1,24 @@
package com.mob.utsmyanmar.utils
import com.kizzy.cmhl.models.PingRequest
import com.kizzy.cmhl.models.PreAuthCancellationRequest
import com.kizzy.cmhl.models.PreAuthCompletionRequest
import com.kizzy.cmhl.models.PreAuthRequest
import com.kizzy.cmhl.models.QrPaymentRequest
import com.kizzy.cmhl.models.SaleRequest
import com.kizzy.cmhl.models.SettlementRequest
import com.kizzy.cmhl.models.VoidQrPaymentRequest
import com.kizzy.cmhl.models.VoidRequest
interface ECRSetupsCMHL {
fun setUpECREchoTest()
fun setupPingRequest(trans: PingRequest)
fun setUpECRSale(trans: SaleRequest): Boolean
fun setUpECRQR(trans: QrPaymentRequest): Boolean
fun setUpECRQRVoid(trans: VoidQrPaymentRequest): Boolean
fun setUpECRVoid(trans: VoidRequest): Boolean
fun setUpECRPreAuth(trans: PreAuthRequest): Boolean
fun setUpECRPreAuthVoid(trans: PreAuthCancellationRequest): Boolean
fun setUpECRPreAuthComplete(trans: PreAuthCompletionRequest): Boolean
fun setUpECRSettlement(trans: SettlementRequest)
}

View File

@ -0,0 +1,57 @@
package com.mob.utsmyanmar.utils
import com.utsmyanmar.checkxread.model.CardDataX
data class MockCardData(
val cardNo: String = "",
val expDate: String = "",
val cardScheme: String = "",
val cardHolderName: String = "",
val phoneNo: String = "",
val iccData: String = ""
)
object MockData {
private val mockCardData: MockCardData = MockCardData(
cardNo="9503712156912514",
expDate="2912",
cardScheme="MPU",
cardHolderName="Htin Kyaw Win",
iccData="9503712156912514=29121010000000000000"
)
fun generateMPUCard(): CardDataX = CardDataX().apply {
pan = mockCardData.cardNo
exp = mockCardData.expDate
cardHolderName = mockCardData.cardHolderName
track2 = mockCardData.iccData
}
/*
// VISA
MockCardData(cardNo="3230101288181", expDate="0425", cardScheme="VISA",
cardHolderName="U AYE", iccData="5F21BLAHBLAH")
// MPU - KBZ Debit
MockCardData(cardNo="9503051034047056", expDate="3002", cardScheme="MPU",
cardHolderName="KBZ Debit", iccData="9503051034047056=30021015930000000000")
// MPU - KBZ Credit
MockCardData(cardNo="9505050161133125", expDate="2701", cardScheme="MPU",
cardHolderName="KBZ Credit", iccData="9505050161133125=27011017250000000000")
// MPU - Htin Kyaw Win
MockCardData(cardNo="9503712156912514", expDate="2912", cardScheme="MPU",
cardHolderName="Htin Kyaw Win", iccData="9503712156912514=29121010000000000000")
// MPU - Bank Q
MockCardData(cardNo="9503742975107251", expDate="0629", cardScheme="MPU",
cardHolderName="Bank Q", iccData="9503742975107251=22081010000000000000")
// WALLET
MockCardData(phoneNo="9794452506", expDate="0425", cardScheme="WALLET",
cardHolderName="YOMA VALUED CUSTOMER")
*/
}

View File

@ -0,0 +1,14 @@
package com.mob.utsmyanmar.utils
interface OldECRSetups {
fun setUpECRTest()
fun setUpECRSale(msg: String): Boolean
fun setUpECRVoid(msg: String): Boolean
fun setUpECRCashAdvance(msg: String): Boolean
fun setUpECRPreAuth(msg: String): Boolean
fun setUpECRPreAuthVoid(msg: String): Boolean
fun setUpECRPreAuthComplete(msg: String): Boolean
fun setUpECRPreAuthCompleteVoid(msg: String): Boolean
fun setUpECRSettlement()
fun setUpECRRefund(msg: String): Boolean
}

View File

@ -0,0 +1,120 @@
package com.mob.utsmyanmar.utils
import com.mob.utsmyanmar.config.Constants
import com.utsmyanmar.checkxread.model.CardDataX
import com.utsmyanmar.checkxread.util.CardTypeX
import com.utsmyanmar.paylibs.model.CardInfo
import com.utsmyanmar.paylibs.model.MAGCardInfo
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.model.TradeData
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
import com.utsmyanmar.paylibs.utils.params.Params
import sunmi.sunmiui.utils.LogUtil
object TransactionUtil {
private const val TAG = "TransactionUtil"
private const val MPU_CARD_SCHEME = "MPU"
private const val VISA_CARD_SCHEME = "VISA"
private const val MASTER_CARD_SCHEME = "MASTER"
private const val UPI_CARD_SCHEME = "UPI"
private val qrTerminalId: String
get() = SystemParamsOperation.getInstance().secHostTerminalId
private val qrMerchantId: String
get() = SystemParamsOperation.getInstance().secHostMerchantId
fun getQRMerchantId(): String {
return qrMerchantId
}
fun getQRTerminalId(): String {
return qrTerminalId
}
fun initMPUTransaction(
cardDataX: CardDataX,
cardTypeX: CardTypeX
): TradeData {
LogUtil.d(TAG, "CardDataX : $cardDataX")
val tradeData = Params.newTrade(false)
val payDetail = tradeData.payDetail
payDetail.cardNo = cardDataX.pan
payDetail.expDate = cardDataX.exp
payDetail.cardType = cardTypeX.value
payDetail.accountType = MPU_CARD_SCHEME
payDetail.cardHolderName = cardDataX.cardHolderName
val cardInfo = CardInfo()
val magCardInfo = MAGCardInfo()
magCardInfo.track2Cipher = cardDataX.track2
cardInfo.magCardInfo = magCardInfo
payDetail.cardInfo = cardInfo
return tradeData
}
fun initMagStripeTransaction(
cardDataX: CardDataX,
isFallback: Boolean
): TradeData {
LogUtil.d(TAG, "CardDataX : $cardDataX")
val tradeData = Params.newTrade(false)
val payDetail = tradeData.payDetail
payDetail.cardNo = cardDataX.pan
payDetail.expDate = cardDataX.exp
payDetail.accountType = when {
cardDataX.pan.startsWith("4") -> VISA_CARD_SCHEME
cardDataX.pan.startsWith("5") -> MASTER_CARD_SCHEME
cardDataX.pan.startsWith("6") -> UPI_CARD_SCHEME
else -> MPU_CARD_SCHEME
}
payDetail.cardType = if (isFallback) {
-9
} else {
CardTypeX.MAG.value
}
payDetail.cardHolderName = cardDataX.cardHolderName
val cardInfo = CardInfo()
val magCardInfo = MAGCardInfo()
magCardInfo.track2Cipher = cardDataX.track2
cardInfo.magCardInfo = magCardInfo
payDetail.cardInfo = cardInfo
return tradeData
}
fun initWalletTransaction(
transactionType: TransactionsType
): PayDetail {
val tradeData = Params.newTrade(true)
val payDetail = tradeData.payDetail
payDetail.accountType = Constants.WALLET
payDetail.transType = transactionType.name
payDetail.transactionType = transactionType.value
payDetail.terminalNo = qrTerminalId
payDetail.merchantNo = qrMerchantId
payDetail.currencyCode = "104"
payDetail.isCanceled = false
return payDetail
}
}

View File

@ -0,0 +1,89 @@
package com.mob.utsmyanmar.utils.tms
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.telephony.TelephonyManager
import androidx.annotation.RequiresPermission
object Connectivity {
fun isConnected(context: Context): Boolean {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val network = cm.activeNetwork ?: return false
val capabilities = cm.getNetworkCapabilities(network) ?: return false
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
}
fun isConnectedWifi(context: Context): Boolean {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val capabilities = cm.getNetworkCapabilities(cm.activeNetwork ?: return false) ?: return false
return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
}
fun isConnectedMobile(context: Context): Boolean {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val capabilities = cm.getNetworkCapabilities(cm.activeNetwork ?: return false) ?: return false
return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
}
fun isConnectedFast(context: Context): Boolean {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val capabilities = cm.getNetworkCapabilities(cm.activeNetwork ?: return false) ?: return false
return when {
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> {
// Downstream bandwidth in Kbps; 2000 Kbps = ~2 Mbps threshold for "fast"
capabilities.linkDownstreamBandwidthKbps >= 2000
}
else -> false
}
}
@RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
fun getNetworkType(context: Context): String {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val capabilities = cm.getNetworkCapabilities(cm.activeNetwork ?: return "None") ?: return "None"
if (!capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
return when {
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> "WiFi"
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> "Ethernet"
else -> "Unknown"
}
}
val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
val networkType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
tm.dataNetworkType
} else {
@Suppress("DEPRECATION")
tm.networkType
}
return when (networkType) {
TelephonyManager.NETWORK_TYPE_GPRS,
TelephonyManager.NETWORK_TYPE_EDGE,
TelephonyManager.NETWORK_TYPE_CDMA,
TelephonyManager.NETWORK_TYPE_1xRTT,
TelephonyManager.NETWORK_TYPE_IDEN -> "2G"
TelephonyManager.NETWORK_TYPE_UMTS,
TelephonyManager.NETWORK_TYPE_EVDO_0,
TelephonyManager.NETWORK_TYPE_EVDO_A,
TelephonyManager.NETWORK_TYPE_EVDO_B,
TelephonyManager.NETWORK_TYPE_HSDPA,
TelephonyManager.NETWORK_TYPE_HSUPA,
TelephonyManager.NETWORK_TYPE_HSPA,
TelephonyManager.NETWORK_TYPE_EHRPD,
TelephonyManager.NETWORK_TYPE_HSPAP -> "3G"
TelephonyManager.NETWORK_TYPE_LTE -> "4G"
TelephonyManager.NETWORK_TYPE_NR -> "5G"
else -> "Unknown"
}
}
}

View File

@ -0,0 +1,25 @@
package com.mob.utsmyanmar.utils.tms
import com.mob.utsmyanmar.model.sirius.SiriusResponse
import com.mob.utsmyanmar.model.sirius.TMSUpdate
import com.utsmyanmar.baselib.emv.EmvParamOperation
interface TMSSetups {
fun initParams(
siriusResponse: SiriusResponse,
tmsUpdate: TMSUpdate,
emvParamOperation: EmvParamOperation
)
fun initParams(json: String)
fun convertToArray(string: String): ArrayList<String>
fun getPayHardwareVersion(): String
fun getRomVersion(): String
fun generateFinalVersion(): String
}

View File

@ -0,0 +1,275 @@
package com.mob.utsmyanmar.utils.tms
import com.google.gson.Gson
import com.mob.utsmyanmar.model.sirius.SiriusResponse
import com.mob.utsmyanmar.model.sirius.TMSUpdate
import com.utsmyanmar.baselib.BaseApplication
import com.utsmyanmar.baselib.emv.EmvParamOperation
import com.utsmyanmar.baselib.network.model.sirius.SiriusHost
import com.utsmyanmar.baselib.network.model.sirius.SiriusMerchant
import com.utsmyanmar.baselib.network.model.sirius.SiriusProperty
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import com.utsmyanmar.paylibs.utils.enums.CurrencyType
import sunmi.sunmiui.utils.LogUtil
class TMSSetupsImpl : TMSSetups {
companion object {
private const val TAG = "TMSSetupsImpl"
private const val UPI = "UPI"
private const val JCB = "JCB"
private const val VISA = "VISA"
private const val MASTERCARD = "MASTERCARD"
}
private fun currencyTextToCurrencyType(currencyText: String): CurrencyType {
return try {
CurrencyType.valueOf(currencyText)
} catch (e: Exception) {
CurrencyType.MMK
}
}
private fun currencyTextToCode(currencyText: String): String {
return when (currencyText) {
"USD" -> "804"
"CNY" -> "156"
"THB" -> "764"
"RUB" -> "643"
else -> "104"
}
}
private fun currencyCodeToText(currencyCode: String): String {
return when (currencyCode) {
"804" -> "USD"
"156" -> "CNY"
"764" -> "THB"
"643" -> "RUB"
else -> "MMK"
}
}
private fun init() {
SystemParamsOperation.getInstance().apply {
hostName = ""
terminalId = ""
merchantId = ""
ipAddress = ""
secIpAddress = ""
secHostName = ""
secHostTerminalId = ""
secHostMerchantId = ""
secHostIpAddress = ""
secHostSecIpAddress = ""
}
}
override fun initParams(
siriusResponse: SiriusResponse,
tmsUpdate: TMSUpdate,
emvParamOperation: EmvParamOperation
) {
val siriusMerchant: SiriusMerchant = siriusResponse.merchant
val siriusHosts: List<SiriusHost> = siriusResponse.hosts
val siriusProperty: List<SiriusProperty> = siriusResponse.properties
val imgUrls = mutableListOf<String>()
val ops = SystemParamsOperation.getInstance()
if (tmsUpdate == TMSUpdate.UPDATE) ops.isNeedSettlement = false
siriusResponse.address.takeIf { it.isEmpty() }?.let { ops.merchantAddress = "" }
init()
siriusMerchant.let { m ->
ops.merchantName = m.name
ops.merchantAddress = m.address
ops.merchantPhoneNo = m.phone
}
siriusResponse.address.takeIf { it.isNotEmpty() }?.let { ops.merchantAddress = it }
//host
if (siriusHosts.isNotEmpty()) {
for (host in siriusHosts) {
val isMMQR = listOf(host.name, host.description).any {
it.lowercase().run { contains("mmqr") }
}
if (isMMQR) {
ops.secHostName = host.name
ops.secHostTerminalId = host.tid
ops.secHostMerchantId = host.mid
host.secondaryIP.trim().let { ip ->
ops.secHostIpAddress = if (ip.contains(":")) "$ip/" else ""
}
host.currency.takeIf { it.isNotEmpty() }?.let {
ops.secHostCurrency = currencyTextToCode(it)
}
if (host.tid.isEmpty()) ops.secHostTerminalId = ""
if (host.mid.isEmpty()) ops.secHostTerminalId = ""
} else {
ops.hostName = host.name
ops.terminalId = host.tid
ops.merchantId = host.mid
host.primaryIP.trim().let { ip ->
ops.ipAddress = if (ip.contains(":")) ip else ""
}
host.secondaryIP.trim().let { ip ->
ops.secIpAddress = if (ip.contains(":")) ip else ""
}
host.currency.takeIf { it.isNotEmpty() }?.let {
ops.currencyType = currencyTextToCurrencyType(it)
}
if (host.tid.isEmpty()) ops.terminalId = ""
if (host.mid.isEmpty()) ops.merchantId = ""
}
}
}
//Properties
for (prop in siriusProperty) {
val name = prop.name
val data = prop.property
when (name) {
//image
"carousel_img_1",
"carousel_img_2",
"carousel_img_3",
"carousel_img_4",
"carousel_img_5",
"carousel_img_6" -> imgUrls.add(data)
//host
"host_timeout",
"host_connect_timeout" -> ops.hostResponseTimeout = data
"host_read_timeout" -> ops.hostReadTimeout = data
"reversal_delay" -> ops.reversalDelay = data
"key_index" -> ops.tmkIndex = data
"receipt_footer" -> ops.receiptFooter = data
"time_out" -> ops.setTmsTimeout(data)
"manual_update" -> ops.manualUpdate = parseBoolean(data)
"emv_enable" -> ops.setEmvEnable(parseBoolean(data))
"hostport" -> ops.portAddress = data.toInt()
"pre_auth_enable" -> ops.preAuthStatus = parseBoolean(data)
"void_enable" -> ops.voidStatus = parseBoolean(data)
"cash_advance_enable" -> ops.cashAdvanceStatus = parseBoolean(data)
"refund_enable" -> ops.refundStatus = parseBoolean(data)
"settlement_enable" -> ops.settlementStatus = parseBoolean(data)
"system_password" -> ops.systemPassword = data.take(6).ifEmpty { data }
"settlement_password" -> ops.settlementPassword = data.take(6).ifEmpty { data }
"setting_password" -> ops.settingPassword = data.take(6).ifEmpty { data }
"terminal_enable" -> ops.isActive = parseBoolean(data)
"terminal_enable_msg" -> ops.disabledMsg = data
"ssl_enable" -> ops.setSslSwitchStatus(parseBoolean(data))
"wave_pay_inquiry_status_enable" -> ops.wavePayInquiryStatus = parseBoolean(data)
"tips_adjustment_enable" -> ops.tipsAdjustmentStatus = parseBoolean(data)
"wave_enable" -> ops.wavePayStatus = parseBoolean(data)
"print_iso_enable" -> ops.printISOStatus = parseBoolean(data)
"receipt_header" -> ops.receiptHeader = data
"random_pin_pad_enable" -> ops.isRandomPinPad = parseBoolean(data)
"clear_batch_time" -> ops.clearBatchTime = data
"alert_sound_enable" -> ops.isAlertSound = parseBoolean(data)
"auto_print_enable" -> ops.isAutoPrintCustomerCopy = parseBoolean(data)
"ecr_enable" -> ops.ecrStatus = parseBoolean(data)
"manual_entry_enable" -> ops.setManualEntyrStatus(parseBoolean(data))
"mmqr_interval_waiting_time" -> ops.waveIntervalTime = data
"full_void_preauth_enable" -> ops.fullVoidPreauthStatus = parseBoolean(data)
"partial_void_preauth_enable" -> ops.partialVoidPreauthStatus = parseBoolean(data)
"clear_batch_day" -> ops.clearBatchDay = data
"qr_min_amount" -> ops.minAmount = data
"qr_max_amount" -> ops.maxAmount = data
"mmqr_auth_token" -> ops.authToken = data
"mmqr_grant_type" -> ops.grantType = data
"mmqr_token_host_address" -> ops.tokenHostAddress = "${data.trim()}/"
"mmpay_enable" -> ops.isMMPayEnabled = parseBoolean(data)
"fallback_enable" -> ops.fallbackEnabled = parseBoolean(data)
"magstripe_enable" -> ops.isMagStripeEnabled = parseBoolean(data)
"nfc_enable" -> ops.isNfcEnabled = parseBoolean(data)
"cvv_bypass_enable" -> ops.cvvBypassStatus = parseBoolean(data)
"upi_chip_cvm" -> emvParamOperation.updateChipCVM(UPI, data.toLong())
"upi_contactless_cvm" -> {
val limit = data.toLong()
ops.upiCvmLimit = limit
emvParamOperation.updateUpiCVM(limit)
}
"upi_currency_code" -> emvParamOperation.updateUpiCurrencyCode(data)
"jcb_chip_cvm" -> emvParamOperation.updateChipCVM(JCB, data.toLong())
"visa_chip_cvm" -> emvParamOperation.updateChipCVM(VISA, data.toLong())
"visa_contactless_cvm" -> emvParamOperation.updatePayWaveCVM(data.toLong())
"visa_currency_code" -> emvParamOperation.updatePayWaveCurrencyCode(data)
"master_chip_cvm" -> emvParamOperation.updateChipCVM(MASTERCARD, data.toLong())
"master_contactless_cvm" -> emvParamOperation.updatePayPassCVM(data.toLong())
"master_currency_code" -> emvParamOperation.updatePayPassCurrencyCode(data)
"terminal_capability" -> {
if (data.isNotEmpty()) ops.setTerminalCapability(data)
}
"upi_ttq" -> emvParamOperation.updateQuickPassTTQ(data)
"visa_ttq" -> emvParamOperation.updatePayWaveTTQ(data)
"master_ttq" -> emvParamOperation.updatePayPassTTQ(data)
"upi_tc_enabled" -> ops.setUpiTCEnabled(parseBoolean(data))
"debug_feature_enable" -> ops.setDebugFeatureEnabled(parseBoolean(data))
"master_terminal_capability" -> {
if (data.isNotEmpty()) emvParamOperation.updatePayPassTerminalCapability(data)
}
"speedup_contactless_enable" -> ops.setSpeedUpContactless(parseBoolean(data))
"manual_entry_pin_enable" -> ops.isManualEntryPinEnable = parseBoolean(data)
"cmhl_enabled" -> ops.setCMHLEnable(parseBoolean(data))
}
}
ops.carouselUrls = imgUrls.joinToString(",")
}
override fun initParams(json: String) {
val response = Gson().fromJson<SiriusResponse>(json, SiriusResponse::class.java)
response.properties.forEach { prop ->
LogUtil.d(TAG, "name : ${prop.name}")
LogUtil.d(TAG, "value: ${prop.property}")
}
}
override fun convertToArray(string: String): ArrayList<String> {
if (string.isEmpty()) return ArrayList()
return ArrayList(string.split(",").filter { it.isNotEmpty() })
}
override fun getPayHardwareVersion(): String {
return try {
BaseApplication.getInstance().applicationContext.packageManager
.getPackageInfo("com.sunmi.pay.hardware_v3", 0)
.versionName ?: "?"
} catch (e: Exception) {
"?"
}
}
override fun getRomVersion(): String {
return try {
android.os.Build.VERSION.RELEASE
} catch (e: Exception) {
"UNKNOWN"
}
}
override fun generateFinalVersion(): String {
val phv = getPayHardwareVersion()
val rv = getRomVersion()
val sv = try {
BaseApplication.getInstance().applicationContext.packageManager
.getPackageInfo(BaseApplication.getInstance().packageName, 0)
.versionName ?: "?"
} catch (e: Exception) {
"?"
}
SystemParamsOperation.getInstance().setFinalVersion("PHV$phv-RV$rv-SV$sv")
return "PHV$phv-RV$rv-SV$sv"
}
private fun parseBoolean(data: String): Boolean =
data.toIntOrNull()?.let { it == 1 } ?: data.toBoolean();
}

View File

@ -0,0 +1,169 @@
package com.mob.utsmyanmar.utils.tms
import android.Manifest
import android.content.Context
import android.os.BatteryManager
import android.os.Build
import androidx.annotation.RequiresPermission
import com.mob.utsmyanmar.BuildConfig
import com.mob.utsmyanmar.model.sirius.TMSValidity
import com.mob.utsmyanmar.model.sirius.ValidityStatus
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.sunmi.pay.hardware.aidl.AidlConstants
import com.utsmyanmar.baselib.BaseApplication
import com.utsmyanmar.baselib.network.model.sirius.SiriusRequest
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import sunmi.sunmiui.utils.LogUtil
class TMSUtil private constructor() {
private val tmsSetups: TMSSetups = TMSSetupsImpl();
companion object {
private val TAG = TMSUtil::class.java.simpleName
@Volatile
private var app: TMSUtil? = null
fun getInstance(): TMSUtil = app ?: synchronized(this) {
app ?: TMSUtil().also { app = it }
}
}
// --- Param Init ---
fun initParams(json: String) {
tmsSetups.initParams(json)
}
fun convertToArray(string: String): ArrayList<String> = tmsSetups.convertToArray(string)
fun getPayHardwareVersion(): String = tmsSetups.getPayHardwareVersion()
fun getRomVersion(): String = tmsSetups.getRomVersion()
fun generateFinalVersion(): String = tmsSetups.generateFinalVersion()
//---shared view model---
fun loadDownloadParameters(sharedViewModel: SharedViewModel) {
sharedViewModel.setManualEntryStatus(SystemParamsOperation.getInstance().manualEntryStatus)
}
//---system params---
fun getSystemParams(name: String): String = runCatching {
BaseApplication.basicOptV2.getSysParam(name)
}.getOrDefault("")
fun getSerialNumber(): String = runCatching {
BaseApplication.basicOptV2.getSysParam("SN")
}.getOrDefault("")
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
fun generateRequestParams(lastTransName: String, lastTransTime: Long): SiriusRequest =
SiriusRequest().apply {
serial = getSerialNumber()
appPackage = BuildConfig.APPLICATION_ID
androidVersion = Build.VERSION.RELEASE
firmwareVersion = getSystemParams(AidlConstants.SysParam.FIRMWARE_VERSION)
applicationVersion = BuildConfig.VERSION_NAME
currentNetwork = getNetworkType()
lastTransaction = lastTransName
lastTranTime = lastTransTime
latitude = 0.000000
longitude = 0.000000
value = "YourValueHere"
}
//---logging--
fun retrieveParameters() {
val ops = SystemParamsOperation.getInstance()
LogUtil.d(TAG, "TID: ${ops.terminalId}")
LogUtil.d(TAG, "MID: ${ops.merchantId}")
LogUtil.d(TAG, "Merchant Name: ${ops.merchantName}")
LogUtil.d(TAG, "Merchant Address: ${ops.merchantAddress}")
LogUtil.d(TAG, "Host Timeout: ${ops.hostResponseTimeout}")
LogUtil.d(TAG, "TMS Timeout: ${ops.tmsTimeOut}")
LogUtil.d(TAG, "Key Index: ${ops.tmkIndex}")
LogUtil.d(TAG, "Receipt Footer: ${ops.receiptFooter}")
LogUtil.d(TAG, "Manual Update: ${ops.manualUpdate}")
LogUtil.d(TAG, "Master Enabled: ${ops.isEmvEnabled}")
}
//---checks---
fun checkParams(): TMSValidity {
val ops = SystemParamsOperation.getInstance()
val tid = ops.terminalId
val mid = ops.merchantId
val hostIp = ops.ipAddress
val secIp = ops.secIpAddress
val keyIndex = ops.tmkIndex
return when {
tid.length == 8 && mid.length == 15 && hostIp.isNotEmpty() && secIp.isNotEmpty() && keyIndex.isNotEmpty() -> TMSValidity(
ValidityStatus.SUCCESS, "Success"
)
tid.length != 8 -> TMSValidity(ValidityStatus.FAILURE, "Tid is invalid")
mid.length != 15 -> TMSValidity(ValidityStatus.FAILURE, "Mid is invalid")
hostIp.isEmpty() -> TMSValidity(ValidityStatus.FAILURE, "host ip is empty")
secIp.isEmpty() -> TMSValidity(ValidityStatus.FAILURE, "sec ip is empty")
else -> TMSValidity(ValidityStatus.FAILURE, "KeyIndex is invalid")
}
}
fun checkSecHostParams(): TMSValidity {
val ops = SystemParamsOperation.getInstance()
val tid = ops.secHostTerminalId
val mid = ops.secHostMerchantId
val hostIp = ops.secHostIpAddress
val secIp = ops.secHostSecIpAddress
val keyIndex = ops.tmkIndex
return when {
tid.length == 8 && mid.length == 15 && hostIp.isNotEmpty() && secIp.isNotEmpty() && keyIndex.isNotEmpty()
-> TMSValidity(ValidityStatus.SUCCESS, "Success")
tid.length != 8 -> TMSValidity(ValidityStatus.FAILURE, "MMQR Tid is invalid!")
mid.length != 15 -> TMSValidity(ValidityStatus.FAILURE, "MMQR Mid is invalid!")
hostIp.isEmpty() -> TMSValidity(ValidityStatus.FAILURE, "MMQR Pri-Ip is invalid!")
else -> TMSValidity(ValidityStatus.FAILURE, "MMQR Sec-Ip is invalid!")
}
}
//---helpers---
private fun getTransactionStatus(code: String): String = when (code) {
"00" -> "Transaction Approved"
else -> "Transaction Failed, reason : $code"
}
private fun getBoolean(data: String): Boolean = data == "1"
private fun getBatteryLevel(context: Context): Int {
val bm = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
return bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
}
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
private fun getNetworkType(): String {
val ctx = BaseApplication.getInstance().applicationContext
return when {
Connectivity.isConnectedWifi(ctx) -> {
LogUtil.d(TAG, "Connected to Wifi")
"WIFI"
}
Connectivity.isConnectedMobile(ctx) -> {
val type = try {
Connectivity.getNetworkType(ctx)
} catch (e: SecurityException) {
LogUtil.d(TAG, "READ_PHONE_STATE permission not granted: ${e.message}")
"MOBILE"
}
LogUtil.d(TAG, "Connected to Mobile Network: $type")
type
}
else -> "No Internet Connection"
}
}
}

View File

@ -0,0 +1,152 @@
package com.mob.utsmyanmar.viewmodel
import android.os.Handler
import android.os.Looper
import androidx.lifecycle.ViewModel
import com.mob.utsmyanmar.model.CardTransactionType
import com.utsmyanmar.checkxread.CheckXRead
import com.utsmyanmar.checkxread.checkcard.CheckCardResultX
import com.utsmyanmar.checkxread.model.CardDataX
import com.utsmyanmar.checkxread.readcard.ReadCardResultX
import com.utsmyanmar.checkxread.readcard.ReadCardX
import com.utsmyanmar.checkxread.sdk.SunmiSDK
import com.utsmyanmar.paylibs.model.PayDetail
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
@HiltViewModel
class CardReaderViewModel @Inject constructor() : ViewModel() {
companion object {
private val TAG = CardReaderViewModel::class.java.simpleName
}
private val mainThreadHandler = Handler(Looper.getMainLooper())
private var oneTimeFlag = false
private var cardTransactionType: CardTransactionType? = null
/*
* UI States
*/
private val _errorCode = MutableStateFlow("")
val errorCode = _errorCode.asStateFlow()
private val _cardData = MutableStateFlow<CardDataX?>(null)
val cardData = _cardData.asStateFlow()
private var _cardTypeData = MutableStateFlow<Int?>(null)
val cardTypeData = _cardTypeData.asStateFlow()
private val _payDetail = MutableStateFlow<PayDetail?>(null)
val payDetail = _payDetail.asStateFlow()
private val _checkCardAlertMsg =
MutableStateFlow<String?>(null)
val checkCardAlertMsg =
_checkCardAlertMsg.asStateFlow()
/*
* Transaction Type
*/
fun setCardTransactionType(
cardTransactionType: CardTransactionType
) {
this.cardTransactionType = cardTransactionType
}
fun getCardTransactionType(): CardTransactionType? {
return cardTransactionType
}
/*
* Check Card Process
*/
fun startCheckXProcess(
allType: Int,
timeOut: Int,
cardResultX: CheckCardResultX
) {
CheckXRead.getInstance()
.startCheckXProcess(
allType,
timeOut,
cardResultX
)
}
fun isReaderReady(): Boolean {
return SunmiSDK.getInstance().readCardOptV2 != null
}
fun cancelCheckXProcess() {
CheckXRead.getInstance()
.cancelCheckXProcess()
}
/*
* Read Card Process
*/
fun startReadXProcess(
readCardX: ReadCardX,
readCardResultX: ReadCardResultX
) {
CheckXRead.getInstance()
.startReadXProcess(
readCardX,
readCardResultX
)
}
/*
* Alert Message
*/
fun setCheckCardAlertMsg(
msg: String,
isAutoHide: Boolean
) {
_checkCardAlertMsg.value = msg
if (isAutoHide) {
Handler(Looper.getMainLooper()).postDelayed({
_checkCardAlertMsg.value = null
}, 5000)
}
}
fun resetUI() {
_checkCardAlertMsg.value = null
}
/*
* One Time Flag
*/
fun resetOneTimeFlag() {
oneTimeFlag = false
}
/*
* Cancel Card Checking
*/
fun cancelCheckCard() {
if (isReaderReady()) {
SunmiSDK.getInstance()
.cancelCheckCard()
}
}
fun setCardData(value: Int){
_cardTypeData.value = value
}
}

View File

@ -0,0 +1,429 @@
package com.mob.utsmyanmar.viewmodel
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.text.TextUtils
import com.utsmyanmar.baselib.emv.EmvParamOperation
import com.utsmyanmar.baselib.repo.Repository
import com.utsmyanmar.baselib.util.enums.EmvResultStatus
import com.utsmyanmar.baselib.viewModel.EmvBaseViewModel
import com.utsmyanmar.checkxread.util.CardTypeX
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.model.TradeData
import com.utsmyanmar.paylibs.model.enums.TransCVM
import com.utsmyanmar.paylibs.network.ISOSocket
import com.utsmyanmar.paylibs.reversal.ReversalAction
import com.utsmyanmar.paylibs.reversal.ReversalListener
import com.utsmyanmar.paylibs.system.SingleLiveEvent
import com.utsmyanmar.paylibs.transactions.TransactionsOperation
import com.utsmyanmar.paylibs.transactions.TransactionsOperationListener
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionType
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
import dagger.hilt.android.lifecycle.HiltViewModel
import sunmi.sunmiui.utils.LogUtil
import javax.inject.Inject
import com.mob.utsmyanmar.model.TransResultStatus
@HiltViewModel
class EmvTransactionProcessViewModel @Inject constructor(
private val repository: Repository,
emvParamOperation: EmvParamOperation
) : EmvBaseViewModel(repository, emvParamOperation), ProcessingTransaction {
private val transResult = SingleLiveEvent<TransResultStatus>()
override fun handleMsg(msg: Message) {
when (msg.what) {
PIN_CLICK_NUMBER -> {
showPasswordView(msg.arg1)
}
PIN_CLICK_CONFIRM -> {
if (!mPayDetail.PINCipher.isNullOrEmpty()) {
importPinInputStatus(0)
} else if (isOfflinePinEntered) {
importPinInputStatus(0)
} else {
importPinInputStatus(2)
}
if (
transType.value == TransactionsType.PRE_AUTH_COMPLETE ||
transType.value == TransactionsType.PRE_AUTH_VOID ||
transType.value == TransactionsType.REFUND
) {
emvResultStatus.postValue(EmvResultStatus.ON_NEXT_SCREEN)
}
}
PIN_CLICK_CANCEL -> {
importPinInputStatus(1)
}
PIN_ERROR -> {
increasedKSN()
importPinInputStatus(3)
handleTransactionFail(msg.arg1, PIN_PAD_FAILED)
}
PIN_CLICK_EMPTY -> {
emvResultStatus.postValue(EmvResultStatus.PIN_EMPTY)
}
EMV_APP_SELECT -> {
val candiNames = msg.obj as Array<String>
LogUtil.d(TAG, "CandiNames size:${candiNames.size}")
candiList.value = candiNames
emvResultStatus.postValue(EmvResultStatus.SELECT_APP)
}
EMV_FINAL_APP_SELECT -> {
importFinalAppSelectStatus(0)
}
EMV_CONFIRM_CARD_NO -> {
importCardNoStatus(0)
}
EMV_CERT_VERIFY -> {
importCertStatus(0)
}
EMV_SIGNATURE -> {
mPayDetail.setIsFreeSign(false)
mPayDetail.transCVM = TransCVM.SIGNATURE
importSignatureStatus(0)
}
EMV_SHOW_PIN_PAD -> {
startPinProcess()
}
EMV_ONLINE_PROCESS -> {
handleTransactionProcess()
}
EMV_ERROR -> {
handleTransactionFail(msg.arg1, msg.obj.toString())
}
EMV_TRY_AGAIN -> {
emvResultStatus.postValue(EmvResultStatus.TRY_AGAIN)
}
EMV_CONFIRM_CODE_VERIFY -> {
emvResultStatus.postValue(EmvResultStatus.CONFIRM_CODE_VERIFY)
}
EMV_SUCCESS_ONLINE -> {
transResult.postValue(TransResultStatus.SUCCESS)
}
EMV_FAILURE_ONLINE -> {
transResult.postValue(TransResultStatus.FAIL)
}
EMV_SUCCESS_OFFLINE -> {
transResult.postValue(TransResultStatus.OFFLINE_SUCCESS)
}
EMV_FAILURE_OFFLINE -> {
transResult.postValue(TransResultStatus.OFFLINE_FAILURE)
}
}
}
fun onCancelEmvTransaction() {
LogUtil.e(TAG, "Terminate Transaction : $mProcessStep")
when (mProcessStep) {
EMV_APP_SELECT -> importAppSelect(-1)
EMV_FINAL_APP_SELECT -> importFinalAppSelectStatus(-1)
EMV_CONFIRM_CARD_NO -> importCardNoStatus(1)
EMV_CERT_VERIFY -> importCertStatus(1)
PIN_ERROR -> importPinInputStatus(3)
EMV_ONLINE_PROCESS -> importOnlineProcessStatus(1)
EMV_SIGNATURE -> importSignatureStatus(1)
EMV_SHOW_PIN_PAD -> importPinInputStatus(1)
}
}
fun setPayDetail(payDetail: PayDetail) {
mPayDetail = payDetail
mTradeData = TradeData().apply {
setPayDetail(payDetail)
}
}
fun getPayDetail(): PayDetail {
return mPayDetail
}
fun resetProcessStepCount() {
mProcessStep = 0
}
override fun resetTransactionStatus() {
emvResultStatus.clear()
transResult.clear()
}
override fun getTransStatus(): SingleLiveEvent<TransResultStatus> {
return transResult
}
private fun isCardLessTrans(): Boolean {
return transType.value == TransactionsType.VOID ||
transType.value == TransactionsType.PRE_AUTH_COMPLETE_VOID ||
transType.value == TransactionsType.PRE_AUTH_VOID ||
transType.value == TransactionsType.REFUND
}
fun setPinPadCancel() {
importPinInputStatus(1)
}
override fun startOnlineProcess() {
isReversal = false
TransactionsOperation.getInstance()
.getStartOperation(mTradeData, transType.value)
.checkOperation(object : TransactionsOperationListener {
override fun onSuccess(tradeData: TradeData) {
val payDetailRes = tradeData.payDetail
payDetailRes.invoiceNo =
SystemParamsOperation.getInstance().incrementInvoiceNum
LogUtil.d(TAG, "Transaction Operation Success: ${payDetailRes.tradeAnswerCode}")
LogUtil.d(TAG, "Transaction Type: ${payDetailRes.transactionType}")
if (
TextUtils.equals(payDetailRes.tradeAnswerCode, RC_APPROVED_V1) ||
TextUtils.equals(payDetailRes.tradeAnswerCode, RC_APPROVED_V2)
) {
when (transType.value) {
TransactionsType.VOID -> processVoidDB(payDetailRes)
TransactionsType.REFUND -> processRefundDB(payDetailRes)
TransactionsType.PRE_AUTH_VOID -> processPreVoidDb(payDetailRes)
TransactionsType.PRE_AUTH_COMPLETE -> processPreCompDb(payDetailRes)
TransactionsType.PRE_AUTH_COMPLETE_VOID -> processPreCompVoidDb(payDetailRes)
else -> insertDB(payDetailRes)
}
payDetailResult.value = payDetailRes
if (isCardLessTrans()) {
transResult.value = TransResultStatus.SUCCESS
} else if (payDetailRes.cardType == CardTypeX.MANUAL.value) {
transResult.value = TransResultStatus.SUCCESS
}
} else {
payDetailResult.value = payDetailRes
if (isCardLessTrans()) {
transResult.value = TransResultStatus.FAIL
} else if (payDetailRes.cardType == CardTypeX.MANUAL.value) {
transResult.value = TransResultStatus.FAIL
}
}
}
override fun onReversal(tradeData: TradeData) {
LogUtil.d(TAG, "<<<< On Reversal >>>>")
isReversal = true
transResult.postValue(TransResultStatus.REVERSAL_PREPARE)
var reversalDelay = 15000
val delayValue = SystemParamsOperation.getInstance().reversalDelay
if (!delayValue.isNullOrEmpty()) {
reversalDelay = "${delayValue}000".toInt()
}
Handler(Looper.getMainLooper()).postDelayed({
val payDetailReversal = tradeData.payDetail
if (
payDetailReversal.transactionType == TransactionType.SALE ||
payDetailReversal.transactionType == TransactionType.VOID ||
payDetailReversal.transactionType == TransactionType.REFUND ||
payDetailReversal.transactionType == TransactionType.PRE_SALE ||
payDetailReversal.transactionType == TransactionType.CASH_ADVANCE ||
payDetailReversal.transactionType == TransactionType.PRE_SALE_CANCEL ||
payDetailReversal.transactionType == TransactionType.PRE_SALE_COMPLETE ||
payDetailReversal.transactionType == TransactionType.PRE_SALE_COMPLETE_VOID
) {
emvResultStatus.postValue(EmvResultStatus.REVERSAL_PROCESS)
transResult.postValue(TransResultStatus.REVERSAL_PROCESS)
callReversal(tradeData)
} else {
emvResultStatus.postValue(EmvResultStatus.REVERSAL_FAIL)
transResult.postValue(TransResultStatus.REVERSAL_FAIL)
}
}, reversalDelay.toLong())
}
override fun onError(message: String) {
LogUtil.e(TAG, "<<<< On Error >>>>")
LogUtil.e(TAG, "error message:$message")
if (TextUtils.equals(message, TRY_SECONDARY)) {
emvResultStatus.postValue(EmvResultStatus.SECONDARY)
transResult.postValue(TransResultStatus.SECONDARY)
startOnlineProcess()
} else {
importOnlineProcessStatus(1)
setNetWorkErrorDesc(message)
errorCodeMsg.value = message
emvResultStatus.postValue(EmvResultStatus.NETWORK_ERROR)
transResult.postValue(TransResultStatus.NETWORK_ERROR)
}
}
})
}
override fun insertDB(payResult: PayDetail) {
payResult.PINCipher = ""
repository.insertPayDetail(payResult)
}
override fun processVoidDB(payResult: PayDetail) {
mPayDetail.setIsCanceled(true)
updatePayDetail(mPayDetail)
repository.insertPayDetail(updateCurrentDateAndTime(payResult))
}
override fun processPreVoidDb(payResult: PayDetail) {
oldTransPayDetail.setIsCanceled(true)
updatePayDetail(oldTransPayDetail)
repository.insertPayDetail(updateCurrentDateAndTime(payResult))
}
override fun processPreCompDb(payResult: PayDetail) {
if (oldTransPayDetail.amount == payResult.amount) {
oldTransPayDetail.setIsCanceled(true)
updatePayDetail(oldTransPayDetail)
} else {
oldTransPayDetail.setIsCanceled(false)
updatePayDetail(oldTransPayDetail)
}
repository.insertPayDetail(updateCurrentDateAndTime(payResult))
}
override fun processPreCompVoidDb(payResult: PayDetail) {
mPayDetail.setIsCanceled(true)
updatePayDetail(mPayDetail)
repository.insertPayDetail(updateCurrentDateAndTime(payResult))
}
override fun processRefundDB(payResult: PayDetail) {
mPayDetail.setReturnGood(true)
updatePayDetail(mPayDetail)
repository.insertPayDetail(updateCurrentDateAndTime(payResult))
}
private fun handleTransactionFail(code: Int, description: String) {
Handler(Looper.getMainLooper()).postDelayed({
LogUtil.d(TAG, "On handleTransactionFail ::")
if (isReversal) {
transResult.value = TransResultStatus.REVERSAL_PREPARE
} else {
emvResultStatus.postValue(EmvResultStatus.ERROR)
transResult.value = TransResultStatus.FAIL
errorCodeMsg.postValue("$code:$description")
}
}, 500)
}
private fun handleTransactionProcess() {
val cardNo = mPayDetail.cardNo
if (cardNo.isNullOrEmpty()) {
getCardInfo()
}
if (
transType.value == TransactionsType.PRE_AUTH_COMPLETE ||
transType.value == TransactionsType.PRE_AUTH_VOID ||
transType.value == TransactionsType.REFUND
) {
emvResultStatus.postValue(EmvResultStatus.ON_NEXT_SCREEN)
} else {
emvResultStatus.postValue(EmvResultStatus.SUCCESS)
}
}
protected fun callReversal(tradeData: TradeData) {
if (transType.value == TransactionsType.VOID) {
tradeData.payDetail.icC55 = ""
}
ReversalAction.getInstance()
.setData(tradeData)
.enqueue()
.startReversal(object : ReversalListener {
override fun onSuccessReversal() {
importOnlineProcessStatus(0)
emvResultStatus.postValue(EmvResultStatus.REVERSAL_SUCCESS)
transResult.postValue(TransResultStatus.REVERSAL_SUCCESS)
}
override fun onNetworkFail(msg: String) {
importOnlineProcessStatus(0)
}
override fun onFailReversal(msg: String) {
importOnlineProcessStatus(0)
if (!isSecondCall) {
if (!TextUtils.equals(msg, REVERSAL)) {
emvResultStatus.postValue(EmvResultStatus.REVERSAL_SECONDARY)
transResult.postValue(TransResultStatus.REVERSAL_SECONDARY)
ISOSocket.getInstance().switchIp()
isSecondCall = true
callReversal(tradeData)
} else {
saveNeedReversal()
}
} else {
saveNeedReversal()
}
}
})
}
private fun saveNeedReversal() {
mPayDetail.setNeedReversal(true)
mPayDetail.transactionType = TransactionType.REVERSAL
mPayDetail.transType = REVERSAL
mPayDetail.pid = null
repository.insertPayDetail(mPayDetail)
emvResultStatus.postValue(EmvResultStatus.REVERSAL_FAIL)
transResult.postValue(TransResultStatus.REVERSAL_FAIL)
isSecondCall = false
}
companion object {
private val TAG = EmvTransactionProcessViewModel::class.java.simpleName
private const val TRY_SECONDARY = "TRY_SECONDARY"
private const val PIN_PAD_FAILED = "Pin pad failed"
private const val RC_APPROVED_V1 = "00"
private const val RC_APPROVED_V2 = "000"
private const val REVERSAL = "REVERSAL"
}
}

View File

@ -0,0 +1,25 @@
package com.mob.utsmyanmar.viewmodel
import com.mob.utsmyanmar.model.TransResultStatus
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.system.SingleLiveEvent
interface ProcessingTransaction {
fun resetTransactionStatus()
fun getTransStatus(): SingleLiveEvent<TransResultStatus>
fun startOnlineProcess()
fun insertDB(payResult: PayDetail)
fun processVoidDB(payResult: PayDetail)
fun processPreVoidDb(payResult: PayDetail)
fun processPreCompDb(payResult: PayDetail)
fun processPreCompVoidDb(payResult: PayDetail)
fun processRefundDB(payResult: PayDetail)
}

View File

@ -0,0 +1,669 @@
package com.mob.utsmyanmar.viewmodel
import android.graphics.Bitmap
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.mob.utsmyanmar.model.SettlementType
import com.mob.utsmyanmar.ui.processing_card.ProcessingCardState
import com.mob.utsmyanmar.ui.sending_to_host.ProcessingState
import com.utsmyanmar.baselib.network.model.sirius.SiriusRequest
import com.utsmyanmar.baselib.network.model.sirius.SiriusResponse
import com.utsmyanmar.baselib.repo.Repository
import com.utsmyanmar.ecr.data.TransType
import com.utsmyanmar.ecr.data.model.Transactions
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.print.printx.PrintXReceipt
import com.utsmyanmar.paylibs.print.printx.PrintXStatus
import com.utsmyanmar.paylibs.system.SingleLiveEvent
import com.utsmyanmar.paylibs.system.SystemDateTime
import com.utsmyanmar.paylibs.utils.AccountType
import com.utsmyanmar.paylibs.utils.PrintStatus
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
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.iso_utils.TransactionsType
import dagger.hilt.android.lifecycle.HiltViewModel
import io.reactivex.rxjava3.core.Observable
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import sunmi.sunmiui.utils.LogUtil
import javax.inject.Inject
@HiltViewModel
class SharedViewModel @Inject constructor(
private val repository: Repository
) : ViewModel() {
companion object {
private const val TAG = "SharedViewModel"
}
val transactionsType = SingleLiveEvent<TransactionsType>()
val settlementType = SingleLiveEvent<SettlementType>()
val accountType = SingleLiveEvent<AccountType>()
val amount = SingleLiveEvent<String>()
val totalAmount = SingleLiveEvent<Long>()
val cardNo = SingleLiveEvent<String>()
val processCode = SingleLiveEvent<String>()
val payDetail = SingleLiveEvent<PayDetail>()
val printStatus = SingleLiveEvent<PrintStatus>()
val merchantName = SingleLiveEvent<String>()
val transactionName = SingleLiveEvent<String>()
val payDetailList = SingleLiveEvent<List<PayDetail>>()
val isEcr = SingleLiveEvent<Boolean>()
val traceNo = SingleLiveEvent<String>()
val rrNNo = SingleLiveEvent<String>()
val approvalCode = SingleLiveEvent<String>()
val isEcrFinished = SingleLiveEvent<Boolean>()
val isEmv = SingleLiveEvent<Boolean>()
private val settlementStatus = MutableLiveData<Boolean>()
private val wavePayStatus = MutableLiveData<Boolean>()
val sendMsg = SingleLiveEvent<String>()
val qrData = SingleLiveEvent<String>()
val qrRefNum = SingleLiveEvent<String>()
val ecrCMD = SingleLiveEvent<TransType>()
val _transMenu = SingleLiveEvent<TransMenu?>()
val twoBtnLayout = MutableLiveData(0)
val oneBtnLayout = MutableLiveData(8)
private val reprintBtnLayout = MutableLiveData(8)
val isReprint = SingleLiveEvent<Boolean>()
var signBitmap: Bitmap? = null
val hostType = SingleLiveEvent<HostType>()
private val printReceiptButtons = MutableLiveData(0)
val printReceiptMsg = SingleLiveEvent<String>()
val reprintTransTypeMsg = SingleLiveEvent<String>()
val ecrTrans = SingleLiveEvent<Transactions>()
val _manualEntryStatus = MutableLiveData<Boolean>()
val fullVoidPreauthStatus = MutableLiveData<Boolean>()
val partialVoidPreauthStatus = MutableLiveData<Boolean>()
val printXStatus = SingleLiveEvent<PrintStatus>()
private val _errorFragmentMsg = SingleLiveEvent<String>()
private val _successFragmentMsg = SingleLiveEvent<String>()
private val _currencyText = SingleLiveEvent<String>()
val tapCardStatus = MutableLiveData(1)
val tapDeviceStatus = MutableLiveData(1)
val insertCardStatus = MutableLiveData(1)
val swipeCardStatus = MutableLiveData(0)
val countDownTxt = MutableLiveData<String>()
val mmqrLoading = MutableLiveData<Int>()
val isMMPay = MutableLiveData<Int>()
val isWavePay = MutableLiveData<Int>()
val mockData = SingleLiveEvent<String>()
val qrPayVisibility = MutableLiveData<Int>()
val loadingView = MutableLiveData(8)
val loadingMsg = SingleLiveEvent<String>()
private val isFallback = SingleLiveEvent<Boolean>()
private val _isCardDataExist = SingleLiveEvent<Boolean>()
private val _isAmountExist = SingleLiveEvent<Boolean>()
private var mPayDetail = PayDetail()
init {
setPrintStatus(PrintStatus.FIRST_PRINT)
isReprint.value = false
cardNo.value = ""
}
fun setSettlementStatus(status: Boolean) {
settlementStatus.value = status
}
fun getSettlementStatus(): MutableLiveData<Boolean> {
return settlementStatus
}
fun setWavePayStatus(status: Boolean) {
wavePayStatus.value = status
}
fun getWavePayStatus(): MutableLiveData<Boolean> {
return wavePayStatus
}
fun setEmvTrans(status: Boolean) {
isEmv.value = status
}
fun isEmvTrans(): SingleLiveEvent<Boolean> {
return isEmv
}
private fun getPayDetail(): PayDetail {
return mPayDetail
}
private fun cachePayDetail(payDetail: PayDetail) {
mPayDetail = payDetail
}
fun setPrintReceiptButtons(visible: Boolean) {
printReceiptButtons.value = if (visible) 0 else 8
}
fun postPrintReceiptButtons(visible: Boolean) {
printReceiptButtons.postValue(if (visible) 0 else 8)
}
fun getPrintReceiptButtons(): MutableLiveData<Int> {
return printReceiptButtons
}
fun getReprintBtnLayout(): MutableLiveData<Int> {
return reprintBtnLayout
}
fun setReprintBtnLayout(visible: Boolean) {
reprintBtnLayout.value = if (visible) 0 else 8
}
fun setPrintReceiptMsg(msg: String) {
printReceiptMsg.value = msg
}
fun postPrintReceiptMsg(msg: String) {
printReceiptMsg.postValue(msg)
}
fun setPrintStatus(printStatus: PrintStatus) {
this.printStatus.value = printStatus
}
fun postPrintStatus(printStatus: PrintStatus) {
this.printStatus.postValue(printStatus)
}
fun getPrintStatusEvent(): SingleLiveEvent<PrintStatus> {
return printStatus
}
fun setIsFallback(status: Boolean) {
isFallback.value = status
}
fun getIsFallback(): SingleLiveEvent<Boolean> {
return isFallback
}
fun setCardDataExist(exist: Boolean) {
_isCardDataExist.value = exist
}
fun getCardDataExist(): SingleLiveEvent<Boolean> {
return _isCardDataExist
}
fun setAmountExist(exist: Boolean) {
_isAmountExist.value = exist
}
fun getAmountExist(): SingleLiveEvent<Boolean> {
return _isAmountExist
}
fun getManualEntryStatus(): MutableLiveData<Boolean> {
return _manualEntryStatus
}
fun setManualEntryStatus(status: Boolean) {
_manualEntryStatus.value = status
}
fun getErrorFragmentMsg(): SingleLiveEvent<String> {
return _errorFragmentMsg
}
fun set_errorFragmentMsg(msg: String) {
_errorFragmentMsg.value = msg
}
fun getSuccessFragmentMsg(): SingleLiveEvent<String> {
return _successFragmentMsg
}
fun set_currencyText(msg: String) {
_currencyText.value = msg
}
fun get_currencyText(): SingleLiveEvent<String> {
return _currencyText
}
fun set_successFragmentMsg(msg: String) {
_successFragmentMsg.value = msg
}
fun getTransMenu(): SingleLiveEvent<TransMenu?> {
return _transMenu
}
fun setTransMenu(transMenu: TransMenu?) {
_transMenu.value = transMenu
}
fun loadingMsg(msg: String) {
loadingView.value = 0
loadingMsg.value = msg
}
fun dismissLoadingMsg() {
loadingView.value = 8
loadingMsg.value = ""
}
fun getParams(siriusRequest: SiriusRequest): Observable<SiriusResponse> {
return repository.getParams(siriusRequest)
}
private fun printReceipt(isMerchantCopy: Boolean) {
PrintXReceipt.getInstance()
.printSmileReceipt(payDetail.value, isMerchantCopy, object : PrintXStatus {
override fun onSuccess() {
if (isMerchantCopy) {
if (!SystemParamsOperation.getInstance().demoStatus) {
if (SystemParamsOperation.getInstance().printISOStatus) {
PrintXReceipt.getInstance()
.printSmileISoReceipt(payDetail.value)
}
}
postPrintStatus(PrintStatus.FIRST_PRINT_DONE)
} else {
setPrintStatus(PrintStatus.SECOND_PRINT_DONE)
}
}
override fun onFailure() {
LogUtil.d(TAG, "Print Status Result Failure!")
printXStatus.postValue(PrintStatus.EMPTY_PAPER_ROLL)
postPrintReceiptButtons(true)
}
})
}
fun startPrintReceipt(isFirstPrint: Boolean) {
payDetail.value?.let {
cachePayDetail(it)
}
PrintXReceipt.getInstance()
.printSmileReceipt(getPayDetail(), isFirstPrint, object : PrintXStatus {
override fun onSuccess() {
if (isFirstPrint) {
if (!SystemParamsOperation.getInstance().demoStatus) {
if (SystemParamsOperation.getInstance().printISOStatus) {
PrintXReceipt.getInstance()
.printSmileISoReceipt(getPayDetail())
}
}
postPrintStatus(PrintStatus.FIRST_PRINT_DONE)
if (isEcr.value == true &&
SystemParamsOperation.getInstance()
.isAutoPrintCustomerCopy
) {
printReceiptButtons.postValue(8)
} else {
printReceiptButtons.postValue(0)
}
} else {
postPrintStatus(PrintStatus.SECOND_PRINT_DONE)
}
}
override fun onFailure() {
LogUtil.d(TAG, "Print Status Result Failure!")
if (isFirstPrint) {
setPrintStatus(PrintStatus.EMPTY_PAPER_ROLL_FIRST)
} else {
setPrintStatus(PrintStatus.EMPTY_PAPER_ROLL_SECOND)
}
postPrintReceiptButtons(true)
}
})
}
fun startPrintProcess() {
LogUtil.d(TAG, "Print status : ${getPrintStatusEvent().value}")
when (getPrintStatusEvent().value) {
PrintStatus.FIRST_PRINT -> {
printReceipt(true)
postPrintReceiptMsg("Printing Receipt for Merchant")
postPrintReceiptButtons(false)
}
PrintStatus.SECOND_PRINT -> {
printReceipt(false)
postPrintReceiptMsg("Printing Receipt for Customer")
}
PrintStatus.NOT_PRINT -> {
}
else -> {}
}
}
fun startPrintProcessSettlement() {
val detail = payDetail.value ?: return
PrintXReceipt.getInstance()
.printSmileSettlementReport(detail, object : PrintXStatus {
override fun onSuccess() {
}
override fun onFailure() {
}
})
}
fun getLastThreeTransactions(): LiveData<List<PayDetail>> {
return repository.getLastThreeTransactions()
}
fun getSettlementRecords(): LiveData<List<PayDetail>> {
return repository.getSettlementPOS()
}
fun getReversalTransaction(voucherNo: String): LiveData<PayDetail> {
return repository.getReversalTransaction(voucherNo)
}
fun updatePayDetail(payDetail: PayDetail) {
repository.updatePayDetail(payDetail)
}
fun insertPayDetail(payDetail: PayDetail) {
repository.insertPayDetail(payDetail)
}
fun saveMockHostResultForTesting() {
when (transactionsType.value) {
TransactionsType.SETTLEMENT -> saveMockSettlementForTesting()
TransactionsType.VOID -> saveMockApprovedVoidForTesting()
TransactionsType.REFUND -> saveMockApprovedRefundForTesting()
else -> saveMockApprovedSaleForVoidTesting()
}
}
fun saveMockApprovedSaleForVoidTesting() {
val detail = payDetail.value ?: return
val systemParams = SystemParamsOperation.getInstance()
val mockTraceNo = systemParams.incrementSerialNum
val mockInvoiceNo = systemParams.incrementInvoiceNum
val mockApprovalCode = mockTraceNo.takeLast(6).padStart(6, '0')
val mockReferenceNo = buildString {
append(SystemDateTime.getYYMMDD())
append(SystemDateTime.getHHmmss())
}.takeLast(12)
detail.voucherNo = mockTraceNo
detail.invoiceNo = mockInvoiceNo
detail.referNo = mockReferenceNo
detail.approvalCode = mockApprovalCode
detail.authNo = mockApprovalCode
detail.tradeAnswerCode = "00"
detail.tradeResultDes = "MOCK APPROVED"
detail.transactionType = TransactionType.SALE
detail.transType = TransactionsType.SALE.name
detail.isCanceled = false
detail.isSettle = false
detail.isNeedReversal = false
detail.isReturnGood = false
detail.TradeDate = SystemDateTime.getMMDD()
detail.TradeTime = SystemDateTime.getHHmmss()
detail.tradeDateAndTime = SystemDateTime.getMMDDhhmmss()
detail.tradeDateTime = SystemDateTime.getYYMMDDhhmmss()
detail.transDate = SystemDateTime.getTodayDateFormat()
detail.transTime = SystemDateTime.getTodayTimeFormat()
repository.insertPayDetail(detail)
payDetail.value = detail
traceNo.value = mockTraceNo
rrNNo.value = mockReferenceNo
approvalCode.value = mockApprovalCode
}
fun saveMockApprovedVoidForTesting() {
val originalSale = payDetail.value ?: return
originalSale.isCanceled = true
repository.updatePayDetail(originalSale)
val systemParams = SystemParamsOperation.getInstance()
val mockTraceNo = systemParams.incrementSerialNum
val mockInvoiceNo = systemParams.incrementInvoiceNum
val mockApprovalCode = mockTraceNo.takeLast(6).padStart(6, '0')
val mockReferenceNo = buildString {
append(SystemDateTime.getYYMMDD())
append(SystemDateTime.getHHmmss())
}.takeLast(12)
val voidDetail = PayDetail().apply {
merchantName = originalSale.merchantName
merchantNo = originalSale.merchantNo
terminalNo = originalSale.terminalNo
CardNo = originalSale.CardNo
cardType = originalSale.cardType
EXPDate = originalSale.EXPDate
cardHolderName = originalSale.cardHolderName
processCode = TransactionsType.VOID.processCode
amount = originalSale.amount
transPlatform = originalSale.transPlatform
transactionType = TransactionType.VOID
transType = TransactionsType.VOID.name
currencyCode = originalSale.currencyCode
currency = originalSale.currency
batchNo = originalSale.batchNo
voucherNo = mockTraceNo
invoiceNo = mockInvoiceNo
referNo = mockReferenceNo
approvalCode = mockApprovalCode
authNo = mockApprovalCode
accountType = originalSale.accountType
tradeAnswerCode = "00"
tradeResultDes = "MOCK VOID APPROVED"
TradeDate = SystemDateTime.getMMDD()
TradeTime = SystemDateTime.getHHmmss()
tradeDateAndTime = SystemDateTime.getMMDDhhmmss()
tradeDateTime = SystemDateTime.getYYMMDDhhmmss()
transDate = SystemDateTime.getTodayDateFormat()
transTime = SystemDateTime.getTodayTimeFormat()
originalPOSNum = originalSale.voucherNo
originalReferNo = originalSale.referNo
originalAuthNo = originalSale.approvalCode
originalBathNo = originalSale.batchNo
originalTransDate = originalSale.TradeDate
originalAmount = originalSale.amount
isCanceled = false
isSettle = false
isNeedReversal = false
isReturnGood = false
}
repository.insertPayDetail(voidDetail)
payDetail.value = voidDetail
traceNo.value = mockTraceNo
rrNNo.value = mockReferenceNo
approvalCode.value = mockApprovalCode
}
fun saveMockApprovedRefundForTesting() {
val refundDetail = payDetail.value ?: return
val systemParams = SystemParamsOperation.getInstance()
val mockTraceNo = systemParams.incrementSerialNum
val mockInvoiceNo = systemParams.incrementInvoiceNum
val mockApprovalCode = mockTraceNo.takeLast(6).padStart(6, '0')
val refundRrn = rrNNo.value?.trim().orEmpty()
refundDetail.voucherNo = mockTraceNo
refundDetail.invoiceNo = mockInvoiceNo
refundDetail.referNo = refundRrn
refundDetail.approvalCode = mockApprovalCode
refundDetail.authNo = mockApprovalCode
refundDetail.tradeAnswerCode = "00"
refundDetail.tradeResultDes = "MOCK REFUND APPROVED"
refundDetail.transactionType = TransactionType.REFUND
refundDetail.transType = TransactionsType.REFUND.name
refundDetail.isCanceled = false
refundDetail.isSettle = false
refundDetail.isNeedReversal = false
refundDetail.isReturnGood = false
refundDetail.TradeDate = SystemDateTime.getMMDD()
refundDetail.TradeTime = SystemDateTime.getHHmmss()
refundDetail.tradeDateAndTime = SystemDateTime.getMMDDhhmmss()
refundDetail.tradeDateTime = SystemDateTime.getYYMMDDhhmmss()
refundDetail.transDate = SystemDateTime.getTodayDateFormat()
refundDetail.transTime = SystemDateTime.getTodayTimeFormat()
repository.insertPayDetail(refundDetail)
payDetail.value = refundDetail
traceNo.value = mockTraceNo
rrNNo.value = refundRrn
approvalCode.value = mockApprovalCode
}
fun saveMockSettlementForTesting() {
val systemParams = SystemParamsOperation.getInstance()
val mockTraceNo = systemParams.incrementSerialNum
val mockInvoiceNo = systemParams.incrementInvoiceNum
val settlementDetail = PayDetail().apply {
merchantNo = systemParams.merchantId
merchantName = systemParams.merchantName
terminalNo = systemParams.terminalId
voucherNo = mockTraceNo
invoiceNo = mockInvoiceNo
batchNo = systemParams.systemParamsSettings.batchNumStart
processCode = TransactionsType.SETTLEMENT.processCode
transactionType = TransactionType.SETTLEMENT
transType = TransactionsType.SETTLEMENT.name
currencyCode = systemParams.currencyType.removed0CurrencyCode
tradeAnswerCode = "00"
tradeResultDes = "MOCK SETTLEMENT APPROVED"
TradeDate = SystemDateTime.getMMDD()
TradeTime = SystemDateTime.getHHmmss()
tradeDateAndTime = SystemDateTime.getMMDDhhmmss()
tradeDateTime = SystemDateTime.getYYMMDDhhmmss()
transDate = SystemDateTime.getTodayDateFormat()
transTime = SystemDateTime.getTodayTimeFormat()
isSettle = false
isNeedReversal = false
isCanceled = false
}
payDetail.value = settlementDetail
traceNo.value = mockTraceNo
}
fun enableCardStatusIcon(
tapCard: Boolean,
tapDevice: Boolean,
insertCard: Boolean,
swipeCard: Boolean
) {
tapCardStatus.value = if (tapCard) 1 else 0
tapDeviceStatus.value = if (tapDevice) 1 else 0
insertCardStatus.value = if (insertCard) 1 else 0
swipeCardStatus.value = if (swipeCard) 1 else 0
}
fun resetParamsMain() {
isEcrFinished.postValue(true)
isEcr.postValue(false)
setAmountExist(false)
setCardDataExist(false)
setTransMenu(null)
}
fun updateButtonStatus() {
setSettlementStatus(
SystemParamsOperation.getInstance().settlementStatus
)
setWavePayStatus(
SystemParamsOperation.getInstance().wavePayStatus
)
}
}

View File

@ -0,0 +1,393 @@
package com.mob.utsmyanmar.viewmodel
import android.text.TextUtils
import androidx.lifecycle.ViewModel
import com.mob.utsmyanmar.model.TransResultStatus
import com.mob.utsmyanmar.ui.sending_to_host.ProcessingState
import com.utsmyanmar.baselib.repo.Repository
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.model.TradeData
import com.utsmyanmar.paylibs.network.ISOSocket
import com.utsmyanmar.paylibs.reversal.ReversalAction
import com.utsmyanmar.paylibs.reversal.ReversalListener
import com.utsmyanmar.paylibs.system.SystemDateTime
import com.utsmyanmar.paylibs.transactions.TransactionsOperation
import com.utsmyanmar.paylibs.transactions.TransactionsOperationListener
import com.utsmyanmar.paylibs.utils.PrintStatus
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
@HiltViewModel
class TransProcessViewModel @Inject constructor(
private val repository: Repository
) : ViewModel(), ProcessingTransaction {
companion object {
private const val RC_APPROVED_V1 = "00"
private const val RC_APPROVED_V2 = "000"
private const val TRY_SECONDARY = "TRY_SECONDARY"
}
private var pan: String = ""
private lateinit var tradeData: TradeData
var payDetail: PayDetail? = null
private var oldTransPayDetail: PayDetail? = null
private var isSecondCall = false
private var isThirdCall = false
/*
* States
*/
private val _transResultStatus =
MutableStateFlow<TransResultStatus?>(null)
val transResultStatus =
_transResultStatus.asStateFlow()
private val _transType = MutableStateFlow<TransactionsType?>(null)
val transType =_transType.asStateFlow()
fun setTransType(type: TransactionsType?) {
_transType.value = type
}
fun resetTransactionResultStatus() {
_transResultStatus.value = null
}
private val _printStatus =
MutableStateFlow(PrintStatus.FIRST_PRINT)
val printStatus =
_printStatus.asStateFlow()
private val _payDetailResult =
MutableStateFlow<PayDetail?>(null)
val payDetailResult =
_payDetailResult.asStateFlow()
private val _errorMessage =
MutableStateFlow<String?>(null)
val errorMessage =
_errorMessage.asStateFlow()
/*
* Setup
*/
fun setTradeData(tradeData: TradeData) {
this.tradeData = tradeData
payDetail = tradeData.payDetail
pan = payDetail?.cardNo ?: ""
}
fun getTradeData(): TradeData {
return tradeData
}
fun updatePayDetail(payDetail: PayDetail) {
this.payDetail = payDetail
tradeData = TradeData().apply {
this.payDetail = payDetail
}
}
fun setOldTransPayDetail(payDetail: PayDetail) {
oldTransPayDetail = payDetail
}
fun setNewPayDetail(payDetail: PayDetail) {
this.payDetail = payDetail
tradeData = TradeData()
tradeData.payDetail = payDetail
}
private val _state = MutableStateFlow(ProcessingState())
val state = _state.asStateFlow()
/*
* Transaction
*/
override fun startOnlineProcess() {
_state.value = _state.value.copy("Processing")
TransactionsOperation.getInstance()
.getStartOperation(
tradeData,
transType.value
)
.checkOperation(object : TransactionsOperationListener {
override fun onSuccess(tradeData: TradeData) {
val payDetailRes =
tradeData.payDetail
if (
TextUtils.equals(
payDetailRes.tradeAnswerCode,
RC_APPROVED_V1
) ||
TextUtils.equals(
payDetailRes.tradeAnswerCode,
RC_APPROVED_V2
)
) {
payDetailRes.invoiceNo =
SystemParamsOperation
.getInstance()
.incrementInvoiceNum
when (transType.value) {
TransactionsType.VOID -> {
processVoidDB(payDetailRes)
}
TransactionsType.REFUND -> {
processRefundDB(payDetailRes)
}
TransactionsType.PRE_AUTH_VOID -> {
processPreVoidDb(payDetailRes)
}
TransactionsType.PRE_AUTH_COMPLETE -> {
processPreCompDb(payDetailRes)
}
TransactionsType.PRE_AUTH_COMPLETE_VOID -> {
processPreCompVoidDb(payDetailRes)
}
else -> {
insertDB(payDetailRes)
}
}
_payDetailResult.value =
payDetailRes
_transResultStatus.value =
TransResultStatus.SUCCESS
} else {
_payDetailResult.value =
payDetailRes
_transResultStatus.value =
TransResultStatus.FAIL
}
}
override fun onReversal(
tradeData: TradeData
) {
_transResultStatus.value =
TransResultStatus.REVERSAL_PREPARE
_state.value = _state.value.copy("Preparing Reversal")
callReversal(tradeData)
}
override fun onError(message: String) {
if (message == TRY_SECONDARY) {
_transResultStatus.value =
TransResultStatus.SECONDARY
startOnlineProcess()
} else {
_errorMessage.value = message
_transResultStatus.value =
TransResultStatus.ERROR
}
}
})
}
/*
* Database
*/
override fun insertDB(payResult: PayDetail) {
payDetail?.pinCipher = ""
repository.insertPayDetail(
payDetail
)
}
override fun processVoidDB(payResult: PayDetail) {
payDetail?.isCanceled = true
payDetail?.let {
repository.updatePayDetail(it)
}
repository.insertPayDetail(
updateCurrentDateAndTime(payResult)
)
}
override fun processRefundDB(payResult: PayDetail) {
oldTransPayDetail?.apply {
isReturnGood = true
isCanceled = true
repository.updatePayDetail(this)
}
repository.insertPayDetail(
updateCurrentDateAndTime(payResult)
)
}
override fun processPreVoidDb(payResult: PayDetail) {
oldTransPayDetail?.apply {
isCanceled = true
repository.updatePayDetail(oldTransPayDetail!!)
}
repository.insertPayDetail(updateCurrentDateAndTime(payResult))
}
override fun processPreCompDb(payResult: PayDetail) {
if (oldTransPayDetail?.getAmount() == payResult.getAmount()) {
oldTransPayDetail?.apply {
isCanceled = true
repository.updatePayDetail(this)
}
} else {
oldTransPayDetail?.apply {
isCanceled = false
repository.updatePayDetail(this)
}
}
repository.insertPayDetail(updateCurrentDateAndTime(payResult))
}
override fun processPreCompVoidDb(payResult: PayDetail) {
payDetail?.apply {
isCanceled = true
repository.updatePayDetail(this)
}
repository.insertPayDetail(updateCurrentDateAndTime(payResult))
}
/*
* Reversal
*/
private fun callReversal(tradeData: TradeData) {
_state.value = _state.value.copy("Processing Reversal")
payDetail = tradeData.payDetail
ReversalAction.getInstance()
.setData(tradeData)
.enqueue()
.startReversal(object : ReversalListener {
override fun onSuccessReversal() {
_transResultStatus.value =
TransResultStatus.REVERSAL_SUCCESS
payDetail?.let {
repository.insertPayDetail(it)
}
}
override fun onNetworkFail(msg: String) {
SystemParamsOperation
.getInstance()
.setSecondHostEnable(true)
if (
SystemParamsOperation
.getInstance()
.isSecondHostEnabled
) {
if (!isSecondCall) {
_transResultStatus.value =
TransResultStatus.REVERSAL_SECONDARY
ISOSocket.getInstance()
.switchIp()
isSecondCall = true
callReversal(tradeData)
} else {
_transResultStatus.value =
TransResultStatus.REVERSAL_FAIL
}
} else {
_transResultStatus.value =
TransResultStatus.REVERSAL_FAIL
}
}
override fun onFailReversal(msg: String) {
_transResultStatus.value =
TransResultStatus.REVERSAL_FAIL
}
})
}
/*
* Utils
*/
private fun updateCurrentDateAndTime(
payDetail: PayDetail
): PayDetail {
payDetail.tradeDate =
SystemDateTime.getMMDD()
payDetail.tradeTime =
SystemDateTime.getHHmmss()
return payDetail
}
override fun resetTransactionStatus() {
_transResultStatus.value = null
}
override fun getTransStatus() = TODO()
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,2C6.5,2 2,6.5 2,12s4.5,10 10,10s10,-4.5 10,-10v-1c-0.6,0.9 -1.2,1.7 -1.7,2.4c-0.4,2.2 -1.6,4.1 -3.3,5.4c-0.1,-0.3 -0.2,-0.7 -0.3,-1c-0.1,-0.4 -0.2,-0.8 -0.4,-1.2c-0.1,-0.4 -0.2,-0.8 -0.4,-1.1c-0.1,-0.1 -0.3,-0.2 -0.5,-0.3c-0.7,-0.2 -1.6,0.1 -2.1,-0.6c-0.2,-0.3 -0.2,-0.6 -0.1,-1c0.2,-0.3 0.3,-0.6 0.5,-0.9c0.2,-0.4 0.5,-0.9 0.6,-1.4c-0.8,-1.2 -1.6,-2.6 -2,-3.9h-0.1c-0.1,0 -0.1,0 -0.2,-0.1c-0.2,-0.2 -0.3,-0.7 -0.2,-1c0,-0.5 0.3,-0.8 0.2,-1.3c0,-0.1 -0.1,-0.9 -0.1,-0.9c-0.3,0 -0.8,0 -0.7,-0.5V3.5H12h0.5c0.2,-0.6 0.6,-1.1 0.9,-1.5H12zM18,2c-2.2,0 -4,1.8 -4,4s4,7 4,7s4,-4.8 4,-7S20.2,2 18,2zM18,4.5c0.8,0 1.5,0.7 1.5,1.5S18.8,7.5 18,7.5S16.5,6.8 16.5,6S17.2,4.5 18,4.5zM8,5.1c0.4,0 0.7,0 1,0.1s0.6,0.3 0.8,0.5s0.5,0.5 0.5,0.8c0,0.1 0,0.2 -0.1,0.2C10.1,6.8 10,6.8 9.9,6.8c-0.3,0 -0.6,0 -0.8,-0.1C9,6.6 8.8,6.4 8.6,6.3C8.1,6.1 7.2,7.4 7.1,7.8C7,8.1 7,8.8 7.5,8.9c0.3,0 1,-0.6 1.2,-0.8C8.9,8 9,7.9 9.2,7.8c0.8,-0.1 1.4,0.6 1.6,1.3C11,9.9 9.6,10.5 9,10.7c-0.2,0.1 -0.3,-0.1 -0.5,0c-0.5,0.2 -1.1,0.9 -1.1,1.4s-0.1,1 -0.2,1.5c-0.1,0 -0.2,-0.1 -0.2,-0.1v-0.2c0,-0.3 -0.1,-0.6 -0.4,-0.8c-0.1,0 -0.1,-0.1 -0.2,-0.1c-0.3,-0.1 -0.6,-0.4 -0.9,-0.1c-0.2,0.2 -0.4,0.5 -0.4,0.8c0,0.1 0,0.2 0.1,0.3c0.2,0.1 0.4,0 0.6,0c0.1,0 0.2,0.2 0.3,0.3c0.2,0.3 0.3,0.8 0.7,0.8h0.7h1.3c0.3,0.1 0.8,0.2 1,0.4c0.1,0.2 0.1,0.4 0.2,0.6c0.4,0.5 1.1,0.5 1.7,0.7c0.2,0.1 0.3,0.2 0.3,0.4c0,0.3 -0.1,0.7 -0.2,1s-0.2,0.7 -0.4,0.9s-0.4,0.3 -0.6,0.4c-0.4,0.2 -0.6,0.6 -0.8,0.9c0,0 -0.1,0.2 -0.2,0.3c-0.8,-0.2 -1.5,-0.5 -2.2,-1v-0.2c-0.1,-0.4 -0.2,-0.7 -0.3,-1c-0.2,-0.5 -0.5,-1.1 -0.6,-1.6c0,-0.5 0.1,-1 -0.2,-1.4c-0.3,-0.5 -1.1,-0.5 -1.6,-0.8c-0.4,-0.4 -0.9,-0.8 -1.3,-1.3V12c0,-2.7 1.3,-5.1 3.3,-6.7C7.3,5.2 7.6,5.1 8,5.1z"
android:fillColor="#000000"/>
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="800dp"
android:height="800dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M512.8,1002.4c-65.6,0 -129.6,-12.8 -189.6,-38.4 -58.4,-24.8 -110.4,-60 -155.2,-104.8S88,761.6 64,703.2c-25.6,-60 -38.4,-124 -38.4,-190.4 0,-65.6 12.8,-130.4 38.4,-190.4 24,-57.6 59.2,-110.4 104,-155.2s96.8,-80 155.2,-104.8C383.2,36.8 447.2,24 512.8,24s129.6,12.8 189.6,38.4c58.4,24.8 110.4,60 155.2,104.8 44.8,44.8 80,96.8 104.8,155.2 25.6,60 38.4,124 38.4,190.4 0,65.6 -12.8,130.4 -38.4,190.4 -24.8,58.4 -60,110.4 -104.8,155.2 -44.8,44.8 -96.8,80 -155.2,104.8 -60,25.6 -123.2,39.2 -189.6,39.2zM512.8,71.2c-242.4,0 -440,198.4 -440,441.6s197.6,441.6 440.8,441.6c243.2,0 440.8,-198.4 440.8,-441.6S756,71.2 512.8,71.2z"
android:fillColor="#000000"/>
<path
android:pathData="M512,811.2c-5.6,0 -12,-2.4 -16,-6.4 -1.6,-0.8 -2.4,-1.6 -4,-3.2L280,589.6c-9.6,-9.6 -9.6,-24 0,-33.6 4.8,-4.8 10.4,-7.2 16.8,-7.2s12,2.4 16.8,7.2L488,731.2V236c0,-12.8 10.4,-24 24,-24 12.8,0 24,10.4 24,24v495.2l175.2,-175.2c4.8,-4.8 10.4,-7.2 16.8,-7.2s12,2.4 16.8,7.2c9.6,9.6 9.6,24 0,33.6l-212,212c-1.6,1.6 -2.4,2.4 -4,3.2 -5.6,4 -11.2,6.4 -16.8,6.4z"
android:fillColor="#000000"/>
</vector>

View File

@ -0,0 +1,37 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="14dp"
android:height="14dp"
android:viewportWidth="14"
android:viewportHeight="14">
<path
android:pathData="M9.078,4.669L4.669,9.078M4.669,4.669L9.078,9.078"
android:strokeAlpha="0.8"
android:strokeLineJoin="round"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#6F0D1E"
android:strokeLineCap="round"/>
<path
android:pathData="M6.873,12.997C10.255,12.997 12.997,10.255 12.997,6.873C12.997,3.491 10.255,0.75 6.873,0.75C3.491,0.75 0.75,3.491 0.75,6.873C0.75,10.255 3.491,12.997 6.873,12.997Z"
android:strokeAlpha="0.8"
android:strokeLineJoin="round"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#6F0D1E"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
<path
android:fillColor="#FF000000"
android:pathData="m15.3,13.52c-4.61,2.66 -6.21,8.52 -3.54,13.14L32.21,62.09 15.73,71.6c-4.61,2.66 -6.18,8.51 -3.52,13.12l5.59,9.68 8.03,-4.64c3.18,4.61 7.29,10.53 11,15.48 2.79,3.72 3.86,6.62 7.09,8.89 1.62,1.13 3.79,1.7 5.91,1.63 2.11,-0.07 4.28,-0.6 7.03,-1.47a3.88,3.88 0,0 0,0.44 -0.15L104.38,92.85c2.94,-1.33 5.3,-2.31 7.09,-3.05 1.79,-0.74 2.7,-0.95 4.13,-1.93 0.36,-0.25 0.86,-0.47 1.46,-1.65 0.3,-0.59 0.6,-1.57 0.4,-2.58 -0.19,-1.01 -0.81,-1.8 -1.29,-2.23 -0.95,-0.86 -1.46,-0.9 -1.88,-1.01 -0.42,-0.11 -0.73,-0.15 -1.06,-0.19 -1.32,-0.17 -2.92,-0.2 -5.33,-0.32L95.76,79.25C92.9,79.1 88.82,75.69 86.64,72.48L77.95,59.68 83.07,56.73 77.48,47.04c-2.66,-4.61 -8.53,-6.16 -13.15,-3.5l-15.46,8.92 -20.45,-35.43c-2.66,-4.61 -8.51,-6.18 -13.12,-3.52zM32.55,85.89 L71.21,63.58 80.23,76.84c2.98,4.38 7.96,9.79 15.14,10.16l2.78,0.15 -43.6,19.75c-0.03,0.01 -0.04,-0.01 -0.07,0 -2.38,0.74 -4.04,1.09 -4.91,1.12 -0.89,0.03 -0.87,-0.04 -1.15,-0.24 -0.58,-0.4 -2.22,-2.97 -5.36,-7.16C39.67,96.11 35.71,90.46 32.55,85.89z"/>
</vector>

View File

@ -0,0 +1,20 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="16" android:viewportWidth="16" android:width="24dp">
<path android:fillColor="#00000000" android:pathData="M4.573,5.227L1.96,7.84L4.573,10.453M11.107,5.227L13.72,7.84L11.107,10.453M9.147,2.613L6.533,13.067" android:strokeColor="#6F0D1E" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2"/>
</vector>

View File

@ -0,0 +1,52 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="800dp"
android:height="800dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M0,0h48v48h-48z"
android:fillColor="#ffffff"
android:fillAlpha="0.01"/>
<path
android:pathData="M34,12V20V21C31.045,21 28.389,22.282 26.559,24.32C24.968,26.091 24,28.432 24,31C24,31.579 24.049,32.146 24.144,32.698C24.658,35.705 26.514,38.253 29.074,39.705C26.412,40.51 22.878,41 19,41C10.716,41 4,38.761 4,36V28V20V12"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M34,12C34,14.761 27.284,17 19,17C10.716,17 4,14.761 4,12C4,9.239 10.716,7 19,7C27.284,7 34,9.239 34,12Z"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#CD2029"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M4,28C4,30.761 10.716,33 19,33C20.807,33 22.539,32.894 24.144,32.698"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M4,20C4,22.761 10.716,25 19,25C21.756,25 24.339,24.752 26.559,24.32"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M44,31C44,36.523 39.523,41 34,41C32.209,41 30.528,40.529 29.074,39.705C26.514,38.253 24.658,35.705 24.144,32.698C24.049,32.146 24,31.579 24,31C24,28.432 24.968,26.091 26.559,24.32C28.389,22.282 31.045,21 34,21C39.523,21 44,25.477 44,31Z"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#CD2029"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M34,27V31M37.464,29L34,31M37.464,33L34,31M34,35V31M30.536,33L34,31M30.536,29L34,31"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,28 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="800dp"
android:height="800dp"
android:viewportWidth="192"
android:viewportHeight="192">
<path
android:pathData="M96.01,63.63c-3.32,0 -6.12,2.71 -6.12,6.1a6.12,6.12 0,0 0,6.12 6.12c3.38,0 6.1,-2.8 6.1,-6.12s-2.78,-6.1 -6.1,-6.1z"
android:strokeAlpha="0.98"
android:strokeLineJoin="round"
android:strokeWidth="3.78002"
android:fillColor="#000000"
android:strokeColor="#000000"
android:fillAlpha="0.98"
android:strokeLineCap="round"/>
<path
android:pathData="M83.43,122.37h25.13M87.48,90.96h8.54v31.41"
android:strokeLineJoin="round"
android:strokeWidth="12"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M54,46h84M60,22h72c5.54,0 10,4.46 10,10v128c0,5.54 -4.46,10 -10,10L60,170c-5.54,0 -10,-4.46 -10,-10L50,32c0,-5.54 4.46,-10 10,-10zM54,146h84"
android:strokeLineJoin="round"
android:strokeWidth="12"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
</vector>

View File

@ -0,0 +1,20 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="12" android:viewportWidth="12" android:width="24dp">
<path android:fillColor="#6F0D1E" android:fillType="evenOdd" android:pathData="M11.025,-0C11.22,-0 11.407,0.077 11.545,0.215C11.683,0.353 11.76,0.54 11.76,0.735L11.76,11.025C11.76,11.22 11.683,11.407 11.545,11.545C11.407,11.683 11.22,11.76 11.025,11.76C10.83,11.76 10.643,11.683 10.505,11.545C10.367,11.407 10.29,11.22 10.29,11.025L10.29,0.735C10.29,0.54 10.367,0.353 10.505,0.215C10.643,0.077 10.83,-0 11.025,-0ZM0.735,-0C0.93,-0 1.117,0.077 1.255,0.215C1.393,0.353 1.47,0.54 1.47,0.735L1.47,11.025C1.47,11.22 1.393,11.407 1.255,11.545C1.117,11.683 0.93,11.76 0.735,11.76C0.54,11.76 0.353,11.683 0.215,11.545C0.077,11.407 -0,11.22 -0,11.025L-0,0.735C-0,0.54 0.077,0.353 0.215,0.215C0.353,0.077 0.54,-0 0.735,-0ZM7.595,-0C7.79,-0 7.977,0.077 8.115,0.215C8.253,0.353 8.33,0.54 8.33,0.735L8.33,11.025C8.33,11.22 8.253,11.407 8.115,11.545C7.977,11.683 7.79,11.76 7.595,11.76C7.4,11.76 7.213,11.683 7.075,11.545C6.937,11.407 6.86,11.22 6.86,11.025L6.86,0.735C6.86,0.54 6.937,0.353 7.075,0.215C7.213,0.077 7.4,-0 7.595,-0ZM4.165,-0C4.36,-0 4.547,0.077 4.685,0.215C4.823,0.353 4.9,0.54 4.9,0.735L4.9,11.025C4.9,11.22 4.823,11.407 4.685,11.545C4.547,11.683 4.36,11.76 4.165,11.76C3.97,11.76 3.783,11.683 3.645,11.545C3.507,11.407 3.43,11.22 3.43,11.025L3.43,0.735C3.43,0.54 3.507,0.353 3.645,0.215C3.783,0.077 3.97,-0 4.165,-0Z"/>
</vector>

View File

@ -0,0 +1,25 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:viewportWidth="12"
android:viewportHeight="12">
<path
android:pathData="M0,0.653C0,0.48 0.069,0.314 0.191,0.191C0.314,0.069 0.48,0 0.653,0H4.573C4.747,0 4.913,0.069 5.035,0.191C5.158,0.314 5.227,0.48 5.227,0.653V4.573C5.227,4.747 5.158,4.913 5.035,5.035C4.913,5.158 4.747,5.227 4.573,5.227H0.653C0.48,5.227 0.314,5.158 0.191,5.035C0.069,4.913 0,4.747 0,4.573V0.653ZM0,7.187C0,7.013 0.069,6.847 0.191,6.725C0.314,6.602 0.48,6.533 0.653,6.533H4.573C4.747,6.533 4.913,6.602 5.035,6.725C5.158,6.847 5.227,7.013 5.227,7.187V11.107C5.227,11.28 5.158,11.446 5.035,11.569C4.913,11.691 4.747,11.76 4.573,11.76H0.653C0.48,11.76 0.314,11.691 0.191,11.569C0.069,11.446 0,11.28 0,11.107V7.187ZM6.533,0.653C6.533,0.48 6.602,0.314 6.725,0.191C6.847,0.069 7.013,0 7.187,0H11.107C11.28,0 11.446,0.069 11.569,0.191C11.691,0.314 11.76,0.48 11.76,0.653V4.573C11.76,4.747 11.691,4.913 11.569,5.035C11.446,5.158 11.28,5.227 11.107,5.227H7.187C7.013,5.227 6.847,5.158 6.725,5.035C6.602,4.913 6.533,4.747 6.533,4.573V0.653ZM6.533,7.187C6.533,7.013 6.602,6.847 6.725,6.725C6.847,6.602 7.013,6.533 7.187,6.533H11.107C11.28,6.533 11.446,6.602 11.569,6.725C11.691,6.847 11.76,7.013 11.76,7.187V11.107C11.76,11.28 11.691,11.446 11.569,11.569C11.446,11.691 11.28,11.76 11.107,11.76H7.187C7.013,11.76 6.847,11.691 6.725,11.569C6.602,11.446 6.533,11.28 6.533,11.107V7.187Z"
android:fillColor="#6F0D1E"
android:fillAlpha="0.8"/>
</vector>

View File

@ -0,0 +1,20 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="16" android:viewportWidth="16" android:width="24dp">
<path android:fillColor="#6F0D1E" android:pathData="M15.595,7.53C15.642,7.538 15.68,7.585 15.68,7.632V9.614C15.679,9.638 15.67,9.661 15.655,9.68C15.64,9.698 15.619,9.711 15.595,9.716L13.717,10.066C13.691,10.072 13.668,10.084 13.649,10.101C13.629,10.118 13.615,10.14 13.606,10.165L13.019,11.629C13.009,11.653 13.004,11.679 13.006,11.705C13.008,11.731 13.016,11.757 13.03,11.779L14.097,13.333C14.11,13.354 14.115,13.378 14.113,13.402C14.11,13.426 14.1,13.449 14.084,13.467L12.683,14.867C12.665,14.883 12.643,14.893 12.619,14.896C12.595,14.898 12.571,14.892 12.55,14.88L11.023,13.832C11.001,13.818 10.976,13.811 10.95,13.81C10.925,13.809 10.899,13.815 10.877,13.826L10.204,14.186C10.194,14.191 10.183,14.194 10.172,14.195C10.161,14.196 10.149,14.195 10.139,14.191C10.128,14.187 10.119,14.181 10.111,14.173C10.103,14.165 10.097,14.156 10.093,14.145L8.704,10.79C8.696,10.768 8.695,10.743 8.702,10.72C8.71,10.698 8.725,10.678 8.745,10.665L8.913,10.562C8.945,10.542 8.987,10.509 9.02,10.48C9.419,10.225 9.725,9.847 9.892,9.403C10.058,8.96 10.076,8.474 9.943,8.019C9.81,7.564 9.534,7.165 9.154,6.88C8.775,6.596 8.314,6.442 7.84,6.442C7.366,6.442 6.905,6.596 6.526,6.88C6.146,7.165 5.87,7.564 5.737,8.019C5.604,8.474 5.622,8.96 5.788,9.403C5.955,9.847 6.261,10.225 6.66,10.48C6.693,10.51 6.735,10.543 6.767,10.562L6.935,10.665C6.955,10.678 6.97,10.698 6.978,10.72C6.985,10.743 6.984,10.768 6.976,10.79L5.587,14.145C5.583,14.155 5.577,14.165 5.569,14.173C5.561,14.181 5.552,14.187 5.541,14.191C5.531,14.194 5.519,14.196 5.508,14.195C5.497,14.194 5.486,14.191 5.476,14.186L4.803,13.826C4.781,13.815 4.755,13.809 4.73,13.81C4.704,13.811 4.679,13.818 4.657,13.831L3.129,14.88C3.109,14.892 3.085,14.898 3.061,14.896C3.037,14.894 3.014,14.884 2.996,14.867L1.595,13.467C1.579,13.449 1.568,13.426 1.566,13.402C1.564,13.378 1.57,13.354 1.582,13.333L2.649,11.778C2.663,11.756 2.671,11.731 2.673,11.705C2.675,11.679 2.671,11.653 2.66,11.629L2.074,10.165C2.065,10.14 2.05,10.119 2.031,10.102C2.012,10.085 1.988,10.073 1.963,10.066L0.085,9.716C0.061,9.71 0.04,9.698 0.025,9.679C0.009,9.66 0.001,9.637 0,9.613L0,7.632C0,7.585 0.039,7.538 0.085,7.53L2.01,7.172C2.036,7.166 2.06,7.154 2.079,7.137C2.099,7.12 2.115,7.098 2.125,7.074L2.715,5.696C2.725,5.673 2.729,5.647 2.728,5.621C2.726,5.595 2.719,5.57 2.705,5.548L1.584,3.913C1.571,3.892 1.566,3.868 1.568,3.844C1.57,3.82 1.58,3.798 1.596,3.78L2.997,2.378C3.014,2.361 3.037,2.351 3.061,2.349C3.086,2.347 3.11,2.353 3.13,2.366L4.794,3.508C4.833,3.535 4.9,3.54 4.943,3.519L6.278,2.973C6.324,2.958 6.367,2.907 6.376,2.86L6.746,0.869C6.752,0.845 6.765,0.824 6.783,0.809C6.802,0.794 6.825,0.785 6.849,0.783H8.831C8.879,0.783 8.925,0.822 8.934,0.868L9.304,2.86C9.31,2.885 9.322,2.908 9.339,2.928C9.356,2.948 9.378,2.963 9.402,2.973L10.737,3.519C10.761,3.529 10.787,3.534 10.813,3.532C10.839,3.53 10.865,3.522 10.887,3.508L12.55,2.366C12.571,2.353 12.595,2.348 12.619,2.35C12.643,2.352 12.666,2.363 12.683,2.379L14.084,3.78C14.118,3.813 14.123,3.874 14.097,3.913L12.974,5.548C12.961,5.57 12.954,5.595 12.952,5.621C12.95,5.647 12.955,5.673 12.965,5.696L13.555,7.074C13.566,7.098 13.581,7.119 13.601,7.136C13.621,7.153 13.644,7.165 13.67,7.172L15.595,7.53Z"/>
</vector>

View File

@ -0,0 +1,24 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="30dp"
android:height="30dp"
android:viewportWidth="30"
android:viewportHeight="30">
<path
android:pathData="M14.625,29.25C10.887,29.25 7.631,28.011 4.856,25.534C2.08,23.056 0.489,19.962 0.081,16.25H3.412C3.792,19.067 5.045,21.396 7.171,23.237C9.298,25.079 11.782,26 14.625,26C17.794,26 20.482,24.897 22.69,22.69C24.898,20.483 26.001,17.795 26,14.625C25.999,11.455 24.896,8.767 22.69,6.562C20.484,4.356 17.796,3.252 14.625,3.25C12.756,3.25 11.009,3.683 9.384,4.55C7.759,5.417 6.392,6.608 5.281,8.125H9.75V11.375H0V1.625H3.25V5.444C4.631,3.71 6.317,2.37 8.309,1.422C10.3,0.474 12.405,0 14.625,0C16.656,0 18.559,0.386 20.334,1.159C22.108,1.931 23.652,2.973 24.965,4.285C26.278,5.597 27.321,7.141 28.093,8.916C28.865,10.692 29.251,12.595 29.25,14.625C29.249,16.655 28.863,18.558 28.093,20.334C27.323,22.109 26.28,23.653 24.965,24.965C23.65,26.277 22.106,27.319 20.334,28.093C18.561,28.867 16.658,29.252 14.625,29.25ZM19.175,21.45L13,15.275V6.5H16.25V13.975L21.45,19.175L19.175,21.45Z"
android:fillColor="#6F0D1E"/>
</vector>

Some files were not shown because too many files have changed in this diff Show More