Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b69dfeb292 | ||
|
|
c32c93338e | ||
|
|
36b9d83a40 | ||
|
|
3a63c65e8f | ||
|
|
582b08cead | ||
|
|
d745815f74 | ||
|
|
211e633517 | ||
|
|
7361078369 | ||
|
|
800eeadd22 | ||
|
|
18851cf7a1 | ||
|
|
8b3fb9a1a5 | ||
|
|
6dc180e791 | ||
|
|
86998e1fe2 | ||
| 68dd5eebd7 | |||
| 759cd20209 | |||
|
|
3301cb15e0 | ||
| 9a87493945 | |||
|
|
c1c208187b | ||
|
|
40ee7c600f | ||
|
|
8671d459cf | ||
| 000c48bf6b | |||
|
|
f95208aa9d | ||
|
|
4aebd825d2 | ||
| f80c7c5d1b | |||
|
|
5e2cfa3821 | ||
|
|
0ed4113366 | ||
|
|
6ddf0dda6c | ||
|
|
ba24c60058 | ||
|
|
feb6ea449f | ||
|
|
030eb6f836 | ||
|
|
af917520b6 | ||
|
|
767456b29a | ||
|
|
5c6c8f78c5 | ||
|
|
0f0bd49709 | ||
|
|
dbc5424aab | ||
|
|
675e398aaf | ||
|
|
57dcd13c34 | ||
|
|
72ecfbe4e7 | ||
|
|
0f435192ee | ||
|
|
fe6c5ad15e | ||
| 10f5bcf355 | |||
| e985b37aa2 | |||
|
|
f4fc2fa730 | ||
|
|
ff2858d484 | ||
|
|
9d9ef95140 | ||
|
|
79ab7c0bae | ||
|
|
283d2d64ce | ||
|
|
fb4c832476 | ||
|
|
5bbda074a8 | ||
|
|
ee2f9df351 | ||
|
|
35600bda29 | ||
|
|
44c0fa20af | ||
|
|
04549321fb | ||
|
|
42bd592c3e | ||
|
|
5904b4f69a | ||
|
|
bdd252cda9 | ||
|
|
9602d7cf81 | ||
|
|
0abf7bc6c9 | ||
|
|
bcd5634941 | ||
|
|
b5e2ec8e01 | ||
|
|
d44163b601 | ||
|
|
37a1a2e38d | ||
|
|
d3a6ddbc37 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -14,3 +14,6 @@
|
|||||||
.cxx
|
.cxx
|
||||||
local.properties
|
local.properties
|
||||||
/.idea
|
/.idea
|
||||||
|
/.idea
|
||||||
|
/sunmiui-lib
|
||||||
|
/paysdk-lib
|
||||||
|
|||||||
@ -57,5 +57,6 @@
|
|||||||
<option name="composableFile" value="true" />
|
<option name="composableFile" value="true" />
|
||||||
<option name="previewFile" value="true" />
|
<option name="previewFile" value="true" />
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
|
<inspection_tool class="UsePropertyAccessSyntax" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
</profile>
|
</profile>
|
||||||
</component>
|
</component>
|
||||||
@ -1,7 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<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" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<component name="ProjectType">
|
||||||
|
|||||||
96
CLAUDE.md
Normal file
96
CLAUDE.md
Normal 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`
|
||||||
@ -1,6 +1,8 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.compose)
|
alias(libs.plugins.kotlin.compose)
|
||||||
|
id("com.google.devtools.ksp")
|
||||||
|
id("com.google.dagger.hilt.android")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@ -12,8 +14,9 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.mob.ustmm"
|
applicationId = "com.mob.utsmyanmar"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
|
//noinspection OldTargetApi
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
@ -36,6 +39,7 @@ android {
|
|||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
|
buildConfig = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,10 +48,13 @@ dependencies {
|
|||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(libs.androidx.compose.foundation)
|
implementation(libs.androidx.compose.foundation)
|
||||||
implementation(libs.androidx.compose.material3)
|
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)
|
||||||
implementation(libs.androidx.compose.ui.graphics)
|
implementation(libs.androidx.compose.ui.graphics)
|
||||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
|
implementation(libs.androidx.hilt.navigation.compose)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
@ -57,4 +64,29 @@ dependencies {
|
|||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
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"))
|
||||||
}
|
}
|
||||||
@ -2,7 +2,32 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
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
|
<application
|
||||||
|
android:name="com.mob.utsmyanmar.MyApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
@ -12,13 +37,11 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.MOBPOS">
|
android:theme="@style/Theme.MOBPOS">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name="com.mob.utsmyanmar.MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name">
|
||||||
android:theme="@style/Theme.MOBPOS">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|||||||
9
app/src/main/java/com/mob/utsmyanmar/AGENTS.md
Normal file
9
app/src/main/java/com/mob/utsmyanmar/AGENTS.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
## Working agreements
|
||||||
|
|
||||||
|
- use Scaffold and AppBar() in every main Screen
|
||||||
|
- use MVVM design pattern
|
||||||
|
- separate UiState and ViewModel for screen for better performance and smooth
|
||||||
|
|
||||||
|
### Re-usable Screens
|
||||||
|
- PasswordInput.kt
|
||||||
|
- InputAmountScreen.kt
|
||||||
@ -1,17 +1,28 @@
|
|||||||
package com.mob.utsmyanmar
|
package com.mob.utsmyanmar
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.WindowManager
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.mob.utsmyanmar.ui.navigation.AppNavGraph
|
import com.mob.utsmyanmar.ui.navigation.AppNavGraph
|
||||||
import com.mob.utsmyanmar.ui.theme.MOBPOSTheme
|
import com.mob.utsmyanmar.ui.theme.MOBPOSTheme
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
// installSplashScreen()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
|
||||||
|
windowInsetsController.hide(WindowInsetsCompat.Type.navigationBars())
|
||||||
|
windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
setContent {
|
setContent {
|
||||||
MOBPOSTheme {
|
MOBPOSTheme {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|||||||
18
app/src/main/java/com/mob/utsmyanmar/MyApplication.kt
Normal file
18
app/src/main/java/com/mob/utsmyanmar/MyApplication.kt
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
13
app/src/main/java/com/mob/utsmyanmar/config/Constants.kt
Normal file
13
app/src/main/java/com/mob/utsmyanmar/config/Constants.kt
Normal 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"
|
||||||
|
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package com.mob.utsmyanmar.model
|
||||||
|
|
||||||
|
enum class CardTransactionType {
|
||||||
|
MPU, EMV, MAG, FALLBACK, MOCK
|
||||||
|
}
|
||||||
12
app/src/main/java/com/mob/utsmyanmar/model/PinPadStatus.kt
Normal file
12
app/src/main/java/com/mob/utsmyanmar/model/PinPadStatus.kt
Normal 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,
|
||||||
|
}
|
||||||
39
app/src/main/java/com/mob/utsmyanmar/model/ProcessCode.kt
Normal file
39
app/src/main/java/com/mob/utsmyanmar/model/ProcessCode.kt
Normal 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"
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.mob.utsmyanmar.model
|
||||||
|
|
||||||
|
enum class SettlementType {
|
||||||
|
NORMAL,
|
||||||
|
CUT_OVER
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package com.mob.utsmyanmar.model.ecr
|
||||||
|
|
||||||
|
enum class ECRResultStatus {
|
||||||
|
USER_CANCEL,
|
||||||
|
TIME_OUT,
|
||||||
|
RESPONSE_RECEIVED
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
)
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.mob.utsmyanmar.model.sirius
|
||||||
|
|
||||||
|
enum class TMSUpdate {
|
||||||
|
UPDATE,
|
||||||
|
CHECK
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.mob.utsmyanmar.model.sirius
|
||||||
|
|
||||||
|
data class TMSValidity(
|
||||||
|
var status: ValidityStatus? = null,
|
||||||
|
var message: String? = null
|
||||||
|
)
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.mob.utsmyanmar.model.sirius
|
||||||
|
|
||||||
|
enum class ValidityStatus {
|
||||||
|
SUCCESS,
|
||||||
|
FAILURE
|
||||||
|
}
|
||||||
@ -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 = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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 = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
)
|
||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,180 @@
|
|||||||
|
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.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.text.font.FontWeight
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,104 @@
|
|||||||
|
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.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
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.graphics.Color as ComposeColor
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.mob.utsmyanmar.ui.theme.Color
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NumericKeypad(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onKeyClick: (String) -> Unit,
|
||||||
|
keys : List<List<String>>,
|
||||||
|
) {
|
||||||
|
// 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
|
||||||
|
fun KeypadButton(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val enabled = text.isNotBlank()
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.then(
|
||||||
|
if (enabled) Modifier.shadow(elevation = 2.dp, shape = RoundedCornerShape(8.dp), clip = false)
|
||||||
|
else Modifier
|
||||||
|
)
|
||||||
|
.background(
|
||||||
|
color = if (enabled) Color.White else ComposeColor.Transparent,
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.clickable(enabled = enabled) { onClick() },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (enabled) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = Color.LegacyRed,
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewNumericKeypad(){
|
||||||
|
val keys : List<List<String>> = listOf(
|
||||||
|
listOf("1", "2", "3"),
|
||||||
|
listOf("4", "5", "6"),
|
||||||
|
listOf("7", "8", "9"),
|
||||||
|
listOf(".", "0", "00")
|
||||||
|
)
|
||||||
|
NumericKeypad(
|
||||||
|
keys = keys,
|
||||||
|
onKeyClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.components.appbar
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
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,
|
||||||
|
actions: @Composable RowScope.() -> Unit = {}
|
||||||
|
) {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
actions = actions,
|
||||||
|
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = Color.LegacyRed
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewAppBar(){
|
||||||
|
AppBar(
|
||||||
|
title = "Title"
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,10 +1,13 @@
|
|||||||
package com.mob.utsmyanmar.ui.dashboard
|
package com.mob.utsmyanmar.ui.dashboard
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
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.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
@ -20,23 +23,19 @@ import com.mob.utsmyanmar.ui.theme.MOBPOSTheme
|
|||||||
import com.mob.utsmyanmar.ui.theme.*
|
import com.mob.utsmyanmar.ui.theme.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.mob.utsmyanmar.R
|
import com.mob.utsmyanmar.R
|
||||||
|
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||||
|
import com.utsmyanmar.paylibs.print.NewPrintReceipt
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DashboardScreen(
|
fun DashboardScreen(
|
||||||
settlementEnabled: Boolean,
|
settlementEnabled: Boolean,
|
||||||
wavePayEnabled: Boolean,
|
wavePayEnabled: Boolean,
|
||||||
onAmountClick: (String) -> Unit,
|
onNavigateAmount: (String) -> Unit
|
||||||
onTransactionClick: () -> Unit,
|
|
||||||
onSettlementClick: () -> Unit,
|
|
||||||
onHistoryClick: () -> Unit,
|
|
||||||
onCardClick: () -> Unit,
|
|
||||||
onWavePayClick: () -> Unit
|
|
||||||
) {
|
) {
|
||||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
|
||||||
ModalNavigationDrawer(
|
ModalNavigationDrawer(
|
||||||
drawerState = drawerState,
|
drawerState = drawerState,
|
||||||
drawerContent = {
|
drawerContent = {
|
||||||
@ -68,7 +67,6 @@ fun DashboardScreen(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
scope.launch { drawerState.close() }
|
scope.launch { drawerState.close() }
|
||||||
onTransactionClick()
|
|
||||||
}
|
}
|
||||||
.padding(horizontal = 24.dp, vertical = 14.dp),
|
.padding(horizontal = 24.dp, vertical = 14.dp),
|
||||||
fontSize = 16.sp
|
fontSize = 16.sp
|
||||||
@ -79,7 +77,6 @@ fun DashboardScreen(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
scope.launch { drawerState.close() }
|
scope.launch { drawerState.close() }
|
||||||
onHistoryClick()
|
|
||||||
}
|
}
|
||||||
.padding(horizontal = 24.dp, vertical = 14.dp),
|
.padding(horizontal = 24.dp, vertical = 14.dp),
|
||||||
fontSize = 16.sp
|
fontSize = 16.sp
|
||||||
@ -90,7 +87,6 @@ fun DashboardScreen(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(enabled = settlementEnabled) {
|
.clickable(enabled = settlementEnabled) {
|
||||||
scope.launch { drawerState.close() }
|
scope.launch { drawerState.close() }
|
||||||
onSettlementClick()
|
|
||||||
}
|
}
|
||||||
.padding(horizontal = 24.dp, vertical = 14.dp),
|
.padding(horizontal = 24.dp, vertical = 14.dp),
|
||||||
color = if (settlementEnabled) Black else White,
|
color = if (settlementEnabled) Black else White,
|
||||||
@ -102,7 +98,6 @@ fun DashboardScreen(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(enabled = wavePayEnabled) {
|
.clickable(enabled = wavePayEnabled) {
|
||||||
scope.launch { drawerState.close() }
|
scope.launch { drawerState.close() }
|
||||||
onWavePayClick()
|
|
||||||
}
|
}
|
||||||
.padding(horizontal = 24.dp, vertical = 14.dp),
|
.padding(horizontal = 24.dp, vertical = 14.dp),
|
||||||
color = if (wavePayEnabled) Black else White,
|
color = if (wavePayEnabled) Black else White,
|
||||||
@ -113,28 +108,14 @@ fun DashboardScreen(
|
|||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
CenterAlignedTopAppBar(
|
AppBar(
|
||||||
title = {
|
title = "Dashboard",
|
||||||
Text(text = "Dashboard", color = White, fontWeight = FontWeight.SemiBold)
|
icon = Icons.Default.Menu,
|
||||||
},
|
onIconClick = {
|
||||||
navigationIcon = {
|
scope.launch {
|
||||||
IconButton(
|
drawerState.open()
|
||||||
onClick = {
|
|
||||||
scope.launch {
|
|
||||||
drawerState.open()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.ic_menu),
|
|
||||||
contentDescription = "Menu Icon",
|
|
||||||
tint = White
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
|
||||||
containerColor = Primary
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
@ -175,6 +156,16 @@ fun DashboardScreen(
|
|||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.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(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
@ -187,7 +178,7 @@ fun DashboardScreen(
|
|||||||
contentColor = Primary,
|
contentColor = Primary,
|
||||||
iconTint = Primary,
|
iconTint = Primary,
|
||||||
border = null,
|
border = null,
|
||||||
onClick = { onAmountClick("Sale") },
|
onClick = { onNavigateAmount("Sale") },
|
||||||
)
|
)
|
||||||
SquareButton(
|
SquareButton(
|
||||||
title = "Sign On",
|
title = "Sign On",
|
||||||
@ -197,7 +188,7 @@ fun DashboardScreen(
|
|||||||
contentColor = Primary,
|
contentColor = Primary,
|
||||||
iconTint = Primary,
|
iconTint = Primary,
|
||||||
border = null,
|
border = null,
|
||||||
onClick = { onAmountClick("Sign On") }
|
onClick = { onNavigateAmount("Sign On") }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,7 +205,7 @@ fun DashboardScreen(
|
|||||||
contentColor = Primary,
|
contentColor = Primary,
|
||||||
iconTint = Primary,
|
iconTint = Primary,
|
||||||
border = null,
|
border = null,
|
||||||
onClick = onSettlementClick
|
onClick = {}
|
||||||
)
|
)
|
||||||
SquareButton(
|
SquareButton(
|
||||||
title = "Others",
|
title = "Others",
|
||||||
@ -224,7 +215,7 @@ fun DashboardScreen(
|
|||||||
contentColor = Primary,
|
contentColor = Primary,
|
||||||
iconTint = Primary,
|
iconTint = Primary,
|
||||||
border = null,
|
border = null,
|
||||||
onClick = onCardClick
|
onClick = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -241,12 +232,7 @@ private fun DashboardScreenPreview() {
|
|||||||
DashboardScreen(
|
DashboardScreen(
|
||||||
settlementEnabled = true,
|
settlementEnabled = true,
|
||||||
wavePayEnabled = true,
|
wavePayEnabled = true,
|
||||||
onAmountClick = {},
|
onNavigateAmount = {}
|
||||||
onTransactionClick = {},
|
|
||||||
onSettlementClick = {},
|
|
||||||
onHistoryClick = {},
|
|
||||||
onCardClick = {},
|
|
||||||
onWavePayClick = {}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,913 @@
|
|||||||
|
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.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.offset
|
||||||
|
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.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
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.AccountBalanceWallet
|
||||||
|
import androidx.compose.material.icons.filled.BarChart
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material.icons.filled.ChevronRight
|
||||||
|
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.Menu
|
||||||
|
import androidx.compose.material.icons.filled.Notifications
|
||||||
|
import androidx.compose.material.icons.filled.Replay
|
||||||
|
import androidx.compose.material.icons.filled.SwapHoriz
|
||||||
|
import androidx.compose.material.icons.filled.Sync
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.DrawerValue
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalDrawerSheet
|
||||||
|
import androidx.compose.material3.ModalNavigationDrawer
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.VerticalDivider
|
||||||
|
import androidx.compose.material3.rememberDrawerState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.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.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
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 kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DashboardScreen2(
|
||||||
|
onNavigateAmount: (String) -> Unit = {},
|
||||||
|
onNavigateSignOn: () -> Unit = {},
|
||||||
|
onNavigateSettlement: () -> Unit = {},
|
||||||
|
onNavigateVersion: () -> Unit = {},
|
||||||
|
onNavigateFunctions: () -> Unit = {},
|
||||||
|
onNavigateAction: (String) -> Unit = {},
|
||||||
|
onNavigateNotifications: () -> Unit = {},
|
||||||
|
dashboardUiState: DashboardUiState = DashboardUiState(),
|
||||||
|
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))
|
||||||
|
Text(
|
||||||
|
text = "Connection Settings",
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Item(
|
||||||
|
title = "Echo Test", subTitle = "Test Connection Status",
|
||||||
|
onClick = {
|
||||||
|
scope.launch { drawerState.close() }
|
||||||
|
openHostActionDialog("Echo Test")
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.ic_up_down_arrow),
|
||||||
|
contentDescription = "icon",
|
||||||
|
tint = Color.LegacyRed
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingIcon = {},
|
||||||
|
)
|
||||||
|
Item(
|
||||||
|
title = "Log-On", subTitle = "Log on to System",
|
||||||
|
onClick = {
|
||||||
|
scope.launch { drawerState.close() }
|
||||||
|
openHostActionDialog("Log-On")
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.ic_lock),
|
||||||
|
contentDescription = "icon",
|
||||||
|
tint = Color.LegacyRed
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingIcon = {},
|
||||||
|
)
|
||||||
|
Item(
|
||||||
|
title = "Log-Off", subTitle = "Log off from System",
|
||||||
|
onClick = {
|
||||||
|
scope.launch { drawerState.close() }
|
||||||
|
openHostActionDialog("Log-Off")
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.ic_cancel_circle),
|
||||||
|
contentDescription = "icon",
|
||||||
|
tint = Color.LegacyRed
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingIcon = {},
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "System Management",
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Item(
|
||||||
|
title = "Functions", subTitle = "System Function Settings",
|
||||||
|
onClick = {
|
||||||
|
scope.launch { drawerState.close() }
|
||||||
|
onNavigateFunctions()
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.ic_four_boxes),
|
||||||
|
contentDescription = "icon",
|
||||||
|
tint = Color.LegacyRed,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingIcon = {},
|
||||||
|
)
|
||||||
|
Item(
|
||||||
|
title = "Version", subTitle = "View App Version Info",
|
||||||
|
onClick = {
|
||||||
|
scope.launch { drawerState.close() }
|
||||||
|
onNavigateVersion()
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.ic_version),
|
||||||
|
contentDescription = "icon",
|
||||||
|
tint = Color.LegacyRed
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingIcon = {},
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Scaffold(
|
||||||
|
containerColor = Color.IvoryBeige, topBar = {
|
||||||
|
AppBar(
|
||||||
|
title = "Dashboard",
|
||||||
|
icon = Icons.Default.Menu,
|
||||||
|
onIconClick = { scope.launch { drawerState.open() } },
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = onNavigateNotifications) {
|
||||||
|
Box {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Notifications,
|
||||||
|
contentDescription = "Notifications",
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(8.dp)
|
||||||
|
.background(Color.GoldenGlow, CircleShape)
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.offset(x = 2.dp, y = (-2).dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}) { 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()
|
||||||
|
}
|
||||||
|
//pager section
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1.3f)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
MenuPager(
|
||||||
|
items = buildMenuItems(
|
||||||
|
onNavigateAmount = onNavigateAmount,
|
||||||
|
onNavigateSignOn = onNavigateSignOn,
|
||||||
|
onNavigateSettlement = onNavigateSettlement,
|
||||||
|
onNavigateAction = onNavigateAction
|
||||||
|
),
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
//transactions section
|
||||||
|
RecentTransactions(
|
||||||
|
transactions = dashboardUiState.recentTransactions,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1.2f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.milliseconds)
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DashboardMenuItem(
|
||||||
|
val title: String,
|
||||||
|
val iconContent: @Composable () -> Unit,
|
||||||
|
val onClick: () -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun buildMenuItems(
|
||||||
|
onNavigateAmount: (String) -> Unit,
|
||||||
|
onNavigateSignOn: () -> Unit,
|
||||||
|
onNavigateSettlement: () -> Unit,
|
||||||
|
onNavigateAction: (String) -> Unit
|
||||||
|
): List<DashboardMenuItem> = listOf(
|
||||||
|
DashboardMenuItem("Sale", { Icon(painterResource(R.drawable.ic_terminal), contentDescription = null, modifier = Modifier.size(40.dp), tint = Color.LegacyRed) }) { onNavigateAmount("Sale") },
|
||||||
|
DashboardMenuItem("MMQR", { Image(painter = painterResource(R.drawable.ic_mmqr_logo), contentDescription = null, modifier = Modifier.height(48.dp)) }) { },
|
||||||
|
DashboardMenuItem("History", { Icon(painterResource(R.drawable.ic_history), contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { },
|
||||||
|
DashboardMenuItem("Sign On", { Icon(painterResource(R.drawable.ic_sign_on), contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateSignOn() },
|
||||||
|
DashboardMenuItem("Settlement", { Icon(painterResource(R.drawable.ic_settlement), contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateSettlement() },
|
||||||
|
DashboardMenuItem("Void", { Icon(Icons.Default.Lock, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Void") },
|
||||||
|
DashboardMenuItem("Refund", { Icon(Icons.Default.Replay, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Refund") },
|
||||||
|
DashboardMenuItem("Pre-Auth", { Icon(Icons.Default.Lock, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Pre-Auth") },
|
||||||
|
DashboardMenuItem("Pre-Auth Void", { Icon(Icons.Default.LockOpen, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Pre-Auth Void") },
|
||||||
|
DashboardMenuItem("Pre-Auth Complete", { Icon(Icons.Default.CreditCard, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Pre-Auth Complete") },
|
||||||
|
DashboardMenuItem("Pre-Auth Complete Void", { Icon(Icons.Default.SwapHoriz, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Pre-Auth Complete Void") },
|
||||||
|
DashboardMenuItem("Cash Out", { Icon(Icons.Default.AccountBalanceWallet, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Cash Out") },
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MenuPager(
|
||||||
|
items: List<DashboardMenuItem>,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val pages = items.chunked(6)
|
||||||
|
val pagerState = rememberPagerState(pageCount = { pages.size })
|
||||||
|
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
HorizontalPager(
|
||||||
|
state = pagerState,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) { pageIndex ->
|
||||||
|
MenuPage(items = pages[pageIndex])
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
repeat(pages.size) { index ->
|
||||||
|
val selected = pagerState.currentPage == index
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 4.dp)
|
||||||
|
.size(if (selected) 8.dp else 6.dp)
|
||||||
|
.background(
|
||||||
|
color = if (selected) Color.LegacyRed else Color.Gray.copy(alpha = 0.4f),
|
||||||
|
shape = CircleShape
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MenuPage(items: List<DashboardMenuItem>) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
items.chunked(3).forEach { rowItems ->
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
rowItems.forEach { item ->
|
||||||
|
MenuCard(
|
||||||
|
title = item.title,
|
||||||
|
icon = item.iconContent,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
onClick = item.onClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
repeat(3 - rowItems.size) {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RecentTransactions(
|
||||||
|
transactions: List<TrnxRecord>,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Recent Transactions",
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
color = Color.Gray.copy(alpha = 0.2f)
|
||||||
|
)
|
||||||
|
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||||
|
if (transactions.isEmpty()) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = "No recent transactions",
|
||||||
|
color = Color.Gray,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items(items = transactions, key = { it.pid }) { record ->
|
||||||
|
TrnxRow(record = record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TrnxRow(record: TrnxRecord) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 4.dp)
|
||||||
|
.clip(RoundedCornerShape(10.dp))
|
||||||
|
.background(Color.White)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(36.dp)
|
||||||
|
.background(Color.LegacyRed.copy(alpha = 0.1f), CircleShape),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = record.typeLabel.take(1),
|
||||||
|
color = Color.LegacyRed,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 15.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.width(10.dp))
|
||||||
|
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = if (record.isVoided) "${record.typeLabel} (Voided)" else record.typeLabel,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = if (record.isVoided) Color.Gray else Color.Black
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = record.maskedCard,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(horizontalAlignment = Alignment.End) {
|
||||||
|
Text(
|
||||||
|
text = record.amountDisplay,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = if (record.isVoided) Color.Gray else Color.LegacyRed
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = record.dateTime,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val previewTransactions = listOf(
|
||||||
|
TrnxRecord(1L, "Sale", "MMK 10,000", "**** 1234", "06/10 14:32", isVoided = false, isApproved = true),
|
||||||
|
TrnxRecord(2L, "Void", "MMK 5,500", "**** 5678", "06/10 13:10", isVoided = true, isApproved = false),
|
||||||
|
TrnxRecord(3L, "Refund", "MMK 2,000", "**** 9012", "06/09 09:45", isVoided = false, isApproved = true),
|
||||||
|
TrnxRecord(4L, "Sale", "MMK 30,000", "**** 3456", "06/09 08:00", isVoided = false, isApproved = true),
|
||||||
|
TrnxRecord(5L, "Settlement", "MMK 0", "----", "06/08 18:00", isVoided = false, isApproved = true),
|
||||||
|
)
|
||||||
|
|
||||||
|
@P2Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewDashboardScreen2() {
|
||||||
|
DashboardScreen2(dashboardUiState = DashboardUiState(previewTransactions))
|
||||||
|
}
|
||||||
|
|
||||||
|
@P3Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewDashboardScreen3() {
|
||||||
|
DashboardScreen2(dashboardUiState = DashboardUiState(previewTransactions))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewItem() {
|
||||||
|
Item(
|
||||||
|
onClick = {},
|
||||||
|
title = "title",
|
||||||
|
subTitle = "sub-title"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Item(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
title: String,
|
||||||
|
subTitle: String,
|
||||||
|
leadingIcon: (@Composable () -> Unit)? = null,
|
||||||
|
trailingIcon: (@Composable () -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = onClick,
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color.White,
|
||||||
|
contentColor = Color.Black
|
||||||
|
),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
// Square icon background
|
||||||
|
leadingIcon?.let {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(RoundedCornerShape(16.dp)) // change to RectangleShape for sharp corners
|
||||||
|
.background(Color.LegacyRed.copy(alpha = 0.1f))
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
it()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.dashboard
|
||||||
|
|
||||||
|
data class TrnxRecord(
|
||||||
|
val pid: Long,
|
||||||
|
val typeLabel: String,
|
||||||
|
val amountDisplay: String,
|
||||||
|
val maskedCard: String,
|
||||||
|
val dateTime: String,
|
||||||
|
val isVoided: Boolean,
|
||||||
|
val isApproved: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class DashboardUiState(
|
||||||
|
val recentTransactions: List<TrnxRecord> = emptyList()
|
||||||
|
)
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.dashboard
|
||||||
|
|
||||||
|
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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class DashboardViewModel @Inject constructor(
|
||||||
|
private val repository: Repository
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow(DashboardUiState())
|
||||||
|
val uiState: StateFlow<DashboardUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
repository.getAllTrans().asFlow().collect { payDetails ->
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
recentTransactions = payDetails.take(10).map { pd -> pd.toRecord() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun PayDetail.toRecord(): TrnxRecord {
|
||||||
|
val type = transType?.takeIf { it.isNotBlank() }?.let { formatTypeLabel(it) } ?: "Transaction"
|
||||||
|
val amount = "MMK %,d".format(amount)
|
||||||
|
val card = CardNo?.takeIf { it.length >= 4 }?.let { "**** ${it.takeLast(4)}" } ?: "----"
|
||||||
|
val dt = buildDateTime(TradeDate, TradeTime)
|
||||||
|
val approved = tradeAnswerCode == "00" || approvalCode?.isNotBlank() == true
|
||||||
|
|
||||||
|
return TrnxRecord(
|
||||||
|
pid = PID ?: 0L,
|
||||||
|
typeLabel = type,
|
||||||
|
amountDisplay = amount,
|
||||||
|
maskedCard = card,
|
||||||
|
dateTime = dt,
|
||||||
|
isVoided = isCanceled,
|
||||||
|
isApproved = approved
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatTypeLabel(raw: String): String = when {
|
||||||
|
raw.contains("SALE", ignoreCase = true) -> "Sale"
|
||||||
|
raw.contains("VOID", ignoreCase = true) -> "Void"
|
||||||
|
raw.contains("REFUND", ignoreCase = true) -> "Refund"
|
||||||
|
raw.contains("SETTLEMENT", ignoreCase = true) -> "Settlement"
|
||||||
|
raw.contains("PRE", ignoreCase = true) -> "Pre-Auth"
|
||||||
|
raw.contains("CASH", ignoreCase = true) -> "Cash Out"
|
||||||
|
raw.contains("WAVE", ignoreCase = true) -> "Wave Pay"
|
||||||
|
else -> raw.lowercase().replaceFirstChar { it.uppercaseChar() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// TradeDate = "MMDD", TradeTime = "HHmmss" (ISO 8583 fields 13 & 12)
|
||||||
|
private fun buildDateTime(date: String?, time: String?): String {
|
||||||
|
val d = date?.padStart(4, '0')?.takeIf { it.length >= 4 } ?: return "--/-- --:--"
|
||||||
|
val t = (time ?: "").padStart(6, '0')
|
||||||
|
return "${d.take(2)}/${d.drop(2).take(2)} ${t.take(2)}:${t.drop(2).take(2)}"
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
)
|
||||||
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,236 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.functions
|
||||||
|
|
||||||
|
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.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.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.R
|
||||||
|
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.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FunctionsScreen(
|
||||||
|
onBack: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val tmsAddress = SystemParamsOperation.getInstance().tmsAddress
|
||||||
|
|
||||||
|
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 = tmsAddress,
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,257 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.input_amount
|
||||||
|
|
||||||
|
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.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.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.mob.utsmyanmar.ui.components.NumericKeypad
|
||||||
|
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
|
||||||
|
|
||||||
|
private val amountKeys = listOf(
|
||||||
|
listOf("1", "2", "3"),
|
||||||
|
listOf("4", "5", "6"),
|
||||||
|
listOf("7", "8", "9"),
|
||||||
|
listOf(".", "0", "00")
|
||||||
|
)
|
||||||
|
|
||||||
|
@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(
|
||||||
|
keys = amountKeys,
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
@ -1,34 +1,278 @@
|
|||||||
package com.mob.utsmyanmar.ui.navigation
|
package com.mob.utsmyanmar.ui.navigation
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.NavType
|
import androidx.navigation.NavType
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.navArgument
|
import androidx.navigation.navArgument
|
||||||
import com.mob.utsmyanmar.ui.amount.AmountScreen
|
import com.mob.utsmyanmar.model.ProcessCode
|
||||||
import com.mob.utsmyanmar.ui.dashboard.DashboardScreen
|
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.DashboardViewModel
|
||||||
|
import com.mob.utsmyanmar.ui.device_info.DeviceInfoViewModel
|
||||||
|
import com.mob.utsmyanmar.ui.functions.FunctionsScreen
|
||||||
|
import com.mob.utsmyanmar.ui.input_amount.AmountRoute
|
||||||
|
import com.mob.utsmyanmar.ui.password_input.InputPassword
|
||||||
|
import com.mob.utsmyanmar.ui.password_input.PasswordType
|
||||||
|
import com.mob.utsmyanmar.ui.pinpad.PinPadRoute
|
||||||
|
import com.mob.utsmyanmar.ui.pinpad.PinPadViewModel
|
||||||
|
import com.mob.utsmyanmar.ui.print_receipt.PrintReceiptScreen
|
||||||
|
import com.mob.utsmyanmar.ui.processing_card.ProcessingCardRoute
|
||||||
|
import com.mob.utsmyanmar.ui.processing_card.ProcessingCardViewModel
|
||||||
|
import com.mob.utsmyanmar.ui.refund_rrn.InputRrnRoute
|
||||||
|
import com.mob.utsmyanmar.ui.sale_void.TranDetailPage
|
||||||
|
import com.mob.utsmyanmar.ui.sale_void.VoidTraceScreen
|
||||||
|
import com.mob.utsmyanmar.ui.sale_void.VoidViewModel
|
||||||
|
import com.mob.utsmyanmar.ui.sending_to_host.ProcessingRoute
|
||||||
|
import com.mob.utsmyanmar.ui.settlement.SettlementScreen
|
||||||
|
import com.mob.utsmyanmar.ui.settlement.SettlementViewModel
|
||||||
|
import com.mob.utsmyanmar.ui.sign_on.SignOnResultScreen
|
||||||
|
import com.mob.utsmyanmar.ui.sign_on.SignOnRoute
|
||||||
|
import com.mob.utsmyanmar.ui.tms_setup.TmsSetupRoute
|
||||||
|
import com.mob.utsmyanmar.ui.tms_setup.TmsSetupViewModel
|
||||||
|
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultEvent
|
||||||
|
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultRoute
|
||||||
|
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultViewModel
|
||||||
|
import com.mob.utsmyanmar.ui.notification.NotificationDetailScreen
|
||||||
|
import com.mob.utsmyanmar.ui.notification.NotificationListScreen
|
||||||
|
import com.mob.utsmyanmar.ui.notification.NotificationViewModel
|
||||||
|
import com.mob.utsmyanmar.ui.version.VersionScreen
|
||||||
|
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.paylibs.utils.iso_utils.TransactionsType
|
||||||
|
|
||||||
|
@SuppressLint("ContextCastToActivity")
|
||||||
@Composable
|
@Composable
|
||||||
fun AppNavGraph(
|
fun AppNavGraph(
|
||||||
navController: NavHostController
|
navController: NavHostController
|
||||||
) {
|
) {
|
||||||
|
val activity = LocalContext.current as ComponentActivity
|
||||||
|
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
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) {
|
composable(Routes.Dashboard.route) {
|
||||||
DashboardScreen(
|
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||||
settlementEnabled = true,
|
val dashboardViewModel: DashboardViewModel = hiltViewModel()
|
||||||
wavePayEnabled = true,
|
val dashboardUiState by dashboardViewModel.uiState.collectAsStateWithLifecycle()
|
||||||
onAmountClick = { action ->
|
DashboardScreen2(
|
||||||
navController.navigate(Routes.Amount.createRoute(action))
|
dashboardUiState = dashboardUiState,
|
||||||
|
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 = {},
|
onNavigateSignOn = {
|
||||||
onSettlementClick = {},
|
navController.navigate(Routes.SignOn.route) {
|
||||||
onHistoryClick = {},
|
launchSingleTop = true
|
||||||
onCardClick = {},
|
}
|
||||||
onWavePayClick = {}
|
},
|
||||||
|
onNavigateSettlement = {
|
||||||
|
navController.navigate(Routes.Settlement.route) {
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNavigateVersion = {
|
||||||
|
navController.navigate(Routes.Version.route)
|
||||||
|
},
|
||||||
|
onNavigateFunctions = {
|
||||||
|
navController.navigate(Routes.Password.createRoute(Routes.Functions.route, PasswordType.SETTING)) {
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNavigateAction = { action ->
|
||||||
|
when (action) {
|
||||||
|
"Void" -> navController.navigate(Routes.VoidTrace.route) { launchSingleTop = true }
|
||||||
|
else -> navController.navigate(Routes.Amount.createRoute(action)) { launchSingleTop = true }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNavigateNotifications = {
|
||||||
|
navController.navigate(Routes.NotificationList.route) { launchSingleTop = true }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(Routes.Version.route){
|
||||||
|
val deviceInfoViewModel: DeviceInfoViewModel = hiltViewModel();
|
||||||
|
VersionScreen(
|
||||||
|
onBack = {navController.popBackStack()},
|
||||||
|
deviceInfoViewModel = deviceInfoViewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = Routes.Password.route,
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("destination") { type = NavType.StringType },
|
||||||
|
navArgument("passwordType") { type = NavType.StringType }
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
val destination = Uri.decode(backStackEntry.arguments?.getString("destination").orEmpty())
|
||||||
|
val passwordType = backStackEntry.arguments?.getString("passwordType").orEmpty()
|
||||||
|
InputPassword(
|
||||||
|
passwordType = passwordType,
|
||||||
|
onBack = { navController.popBackStack() },
|
||||||
|
onPasswordCorrect = {
|
||||||
|
navController.navigate(destination) {
|
||||||
|
popUpTo(Routes.Password.route) { inclusive = true }
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(Routes.Functions.route) {
|
||||||
|
FunctionsScreen(
|
||||||
|
onBack = { navController.popBackStack() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(Routes.VoidTrace.route) {
|
||||||
|
val voidViewModel: VoidViewModel = hiltViewModel()
|
||||||
|
|
||||||
|
VoidTraceScreen(
|
||||||
|
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 ->
|
||||||
|
sharedViewModel.transactionsType.value = TransactionsType.VOID
|
||||||
|
transProcessViewModel.setTransType(TransactionsType.VOID)
|
||||||
|
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 +284,249 @@ fun AppNavGraph(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
) { backStackEntry ->
|
) { backStackEntry ->
|
||||||
AmountScreen(
|
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||||
|
|
||||||
|
AmountRoute(
|
||||||
action = backStackEntry.arguments?.getString("action").orEmpty(),
|
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() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(Routes.InputRrn.route) {
|
||||||
|
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||||
|
|
||||||
|
InputRrnRoute(
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(Routes.NotificationList.route) {
|
||||||
|
val notificationViewModel: NotificationViewModel = hiltViewModel()
|
||||||
|
NotificationListScreen(
|
||||||
|
viewModel = notificationViewModel,
|
||||||
|
onBack = { navController.popBackStack() },
|
||||||
|
onNavigateDetail = { id ->
|
||||||
|
navController.navigate(Routes.NotificationDetail.createRoute(id)) {
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = Routes.NotificationDetail.route,
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("notificationId") { type = NavType.IntType }
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
val notificationViewModel: NotificationViewModel = hiltViewModel()
|
||||||
|
val id = backStackEntry.arguments?.getInt("notificationId") ?: return@composable
|
||||||
|
NotificationDetailScreen(
|
||||||
|
viewModel = notificationViewModel,
|
||||||
|
notificationId = id,
|
||||||
|
onBack = { navController.popBackStack() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun formatAmountForDisplay(amount: String?): String {
|
||||||
|
val normalizedAmount = amount.orEmpty()
|
||||||
|
val value = normalizedAmount.toLongOrNull() ?: return normalizedAmount.ifBlank { "0" }
|
||||||
|
return "%,d".format(value)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,8 +1,40 @@
|
|||||||
package com.mob.utsmyanmar.ui.navigation
|
package com.mob.utsmyanmar.ui.navigation
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
sealed class Routes(val route: String) {
|
sealed class Routes(val route: String) {
|
||||||
|
data object TmsSetup : Routes("tms_setup")
|
||||||
data object Dashboard : Routes("dashboard")
|
data object Dashboard : Routes("dashboard")
|
||||||
data object Amount : Routes("amount/{action}") {
|
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")
|
||||||
|
data object Password : Routes("password/{destination}/{passwordType}") {
|
||||||
|
fun createRoute(destination: String, passwordType: String): String =
|
||||||
|
"password/${Uri.encode(destination)}/$passwordType"
|
||||||
|
}
|
||||||
|
data object NotificationList : Routes("notification_list")
|
||||||
|
data object NotificationDetail : Routes("notification_detail/{notificationId}") {
|
||||||
|
fun createRoute(id: Int): String = "notification_detail/$id"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,116 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.notification
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
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.CircleShape
|
||||||
|
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.Notifications
|
||||||
|
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.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
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NotificationDetailScreen(
|
||||||
|
viewModel: NotificationViewModel,
|
||||||
|
notificationId: Int,
|
||||||
|
onBack: () -> Unit
|
||||||
|
) {
|
||||||
|
val notification = viewModel.getById(notificationId)
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
containerColor = Color.IvoryBeige,
|
||||||
|
topBar = {
|
||||||
|
AppBar(
|
||||||
|
title = "Notification Detail",
|
||||||
|
icon = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
onIconClick = onBack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
if (notification == null) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(text = "Notification not found", color = Color.Gray)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||||
|
elevation = CardDefaults.cardElevation(4.dp)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(20.dp)) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(56.dp)
|
||||||
|
.background(Color.LegacyRed.copy(alpha = 0.1f), CircleShape),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Notifications,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.LegacyRed,
|
||||||
|
modifier = Modifier.size(28.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = notification.title,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color.Black
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(6.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = notification.timestamp,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = notification.message,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = Color.Black,
|
||||||
|
lineHeight = 22.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,174 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.notification
|
||||||
|
|
||||||
|
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.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
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.Notifications
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
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.clip
|
||||||
|
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 com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||||
|
import com.mob.utsmyanmar.ui.theme.Color
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NotificationListScreen(
|
||||||
|
viewModel: NotificationViewModel,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onNavigateDetail: (Int) -> Unit
|
||||||
|
) {
|
||||||
|
val notifications by viewModel.notifications.collectAsState()
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
containerColor = Color.IvoryBeige,
|
||||||
|
topBar = {
|
||||||
|
AppBar(
|
||||||
|
title = "Notifications",
|
||||||
|
icon = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
onIconClick = onBack,
|
||||||
|
actions = {
|
||||||
|
if (notifications.any { !it.isRead }) {
|
||||||
|
TextButton(onClick = { viewModel.markAllAsRead() }) {
|
||||||
|
Text(text = "Mark all read", color = Color.White, fontSize = 12.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
if (notifications.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Notifications,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.Gray,
|
||||||
|
modifier = Modifier.size(64.dp)
|
||||||
|
)
|
||||||
|
Text(text = "No notifications", color = Color.Gray, fontSize = 14.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
items(items = notifications, key = { it.id }) { notification ->
|
||||||
|
NotificationItem(
|
||||||
|
notification = notification,
|
||||||
|
onClick = {
|
||||||
|
viewModel.markAsRead(notification.id)
|
||||||
|
onNavigateDetail(notification.id)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NotificationItem(
|
||||||
|
notification: AppNotification,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(Color.White)
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(42.dp)
|
||||||
|
.background(
|
||||||
|
color = if (notification.isRead) Color.Gray.copy(alpha = 0.12f)
|
||||||
|
else Color.LegacyRed.copy(alpha = 0.12f),
|
||||||
|
shape = CircleShape
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Notifications,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (notification.isRead) Color.Gray else Color.LegacyRed,
|
||||||
|
modifier = Modifier.size(22.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.width(12.dp))
|
||||||
|
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = notification.title,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = if (notification.isRead) FontWeight.Normal else FontWeight.SemiBold,
|
||||||
|
color = if (notification.isRead) Color.Gray else Color.Black,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
if (!notification.isRead) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(8.dp)
|
||||||
|
.background(Color.LegacyRed, CircleShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = notification.message,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.Gray,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = notification.timestamp,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
color = Color.Gray.copy(alpha = 0.7f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.notification
|
||||||
|
|
||||||
|
data class AppNotification(
|
||||||
|
val id: Int,
|
||||||
|
val title: String,
|
||||||
|
val message: String,
|
||||||
|
val timestamp: String,
|
||||||
|
val isRead: Boolean = false
|
||||||
|
)
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.notification
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class NotificationViewModel @Inject constructor() : ViewModel() {
|
||||||
|
|
||||||
|
private val _notifications = MutableStateFlow(sampleNotifications)
|
||||||
|
val notifications: StateFlow<List<AppNotification>> = _notifications.asStateFlow()
|
||||||
|
|
||||||
|
val unreadCount: Int get() = _notifications.value.count { !it.isRead }
|
||||||
|
|
||||||
|
fun markAsRead(id: Int) {
|
||||||
|
_notifications.value = _notifications.value.map {
|
||||||
|
if (it.id == id) it.copy(isRead = true) else it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun markAllAsRead() {
|
||||||
|
_notifications.value = _notifications.value.map { it.copy(isRead = true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getById(id: Int): AppNotification? = _notifications.value.find { it.id == id }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val sampleNotifications = listOf(
|
||||||
|
AppNotification(1, "Settlement Reminder", "Your daily settlement is pending. Please settle before end of day.", "Today, 17:30", isRead = false),
|
||||||
|
AppNotification(2, "TMS Update Available", "A new terminal configuration update is ready. Please restart to apply.", "Today, 09:15", isRead = false),
|
||||||
|
AppNotification(3, "Transaction Approved", "Sale of MMK 50,000 was approved successfully. Trace #001234.", "Yesterday, 14:22", isRead = true),
|
||||||
|
AppNotification(4, "Network Warning", "Intermittent network issues detected. Contact your administrator if the problem persists.", "Yesterday, 11:05", isRead = true),
|
||||||
|
AppNotification(5, "Log-On Required", "Your terminal session has expired. Please log on again to continue.", "06/09, 08:00", isRead = true),
|
||||||
|
)
|
||||||
@ -0,0 +1,210 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.password_input
|
||||||
|
|
||||||
|
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.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBackIosNew
|
||||||
|
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.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
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 androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.mob.utsmyanmar.ui.components.NumericKeypad
|
||||||
|
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
|
||||||
|
|
||||||
|
object PasswordType {
|
||||||
|
const val SYSTEM = "system_password"
|
||||||
|
const val SETTLEMENT = "settlement_password"
|
||||||
|
const val SETTING = "setting_password"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val passwords = mapOf(
|
||||||
|
PasswordType.SYSTEM to "111111",
|
||||||
|
PasswordType.SETTING to "222222",
|
||||||
|
PasswordType.SETTLEMENT to "123456"
|
||||||
|
)
|
||||||
|
|
||||||
|
private val passwordKeys = listOf(
|
||||||
|
listOf("1", "2", "3"),
|
||||||
|
listOf("4", "5", "6"),
|
||||||
|
listOf("7", "8", "9"),
|
||||||
|
listOf("", "0", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InputPassword(
|
||||||
|
passwordType: String,
|
||||||
|
onBack: () -> Unit = {},
|
||||||
|
onPasswordCorrect: () -> Unit = {},
|
||||||
|
viewModel: PasswordInputViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val expectedPassword = passwords[passwordType] ?: passwords[PasswordType.SETTING]!!
|
||||||
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) { viewModel.reset() }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
AppBar(title = "Password", icon = Icons.Default.ArrowBackIosNew, onIconClick = onBack)
|
||||||
|
},
|
||||||
|
containerColor = Color.IvoryBeige
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// Display area
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.weight(2f)
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.clickable(enabled = uiState.canDelete, onClick = viewModel::onDelete),
|
||||||
|
shape = RoundedCornerShape(18.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Backspace,
|
||||||
|
contentDescription = "Delete",
|
||||||
|
tint = if (uiState.canDelete) Color.LegacyRed else Color.Gray,
|
||||||
|
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(text = "Enter Password", color = Color.Gray, fontSize = 18.sp)
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
PasswordDots(filledCount = uiState.input.length, totalCount = PASSWORD_LENGTH)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = uiState.errorMessage ?: "",
|
||||||
|
color = Color.LegacyRed,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keypad — stable lambda: viewModel::onKey never changes between recompositions
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(3f),
|
||||||
|
verticalArrangement = Arrangement.Bottom
|
||||||
|
) {
|
||||||
|
NumericKeypad(
|
||||||
|
keys = passwordKeys,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
onKeyClick = viewModel::onKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(0.5f)
|
||||||
|
) {
|
||||||
|
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("Cancel")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.validate(expectedPassword, onPasswordCorrect) },
|
||||||
|
modifier = Modifier.weight(1f).height(56.dp),
|
||||||
|
enabled = uiState.isConfirmEnabled,
|
||||||
|
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 PasswordDots(filledCount: Int, totalCount: Int) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
repeat(totalCount) { index ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(14.dp)
|
||||||
|
.background(
|
||||||
|
color = if (index < filledCount) Color.LegacyRed
|
||||||
|
else Color.Gray.copy(alpha = 0.3f),
|
||||||
|
shape = CircleShape
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@P3Preview
|
||||||
|
@P2Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewInputPassword() {
|
||||||
|
InputPassword(passwordType = PasswordType.SETTING)
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.password_input
|
||||||
|
|
||||||
|
internal const val PASSWORD_LENGTH = 6
|
||||||
|
|
||||||
|
data class PasswordInputUiState(
|
||||||
|
val input: String = "",
|
||||||
|
val errorMessage: String? = null
|
||||||
|
) {
|
||||||
|
val isConfirmEnabled: Boolean get() = input.length == PASSWORD_LENGTH
|
||||||
|
val canDelete: Boolean get() = input.isNotEmpty()
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.password_input
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
|
||||||
|
class PasswordInputViewModel : ViewModel() {
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow(PasswordInputUiState())
|
||||||
|
val uiState: StateFlow<PasswordInputUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
fun onKey(digit: String) {
|
||||||
|
_uiState.update { state ->
|
||||||
|
if (state.input.length < PASSWORD_LENGTH)
|
||||||
|
state.copy(input = state.input + digit, errorMessage = null)
|
||||||
|
else
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDelete() {
|
||||||
|
_uiState.update { state ->
|
||||||
|
state.copy(input = state.input.dropLast(1), errorMessage = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validate(expectedPassword: String, onCorrect: () -> Unit) {
|
||||||
|
if (_uiState.value.input == expectedPassword) {
|
||||||
|
onCorrect()
|
||||||
|
} else {
|
||||||
|
_uiState.update { it.copy(errorMessage = "Incorrect password", input = "") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
_uiState.value = PasswordInputUiState()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
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
|
||||||
|
) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
pinPadViewModel.cancelPinPad()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PinPadScreen(
|
||||||
|
pinText = pinText,
|
||||||
|
alertMessage = alertMsg,
|
||||||
|
canGoBack = canGoBack,
|
||||||
|
onBack = onBack,
|
||||||
|
onKeyboardReady = { keyboard ->
|
||||||
|
pinPadViewModel.startPinPadProcess(keyboard)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
181
app/src/main/java/com/mob/utsmyanmar/ui/pinpad/PinPadScreen.kt
Normal file
181
app/src/main/java/com/mob/utsmyanmar/ui/pinpad/PinPadScreen.kt
Normal 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,561 @@
|
|||||||
|
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")
|
||||||
|
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 = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
20
app/src/main/java/com/mob/utsmyanmar/ui/preview/Preview.kt
Normal file
20
app/src/main/java/com/mob/utsmyanmar/ui/preview/Preview.kt
Normal 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=427dp,height=949dp,dpi=270",
|
||||||
|
showBackground = true,
|
||||||
|
showSystemUi = true
|
||||||
|
)
|
||||||
|
annotation class P3Preview
|
||||||
@ -0,0 +1,280 @@
|
|||||||
|
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("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 = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.processing_card
|
||||||
|
|
||||||
|
sealed interface ProcessingCardEvent {
|
||||||
|
data object StartProcess : ProcessingCardEvent
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
)
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
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 com.mob.utsmyanmar.ui.components.NumericEntryScreen
|
||||||
|
import com.mob.utsmyanmar.viewmodel.SharedViewModel
|
||||||
|
import com.utsmyanmar.paylibs.utils.POSUtil
|
||||||
|
|
||||||
|
private const val RRN_MAX_LENGTH = 12
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InputRrnRoute(
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
sharedViewModel.payDetail.value?.let { payDetail ->
|
||||||
|
payDetail.referNo = trimmedRrn
|
||||||
|
sharedViewModel.payDetail.value = payDetail
|
||||||
|
}
|
||||||
|
onNavigateProcessing()
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,313 @@
|
|||||||
|
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.utsmyanmar.paylibs.model.PayDetail
|
||||||
|
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VoidTraceScreen(
|
||||||
|
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) {
|
||||||
|
if (searchedTrace.isBlank()) {
|
||||||
|
MutableLiveData<PayDetail?>(null)
|
||||||
|
} else {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
voidViewModel.getVoidTrans(
|
||||||
|
TransactionsType.SALE.value,
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.sending_to_host
|
||||||
|
|
||||||
|
data class ProcessingState(
|
||||||
|
val title: String = "Processing",
|
||||||
|
)
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
transProcessViewModel.startOnlineProcess()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(transStatus) {
|
||||||
|
when(transStatus) {
|
||||||
|
TransResultStatus.SUCCESS -> onNavigateTransactionResult()
|
||||||
|
TransResultStatus.FAIL -> onNavigateTransactionResult()
|
||||||
|
TransResultStatus.REVERSAL_SUCCESS -> onNavigateMain()
|
||||||
|
TransResultStatus.REVERSAL_FAIL -> onNavigateMain()
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(settlementStatus.status) {
|
||||||
|
when(settlementStatus.status) {
|
||||||
|
TransactionStatus.ON_SUCCESS -> onNavigateTransactionResult()
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if(sharedViewModel.transactionsType.value == TransactionsType.SETTLEMENT) {
|
||||||
|
ProcessingScreen(settlementStatus.processingState)
|
||||||
|
} else {
|
||||||
|
ProcessingScreen(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,500 @@
|
|||||||
|
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 ->
|
||||||
|
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 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
262
app/src/main/java/com/mob/utsmyanmar/ui/sign_on/SignOnScreen.kt
Normal file
262
app/src/main/java/com/mob/utsmyanmar/ui/sign_on/SignOnScreen.kt
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,13 +5,22 @@ import androidx.compose.ui.graphics.Color
|
|||||||
val Purple80 = Color(0xFFD0BCFF)
|
val Purple80 = Color(0xFFD0BCFF)
|
||||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||||
val Pink80 = Color(0xFFEFB8C8)
|
val Pink80 = Color(0xFFEFB8C8)
|
||||||
|
|
||||||
val Purple40 = Color(0xFF6650a4)
|
val Purple40 = Color(0xFF6650a4)
|
||||||
val PurpleGrey40 = Color(0xFF625b71)
|
val PurpleGrey40 = Color(0xFF625b71)
|
||||||
val Pink40 = Color(0xFF7D5260)
|
val Pink40 = Color(0xFF7D5260)
|
||||||
|
|
||||||
val Primary = Color(0xFFCA2027)
|
val Primary = Color(0xFFCA2027)
|
||||||
|
|
||||||
val White = Color(0xFFFFFFFF)
|
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)
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,165 @@
|
|||||||
|
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()
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,292 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.transaction_result
|
||||||
|
|
||||||
|
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.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))
|
||||||
|
|
||||||
|
SuccessIcon()
|
||||||
|
|
||||||
|
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 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 = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
)
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -0,0 +1,283 @@
|
|||||||
|
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 -> 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
207
app/src/main/java/com/mob/utsmyanmar/ui/version/Version.kt
Normal file
207
app/src/main/java/com/mob/utsmyanmar/ui/version/Version.kt
Normal 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()
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
1037
app/src/main/java/com/mob/utsmyanmar/utils/CoreUtils.kt
Normal file
1037
app/src/main/java/com/mob/utsmyanmar/utils/CoreUtils.kt
Normal file
File diff suppressed because it is too large
Load Diff
17
app/src/main/java/com/mob/utsmyanmar/utils/ECRSetups.kt
Normal file
17
app/src/main/java/com/mob/utsmyanmar/utils/ECRSetups.kt
Normal 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
|
||||||
|
}
|
||||||
24
app/src/main/java/com/mob/utsmyanmar/utils/ECRSetupsCMHL.kt
Normal file
24
app/src/main/java/com/mob/utsmyanmar/utils/ECRSetupsCMHL.kt
Normal 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)
|
||||||
|
}
|
||||||
57
app/src/main/java/com/mob/utsmyanmar/utils/MockCard.kt
Normal file
57
app/src/main/java/com/mob/utsmyanmar/utils/MockCard.kt
Normal 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")
|
||||||
|
*/
|
||||||
|
}
|
||||||
14
app/src/main/java/com/mob/utsmyanmar/utils/OldECRSetups.kt
Normal file
14
app/src/main/java/com/mob/utsmyanmar/utils/OldECRSetups.kt
Normal 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
|
||||||
|
}
|
||||||
120
app/src/main/java/com/mob/utsmyanmar/utils/TransactionUtil.kt
Normal file
120
app/src/main/java/com/mob/utsmyanmar/utils/TransactionUtil.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/src/main/java/com/mob/utsmyanmar/utils/tms/TMSSetups.kt
Normal file
25
app/src/main/java/com/mob/utsmyanmar/utils/tms/TMSSetups.kt
Normal 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
|
||||||
|
|
||||||
|
}
|
||||||
274
app/src/main/java/com/mob/utsmyanmar/utils/tms/TMSSetupsImpl.kt
Normal file
274
app/src/main/java/com/mob/utsmyanmar/utils/tms/TMSSetupsImpl.kt
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
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) {
|
||||||
|
"?"
|
||||||
|
}
|
||||||
|
return "PHV$phv-RV$rv-SV$sv"
|
||||||
|
}
|
||||||
|
private fun parseBoolean(data: String): Boolean =
|
||||||
|
data.toIntOrNull()?.let { it == 1 } ?: data.toBoolean();
|
||||||
|
}
|
||||||
169
app/src/main/java/com/mob/utsmyanmar/utils/tms/TMSUtil.kt
Normal file
169
app/src/main/java/com/mob/utsmyanmar/utils/tms/TMSUtil.kt
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,357 @@
|
|||||||
|
package com.mob.utsmyanmar.viewmodel
|
||||||
|
|
||||||
|
import android.text.TextUtils
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.mob.utsmyanmar.model.TransResultStatus
|
||||||
|
import com.mob.utsmyanmar.ui.processing_card.ProcessingCardState
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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) {}
|
||||||
|
override fun processPreCompDb(payResult: PayDetail) {}
|
||||||
|
override fun processPreCompVoidDb(payResult: PayDetail) {}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
9
app/src/main/res/drawable/ic_address_global.xml
Normal file
9
app/src/main/res/drawable/ic_address_global.xml
Normal 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>
|
||||||
9
app/src/main/res/drawable/ic_cancel_circle.xml
Normal file
9
app/src/main/res/drawable/ic_cancel_circle.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="800dp"
|
||||||
|
android:height="800dp"
|
||||||
|
android:viewportWidth="32"
|
||||||
|
android:viewportHeight="32">
|
||||||
|
<path
|
||||||
|
android:pathData="M10.771,8.518c-1.144,0.215 -2.83,2.171 -2.086,2.915l4.573,4.571 -4.573,4.571c-0.915,0.915 1.829,3.656 2.744,2.742l4.573,-4.571 4.573,4.571c0.915,0.915 3.658,-1.829 2.744,-2.742l-4.573,-4.571 4.573,-4.571c0.915,-0.915 -1.829,-3.656 -2.744,-2.742l-4.573,4.571 -4.573,-4.571c-0.173,-0.171 -0.394,-0.223 -0.657,-0.173v0zM16,1c-8.285,0 -15,6.716 -15,15s6.715,15 15,15 15,-6.716 15,-15 -6.715,-15 -15,-15zM16,4.75c6.213,0 11.25,5.037 11.25,11.25s-5.037,11.25 -11.25,11.25 -11.25,-5.037 -11.25,-11.25c0.001,-6.213 5.037,-11.25 11.25,-11.25z"
|
||||||
|
android:fillColor="#000000"/>
|
||||||
|
</vector>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user