Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 142716caa4 | |||
| 3b7354fc3f | |||
| f070ad1c10 | |||
| b073e62a20 | |||
| 5d51f53440 | |||
| 4fefae3646 | |||
|
|
7361078369 | ||
|
|
800eeadd22 | ||
|
|
18851cf7a1 | ||
|
|
8b3fb9a1a5 | ||
|
|
6dc180e791 | ||
|
|
86998e1fe2 | ||
| 8e6fde9856 | |||
| 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>
|
||||||
|
|||||||
3
app/src/main/java/com/mob/utsmyanmar/AGENTS.md
Normal file
3
app/src/main/java/com/mob/utsmyanmar/AGENTS.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
## Working agreements
|
||||||
|
|
||||||
|
- use Scaffold and AppBar() in every main Screen
|
||||||
@ -1,17 +1,24 @@
|
|||||||
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.SystemBarStyle
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
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()
|
||||||
|
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,242 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.rounded.Backspace
|
||||||
|
import androidx.compose.material.icons.rounded.Backspace
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.mob.utsmyanmar.ui.theme.Color
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NumericEntryScreen(
|
||||||
|
title: String,
|
||||||
|
prompt: String,
|
||||||
|
displayValue: String,
|
||||||
|
supportingText: String,
|
||||||
|
confirmText: String,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onCancelClick: () -> Unit,
|
||||||
|
onConfirmClick: () -> Unit,
|
||||||
|
onKeyClick: (String) -> Unit,
|
||||||
|
onDeleteClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
prefixLabel: String? = null,
|
||||||
|
errorMessage: String? = null,
|
||||||
|
confirmEnabled: Boolean = true,
|
||||||
|
canDelete: Boolean = true,
|
||||||
|
keys: List<List<String>> = listOf(
|
||||||
|
listOf("1", "2", "3"),
|
||||||
|
listOf("4", "5", "6"),
|
||||||
|
listOf("7", "8", "9"),
|
||||||
|
listOf(".", "0", "00")
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.IvoryBeige)
|
||||||
|
.navigationBarsPadding()
|
||||||
|
.statusBarsPadding()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 20.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = prompt,
|
||||||
|
color = Color.Gray,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||||
|
verticalAlignment = Alignment.Bottom
|
||||||
|
) {
|
||||||
|
if (!prefixLabel.isNullOrBlank()) {
|
||||||
|
Text(
|
||||||
|
text = prefixLabel,
|
||||||
|
color = Color.LegacyRed,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
modifier = Modifier.padding(end = 10.dp, bottom = 6.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = displayValue,
|
||||||
|
color = Color.LegacyRed,
|
||||||
|
fontSize = 32.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = supportingText,
|
||||||
|
color = if (errorMessage == null) Color.Gray else Color.Error,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(80.dp))
|
||||||
|
|
||||||
|
NumericKeypad(
|
||||||
|
keys = keys,
|
||||||
|
onKeyClick = onKeyClick
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = onCancelClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(56.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color.White,
|
||||||
|
contentColor = Color.LegacyRed
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = onConfirmClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(56.dp),
|
||||||
|
enabled = confirmEnabled,
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color.LegacyRed,
|
||||||
|
contentColor = Color.White,
|
||||||
|
disabledContainerColor = Color.LegacyRed.copy(alpha = 0.5f),
|
||||||
|
disabledContentColor = Color.White
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = confirmText,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(18.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(top = 25.dp, end = 20.dp)
|
||||||
|
.clickable(enabled = canDelete) {
|
||||||
|
onDeleteClick()
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(18.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Rounded.Backspace,
|
||||||
|
contentDescription = "Delete",
|
||||||
|
tint = if (canDelete) Color.LegacyRed else Color.Gray,
|
||||||
|
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NumericKeypad(
|
||||||
|
keys: List<List<String>>,
|
||||||
|
onKeyClick: (String) -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
keys.forEach { row ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
row.forEach { key ->
|
||||||
|
KeypadButton(
|
||||||
|
text = key,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
onClick = { onKeyClick(key) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun KeypadButton(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val enabled = text.isNotBlank()
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.height(66.dp)
|
||||||
|
.shadow(
|
||||||
|
elevation = 2.dp,
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
clip = false
|
||||||
|
)
|
||||||
|
.background(
|
||||||
|
color = Color.White,
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.clickable(enabled = enabled) { onClick() },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = if (enabled) Color.LegacyRed else Color.White,
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.components.appbar
|
||||||
|
|
||||||
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import com.mob.utsmyanmar.ui.theme.Color
|
||||||
|
import com.mob.utsmyanmar.ui.theme.Primary
|
||||||
|
import com.mob.utsmyanmar.ui.theme.White
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun AppBar(
|
||||||
|
title: String,
|
||||||
|
icon: ImageVector? = null,
|
||||||
|
onIconClick: (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
color = White,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
navigationIcon = {
|
||||||
|
if (icon != null && onIconClick != null) {
|
||||||
|
IconButton(
|
||||||
|
onClick = onIconClick
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = "App bar icon",
|
||||||
|
tint = White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = Color.LegacyRed
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewAppBar(){
|
||||||
|
AppBar(
|
||||||
|
title = "Title"
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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 = {
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
drawerState.open()
|
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,638 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.dashboard
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import com.mob.utsmyanmar.R
|
||||||
|
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||||
|
import com.mob.utsmyanmar.ui.device_info.DeviceInfoViewModel
|
||||||
|
import com.mob.utsmyanmar.ui.preview.P2Preview
|
||||||
|
import com.mob.utsmyanmar.ui.preview.P3Preview
|
||||||
|
import com.mob.utsmyanmar.ui.theme.Color
|
||||||
|
import com.utsmyanmar.paylibs.sign_on.EchoTestProcess
|
||||||
|
import com.utsmyanmar.paylibs.sign_on.SignOnListener
|
||||||
|
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DashboardScreen2(
|
||||||
|
onNavigateAmount: (String) -> Unit = {},
|
||||||
|
onNavigateSignOn: () -> Unit = {},
|
||||||
|
onNavigateSeeMore: () -> Unit = {},
|
||||||
|
onNavigateSettlement: () -> Unit = {},
|
||||||
|
onNavigateVersion: () -> Unit = {},
|
||||||
|
onNavigateFunctions: () -> Unit = {},
|
||||||
|
deviceInfoViewModel: DeviceInfoViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val deviceInfo by deviceInfoViewModel.uiState.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
deviceInfoViewModel.loadDeviceInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val mainHandler = remember { Handler(Looper.getMainLooper()) }
|
||||||
|
var showHostActionDialog by remember { mutableStateOf(false) }
|
||||||
|
var activeHostAction by remember { mutableStateOf("Log-On") }
|
||||||
|
var isHostActionRunning by remember { mutableStateOf(false) }
|
||||||
|
var dialogMessage by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
val isOnline = true
|
||||||
|
|
||||||
|
fun confirmationMessage(action: String) = "Do you want to start ${action.lowercase()}?"
|
||||||
|
fun processingMessage(action: String) = "Sending ${action.lowercase()} request to host..."
|
||||||
|
fun successMessage(action: String) = "$action success."
|
||||||
|
fun failureMessage(action: String, resultCode: Int?) =
|
||||||
|
"$action failed. Response code: ${resultCode ?: -1}"
|
||||||
|
|
||||||
|
fun networkFailureMessage(action: String) = "Network error during $action."
|
||||||
|
fun openHostActionDialog(action: String) {
|
||||||
|
activeHostAction = action
|
||||||
|
isHostActionRunning = false
|
||||||
|
dialogMessage = confirmationMessage(action)
|
||||||
|
showHostActionDialog = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showHostActionDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
if (!isHostActionRunning) {
|
||||||
|
showHostActionDialog = false
|
||||||
|
}
|
||||||
|
}, title = {
|
||||||
|
Text(
|
||||||
|
text = activeHostAction, color = Color.LegacyRed, fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}, text = {
|
||||||
|
Text(text = dialogMessage)
|
||||||
|
}, confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
if (isHostActionRunning) return@TextButton
|
||||||
|
|
||||||
|
if (dialogMessage != confirmationMessage(activeHostAction)) {
|
||||||
|
showHostActionDialog = false
|
||||||
|
dialogMessage = confirmationMessage(activeHostAction)
|
||||||
|
return@TextButton
|
||||||
|
}
|
||||||
|
|
||||||
|
isHostActionRunning = true
|
||||||
|
dialogMessage = processingMessage(activeHostAction)
|
||||||
|
|
||||||
|
val signOnProcess = EchoTestProcess.getInstance()
|
||||||
|
val request = when (activeHostAction) {
|
||||||
|
"Echo Test" -> signOnProcess.enqueue(false)
|
||||||
|
"Log-Off" -> signOnProcess.enqueueLogOff()
|
||||||
|
else -> signOnProcess.enqueueLogOn()
|
||||||
|
}
|
||||||
|
|
||||||
|
request.startSignOn(object : SignOnListener {
|
||||||
|
override fun onSuccessSignOn() {
|
||||||
|
mainHandler.post {
|
||||||
|
isHostActionRunning = false
|
||||||
|
dialogMessage = successMessage(activeHostAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailureSignOn(resultCode: Int?) {
|
||||||
|
mainHandler.post {
|
||||||
|
isHostActionRunning = false
|
||||||
|
mainHandler.post {
|
||||||
|
isHostActionRunning = false
|
||||||
|
dialogMessage = failureMessage(activeHostAction, resultCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun onNetworkFailSignOn(message: String?) {
|
||||||
|
mainHandler.post {
|
||||||
|
isHostActionRunning = false
|
||||||
|
dialogMessage = message?.takeIf { it.isNotBlank() }
|
||||||
|
?: networkFailureMessage(activeHostAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}) {
|
||||||
|
Text(
|
||||||
|
text = when {
|
||||||
|
isHostActionRunning -> "Processing"
|
||||||
|
dialogMessage == confirmationMessage(activeHostAction) -> "Start"
|
||||||
|
else -> "Close"
|
||||||
|
}, color = Color.LegacyRed
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, dismissButton = {
|
||||||
|
if (!isHostActionRunning) {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
showHostActionDialog = false
|
||||||
|
dialogMessage = confirmationMessage(activeHostAction)
|
||||||
|
}) {
|
||||||
|
Text(text = "Cancel", color = Color.Gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, containerColor = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ModalNavigationDrawer(
|
||||||
|
drawerState = drawerState, drawerContent = {
|
||||||
|
ModalDrawerSheet(
|
||||||
|
modifier = Modifier.fillMaxWidth(0.78f),
|
||||||
|
drawerContainerColor = Color.White,
|
||||||
|
drawerShape = RoundedCornerShape(topEnd = 24.dp, bottomEnd = 24.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(Color.IvoryBeige)
|
||||||
|
.padding(horizontal = 20.dp, vertical = 28.dp)
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(80.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color.White), contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.logo_mob),
|
||||||
|
contentDescription = "mob logo",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(100.dp)
|
||||||
|
.padding(16.dp),
|
||||||
|
contentScale = ContentScale.Fit
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = "MOB Merchant",
|
||||||
|
color = Color.CrimsonRed,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "S/N:${deviceInfo.serialNumber}",
|
||||||
|
color = Color.Black,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = if (isOnline) "Online" else "Offline",
|
||||||
|
color = if (isOnline) Color.Success else Color.Error,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
DrawerItem("Log-On", Icons.Default.Dashboard) {
|
||||||
|
scope.launch { drawerState.close() }
|
||||||
|
openHostActionDialog("Log-On")
|
||||||
|
}
|
||||||
|
DrawerItem("Echo Test", Icons.Default.Sync) {
|
||||||
|
scope.launch { drawerState.close() }
|
||||||
|
openHostActionDialog("Echo Test")
|
||||||
|
}
|
||||||
|
DrawerItem("Log-Off", Icons.Default.Dashboard) {
|
||||||
|
scope.launch { drawerState.close() }
|
||||||
|
openHostActionDialog("Log-Off")
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = "System Management",
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
var switchChecked by remember { mutableStateOf(SystemParamsOperation.getInstance().isReversalOn) }
|
||||||
|
DrawerItem(
|
||||||
|
title = "Reversal On/Off",
|
||||||
|
icon = Icons.Default.Sync,
|
||||||
|
showSwitch = true,
|
||||||
|
isChecked = switchChecked,
|
||||||
|
onCheckedChange = { isChecked ->
|
||||||
|
switchChecked = isChecked
|
||||||
|
SystemParamsOperation.getInstance().setReversalFlag(isChecked)
|
||||||
|
|
||||||
|
},
|
||||||
|
onClick = {}
|
||||||
|
)
|
||||||
|
DrawerItem("Function", Icons.Default.Dashboard) {
|
||||||
|
scope.launch { drawerState.close() }
|
||||||
|
onNavigateFunctions()
|
||||||
|
}
|
||||||
|
DrawerItem("Version", Icons.Default.Dashboard) {
|
||||||
|
scope.launch { drawerState.close() }
|
||||||
|
onNavigateVersion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Scaffold(
|
||||||
|
containerColor = Color.IvoryBeige, topBar = {
|
||||||
|
AppBar(
|
||||||
|
title = "Dashboard",
|
||||||
|
icon = Icons.Default.Menu,
|
||||||
|
onIconClick = { scope.launch { drawerState.open() } })
|
||||||
|
}) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
//top section
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
AdvertisingArea()
|
||||||
|
}
|
||||||
|
//center section
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
SummaryCard()
|
||||||
|
}
|
||||||
|
//bottom section
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1.5f)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
MenuGrid(
|
||||||
|
onNavigateAmount = onNavigateAmount,
|
||||||
|
onNavigateSignOn = onNavigateSignOn,
|
||||||
|
onNavigateSeeMore = onNavigateSeeMore,
|
||||||
|
onNavigateSettlement = onNavigateSettlement
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DrawerItem(
|
||||||
|
title: String,
|
||||||
|
icon: ImageVector,
|
||||||
|
showSwitch: Boolean = false, // New: Flag to enable switch mode
|
||||||
|
isChecked: Boolean = false, // New: Switch state
|
||||||
|
onCheckedChange: (Boolean) -> Unit = {}, // New: Switch callback
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
NavigationDrawerItem(
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
selected = false,
|
||||||
|
// If it's a switch item, clicking the whole row toggles the switch instead of navigating
|
||||||
|
onClick = {
|
||||||
|
if (showSwitch) {
|
||||||
|
onCheckedChange(!isChecked)
|
||||||
|
} else {
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = title
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
badge = {
|
||||||
|
if (showSwitch) {
|
||||||
|
Switch(
|
||||||
|
checked = isChecked,
|
||||||
|
onCheckedChange = onCheckedChange
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp),
|
||||||
|
colors = NavigationDrawerItemDefaults.colors(
|
||||||
|
unselectedContainerColor = androidx.compose.ui.graphics.Color.Transparent,
|
||||||
|
unselectedIconColor = Color.LegacyRed,
|
||||||
|
unselectedTextColor = Color.Black
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AdvertisingArea() {
|
||||||
|
|
||||||
|
val imageArray = listOf(
|
||||||
|
"https://i.ytimg.com/vi/eRUVxGRp1Ms/maxresdefault.jpg",
|
||||||
|
"https://i.ytimg.com/vi/AwvmgTPd7qw/maxresdefault.jpg",
|
||||||
|
"https://mma.prnewswire.com/media/2080956/SUNMI_3rd_generation_products_T3_PRO_series_V3_MIX.jpg?p=facebook"
|
||||||
|
)
|
||||||
|
val pageState = rememberPagerState(pageCount = { imageArray.size })
|
||||||
|
|
||||||
|
LaunchedEffect(pageState) {
|
||||||
|
while (true) {
|
||||||
|
delay(10000)
|
||||||
|
val nextPage = (pageState.currentPage + 1) % imageArray.size
|
||||||
|
|
||||||
|
pageState.animateScrollToPage(
|
||||||
|
page = nextPage, animationSpec = tween(durationMillis = 700)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
shape = RoundedCornerShape(0.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||||
|
) {
|
||||||
|
HorizontalPager(
|
||||||
|
state = pageState, modifier = Modifier.fillMaxSize()
|
||||||
|
) { page ->
|
||||||
|
AsyncImage(
|
||||||
|
model = imageArray[page],
|
||||||
|
contentDescription = "ads images",
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SummaryCard() {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||||
|
elevation = CardDefaults.cardElevation(4.dp)
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
SummaryItem(
|
||||||
|
title = "Sales Today",
|
||||||
|
value = "MMK 2,000,000",
|
||||||
|
subtitle = "47 Transactions",
|
||||||
|
icon = Icons.Default.BarChart,
|
||||||
|
iconBg = Color.CrimsonRed
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
color = Color.Gray,
|
||||||
|
modifier = Modifier.padding(vertical = 6.dp, horizontal = 6.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
SummaryItem(
|
||||||
|
title = "Settlement",
|
||||||
|
value = "Completed",
|
||||||
|
subtitle = "Today 11:00 PM",
|
||||||
|
icon = Icons.Default.Check,
|
||||||
|
iconBg = Color.CrimsonRed
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
VerticalDivider(color = Color.Gray, modifier = Modifier.height(160.dp))
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(0.75f)
|
||||||
|
.padding(start = 18.dp),
|
||||||
|
horizontalAlignment = Alignment.Start
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.Bottom) {
|
||||||
|
Text(
|
||||||
|
text = "Last Sync", fontSize = 12.sp
|
||||||
|
)
|
||||||
|
IconCircle(Icons.Default.Sync, Color.CrimsonRed)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(6.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "5 mins ago",
|
||||||
|
fontSize = 18.sp,
|
||||||
|
color = Color.LegacyRed,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SummaryItem(
|
||||||
|
title: String,
|
||||||
|
value: String,
|
||||||
|
subtitle: String,
|
||||||
|
icon: ImageVector,
|
||||||
|
iconBg: androidx.compose.ui.graphics.Color
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.Top, modifier = Modifier.padding(6.dp)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(title, fontSize = 12.sp)
|
||||||
|
Text(
|
||||||
|
value, fontSize = 14.sp, color = Color.LegacyRed, fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Text(subtitle, fontSize = 12.sp)
|
||||||
|
}
|
||||||
|
IconCircle(icon, iconBg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun IconCircle(
|
||||||
|
icon: ImageVector, color: androidx.compose.ui.graphics.Color
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(38.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(color.copy(alpha = 0.1f)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = color,
|
||||||
|
modifier = Modifier.size(22.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MenuGrid(
|
||||||
|
onNavigateAmount: (String) -> Unit,
|
||||||
|
onNavigateSignOn: () -> Unit,
|
||||||
|
onNavigateSeeMore: () -> Unit,
|
||||||
|
onNavigateSettlement: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
MenuCard(title = "Sale", icon = {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.ic_terminal),
|
||||||
|
contentDescription = "icon",
|
||||||
|
modifier = Modifier.size(40.dp),
|
||||||
|
tint = Color.LegacyRed
|
||||||
|
)
|
||||||
|
}, modifier = Modifier.weight(1f), onClick = { onNavigateAmount("Sale") })
|
||||||
|
MenuCard(title = "MMQR", icon = {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.ic_mmqr_logo),
|
||||||
|
contentDescription = "mmqr image",
|
||||||
|
modifier = Modifier.height(48.dp)
|
||||||
|
)
|
||||||
|
}, modifier = Modifier.weight(1f))
|
||||||
|
MenuCard("History", icon = {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.ic_history),
|
||||||
|
contentDescription = "icon",
|
||||||
|
modifier = Modifier.size(32.dp),
|
||||||
|
tint = Color.LegacyRed
|
||||||
|
)
|
||||||
|
}, modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
MenuCard(
|
||||||
|
title = "Sign On", icon = {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.ic_sign_on),
|
||||||
|
contentDescription = "icon",
|
||||||
|
modifier = Modifier.size(32.dp),
|
||||||
|
tint = Color.LegacyRed
|
||||||
|
)
|
||||||
|
}, modifier = Modifier.weight(1f), onClick = onNavigateSignOn
|
||||||
|
)
|
||||||
|
MenuCard(
|
||||||
|
title = "Settlement", icon = {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.ic_settlement),
|
||||||
|
contentDescription = "icon",
|
||||||
|
modifier = Modifier.size(32.dp),
|
||||||
|
tint = Color.LegacyRed
|
||||||
|
)
|
||||||
|
}, modifier = Modifier.weight(1f), onClick = onNavigateSettlement
|
||||||
|
)
|
||||||
|
MenuCard(
|
||||||
|
title = "See More", icon = {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.ic_see_more),
|
||||||
|
contentDescription = "icon",
|
||||||
|
modifier = Modifier.size(32.dp),
|
||||||
|
tint = Color.LegacyRed
|
||||||
|
)
|
||||||
|
}, modifier = Modifier.weight(1f), onClick = onNavigateSeeMore
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MenuCard(
|
||||||
|
title: String,
|
||||||
|
icon: @Composable () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier
|
||||||
|
.height(100.dp)
|
||||||
|
.then(
|
||||||
|
if (onClick != null) {
|
||||||
|
Modifier.clickable(onClick = onClick)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||||
|
elevation = CardDefaults.cardElevation(5.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(6.dp)
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
icon()
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.Black,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ChevronRight,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.LegacyRed,
|
||||||
|
modifier = Modifier.size(28.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@P2Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewDashboardScreen2() {
|
||||||
|
DashboardScreen2()
|
||||||
|
}
|
||||||
|
|
||||||
|
@P3Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewDashboardScreen3() {
|
||||||
|
DashboardScreen2()
|
||||||
|
}
|
||||||
@ -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,235 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.functions
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.ChevronRight
|
||||||
|
import androidx.compose.material.icons.filled.OnDeviceTraining
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.ElevatedButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||||
|
import com.mob.utsmyanmar.ui.preview.P2Preview
|
||||||
|
import com.mob.utsmyanmar.ui.preview.P3Preview
|
||||||
|
import com.mob.utsmyanmar.ui.theme.Color
|
||||||
|
import com.mob.utsmyanmar.R
|
||||||
|
@Composable
|
||||||
|
fun FunctionsScreen(
|
||||||
|
onBack: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
containerColor = Color.IvoryBeige,
|
||||||
|
topBar = {
|
||||||
|
AppBar(
|
||||||
|
title = "Settings",
|
||||||
|
icon = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
onIconClick = onBack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.padding(16.dp).verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Text(text = "General Settings")
|
||||||
|
FunctionButton(
|
||||||
|
onClick = {},
|
||||||
|
title = "App Version",
|
||||||
|
subTitle = "1.0-uat",
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
painter = painterResource(R.drawable.ic_device_info),
|
||||||
|
contentDescription = "icon",
|
||||||
|
tint = Color.LegacyRed
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
FunctionButton(
|
||||||
|
onClick = {},
|
||||||
|
title = "Host Config",
|
||||||
|
subTitle = "Detail for bound hosts",
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
painter = painterResource(R.drawable.ic_database_config),
|
||||||
|
contentDescription = "icon",
|
||||||
|
tint = Color.LegacyRed
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ChevronRight,
|
||||||
|
contentDescription = "icon",
|
||||||
|
tint = Color.LegacyRed
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(text = "System Configuration")
|
||||||
|
|
||||||
|
FunctionButton(
|
||||||
|
onClick = {},
|
||||||
|
title = "Clear Batch",
|
||||||
|
subTitle = "Detail for bound hosts",
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
painter = painterResource(R.drawable.ic_refresh),
|
||||||
|
contentDescription = "icon",
|
||||||
|
tint = Color.LegacyRed
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ChevronRight,
|
||||||
|
contentDescription = "icon",
|
||||||
|
tint = Color.LegacyRed
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
FunctionButton(
|
||||||
|
onClick = {},
|
||||||
|
title = "Clear Reversal",
|
||||||
|
subTitle = "Detail for bound hosts",
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
painter = painterResource(R.drawable.ic_clear),
|
||||||
|
contentDescription = "icon",
|
||||||
|
tint = Color.LegacyRed
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ChevronRight,
|
||||||
|
contentDescription = "icon",
|
||||||
|
tint = Color.LegacyRed
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
FunctionButton(
|
||||||
|
onClick = {},
|
||||||
|
title = "TMS Server Url",
|
||||||
|
subTitle = "Detail for bound hosts",
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
painter = painterResource(R.drawable.ic_address_global),
|
||||||
|
contentDescription = "icon",
|
||||||
|
tint = Color.LegacyRed
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
FunctionButton(
|
||||||
|
onClick = {},
|
||||||
|
title = "Download Config",
|
||||||
|
subTitle = "Download terminal config from host",
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
painter = painterResource(R.drawable.ic_circle_download_arrow),
|
||||||
|
contentDescription = "icon",
|
||||||
|
tint = Color.LegacyRed
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ChevronRight,
|
||||||
|
contentDescription = "icon",
|
||||||
|
tint = Color.LegacyRed
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FunctionButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
title: String,
|
||||||
|
subTitle: String,
|
||||||
|
leadingIcon: (@Composable () -> Unit)? = null,
|
||||||
|
trailingIcon: (@Composable () -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
ElevatedButton(
|
||||||
|
onClick = onClick,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color.White,
|
||||||
|
contentColor = Color.Black
|
||||||
|
),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
leadingIcon?.invoke()
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Column(modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(horizontal = 8.dp)) {
|
||||||
|
Text(text = title)
|
||||||
|
Text(
|
||||||
|
text = subTitle,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
trailingIcon?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@P3Preview
|
||||||
|
@P2Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewFunctionsScreen() {
|
||||||
|
FunctionsScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewFunctionButton() {
|
||||||
|
FunctionButton(
|
||||||
|
onClick = {},
|
||||||
|
title = "title",
|
||||||
|
subTitle = "sub-title"
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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,317 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.input_amount
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.rounded.Backspace
|
||||||
|
import androidx.compose.material.icons.rounded.Backspace
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||||
|
import com.mob.utsmyanmar.ui.preview.P2Preview
|
||||||
|
import com.mob.utsmyanmar.ui.preview.P3Preview
|
||||||
|
import com.mob.utsmyanmar.ui.theme.Color
|
||||||
|
import kotlin.collections.List
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InputAmount(
|
||||||
|
title: String = "Amount",
|
||||||
|
onBackClick: () -> Unit = {},
|
||||||
|
onChargeClick: (String) -> Unit = {}
|
||||||
|
){
|
||||||
|
|
||||||
|
var amount by remember { mutableStateOf("") }
|
||||||
|
val prefixLabel = "MMK"
|
||||||
|
val supportingText = "Enter the amount to continue"
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = { AppBar(title = "Amount") },
|
||||||
|
containerColor = Color.IvoryBeige
|
||||||
|
) {paddingValues ->
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
//first container
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.weight(2f),
|
||||||
|
){
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.align(Alignment.CenterEnd)
|
||||||
|
.clickable(enabled = true) {
|
||||||
|
amount = amount.dropLast(1)
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(18.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Backspace,
|
||||||
|
contentDescription = "Delete",
|
||||||
|
tint = Color.LegacyRed,
|
||||||
|
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Enter Amount",
|
||||||
|
color = Color.Gray,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||||
|
verticalAlignment = Alignment.Bottom
|
||||||
|
) {
|
||||||
|
if (prefixLabel.isNotBlank()) {
|
||||||
|
Text(
|
||||||
|
text = prefixLabel,
|
||||||
|
color = Color.LegacyRed,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
modifier = Modifier.padding(end = 10.dp, bottom = 6.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = formatAmount(amount.ifEmpty { "0" }),
|
||||||
|
color = Color.LegacyRed,
|
||||||
|
fontSize = 32.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(14.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = supportingText,
|
||||||
|
color = Color.Gray,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//second container
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(3f),
|
||||||
|
verticalArrangement = Arrangement.Bottom
|
||||||
|
){
|
||||||
|
NumericKeypad(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
onKeyClick = { value ->
|
||||||
|
amount = appendAmountValue(amount, value)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
//third container
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(0.5f)
|
||||||
|
){
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = {},
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(56.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color.White,
|
||||||
|
contentColor = Color.LegacyRed
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (amount.isNotEmpty()) {
|
||||||
|
onChargeClick(amount)
|
||||||
|
} },
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(56.dp),
|
||||||
|
enabled = amount.isNotEmpty(),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color.LegacyRed,
|
||||||
|
contentColor = Color.White,
|
||||||
|
disabledContainerColor = Color.LegacyRed.copy(alpha = 0.5f),
|
||||||
|
disabledContentColor = Color.White
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Next",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NumericKeypad(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onKeyClick: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val keys : List<List<String>> = listOf(
|
||||||
|
listOf("1", "2", "3"),
|
||||||
|
listOf("4", "5", "6"),
|
||||||
|
listOf("7", "8", "9"),
|
||||||
|
listOf(".", "0", "00")
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
keys.forEach { row ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().weight(1f),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
row.forEach { key ->
|
||||||
|
KeypadButton(
|
||||||
|
text = key,
|
||||||
|
modifier = Modifier.weight(1f).fillMaxHeight(),
|
||||||
|
onClick = { onKeyClick(key) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun KeypadButton(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val enabled = text.isNotBlank()
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.shadow(
|
||||||
|
elevation = 2.dp,
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
clip = false
|
||||||
|
)
|
||||||
|
.background(
|
||||||
|
color = Color.White,
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.clickable(enabled = enabled) { onClick() },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = if (enabled) Color.LegacyRed else Color.White,
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun appendAmountValue(current: String, value: String): String {
|
||||||
|
if (value == ".") {
|
||||||
|
if (current.contains(".")) return current
|
||||||
|
return if (current.isEmpty()) "0." else "$current."
|
||||||
|
}
|
||||||
|
|
||||||
|
val decimalIndex = current.indexOf('.')
|
||||||
|
return if (decimalIndex >= 0) {
|
||||||
|
val decimalPart = current.substring(decimalIndex + 1)
|
||||||
|
val remainingDecimalDigits = 2 - decimalPart.length
|
||||||
|
if (remainingDecimalDigits <= 0) {
|
||||||
|
current
|
||||||
|
} else {
|
||||||
|
current + value.take(remainingDecimalDigits)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val wholeDigitsCount = current.filter(Char::isDigit).length
|
||||||
|
val remainingWholeDigits = 9 - wholeDigitsCount
|
||||||
|
if (remainingWholeDigits <= 0) {
|
||||||
|
current
|
||||||
|
} else {
|
||||||
|
current + value.take(remainingWholeDigits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatAmount(value: String): String {
|
||||||
|
val normalized = value.ifEmpty { "0" }
|
||||||
|
val wholePart = normalized.substringBefore(".").ifEmpty { "0" }
|
||||||
|
val groupedWholePart = "%,d".format(wholePart.toLongOrNull() ?: 0L)
|
||||||
|
|
||||||
|
if (!normalized.contains(".")) {
|
||||||
|
return groupedWholePart
|
||||||
|
}
|
||||||
|
|
||||||
|
val decimalPart = normalized.substringAfter(".", "")
|
||||||
|
return if (normalized.endsWith(".")) {
|
||||||
|
"$groupedWholePart."
|
||||||
|
} else {
|
||||||
|
"$groupedWholePart.$decimalPart"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@P2Preview
|
||||||
|
@P3Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewInputAmount(){
|
||||||
|
InputAmount();
|
||||||
|
}
|
||||||
@ -1,34 +1,285 @@
|
|||||||
package com.mob.utsmyanmar.ui.navigation
|
package com.mob.utsmyanmar.ui.navigation
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.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.input_amount.AmountRoute
|
||||||
|
import com.mob.utsmyanmar.ui.cardwaiting.CardWaitingScreen
|
||||||
|
import com.mob.utsmyanmar.ui.cardwaiting.CardWaitingViewModel
|
||||||
|
import com.mob.utsmyanmar.ui.dashboard.DashboardScreen2
|
||||||
|
import com.mob.utsmyanmar.ui.dashboard.SeeMoreScreen
|
||||||
|
import com.mob.utsmyanmar.ui.device_info.DeviceInfoViewModel
|
||||||
|
import com.mob.utsmyanmar.ui.pinpad.PinPadRoute
|
||||||
|
import com.mob.utsmyanmar.ui.processing_card.ProcessingCardRoute
|
||||||
|
import com.mob.utsmyanmar.ui.processing_card.ProcessingCardViewModel
|
||||||
|
import com.mob.utsmyanmar.ui.print_receipt.PrintReceiptScreen
|
||||||
|
import com.mob.utsmyanmar.ui.refund_rrn.InputRrnRoute
|
||||||
|
import com.mob.utsmyanmar.ui.sign_on.SignOnResultScreen
|
||||||
|
import com.mob.utsmyanmar.ui.sign_on.SignOnRoute
|
||||||
|
import com.mob.utsmyanmar.ui.sending_to_host.ProcessingRoute
|
||||||
|
import com.mob.utsmyanmar.ui.settlement.SettlementScreen
|
||||||
|
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultRoute
|
||||||
|
import com.mob.utsmyanmar.ui.sale_void.TranDetailPage
|
||||||
|
import com.mob.utsmyanmar.ui.sale_void.VoidViewModel
|
||||||
|
import com.mob.utsmyanmar.ui.sale_void.VoidTraceScreen
|
||||||
|
import com.mob.utsmyanmar.viewmodel.CardReaderViewModel
|
||||||
|
import com.mob.utsmyanmar.viewmodel.EmvTransactionProcessViewModel
|
||||||
|
import com.mob.utsmyanmar.ui.pinpad.PinPadViewModel
|
||||||
|
import com.mob.utsmyanmar.ui.settlement.SettlementViewModel
|
||||||
|
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultEvent
|
||||||
|
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultViewModel
|
||||||
|
import com.mob.utsmyanmar.ui.functions.FunctionsScreen
|
||||||
|
import com.mob.utsmyanmar.ui.refund_rrn.RRNViewModel
|
||||||
|
import com.mob.utsmyanmar.ui.tms_setup.TmsSetupRoute
|
||||||
|
import com.mob.utsmyanmar.ui.tms_setup.TmsSetupViewModel
|
||||||
|
import com.mob.utsmyanmar.ui.version.VersionScreen
|
||||||
|
import com.mob.utsmyanmar.viewmodel.SharedViewModel
|
||||||
|
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
|
||||||
|
import com.utsmyanmar.ecr.data.TransType
|
||||||
|
import com.utsmyanmar.paylibs.model.TradeData
|
||||||
|
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||||
|
|
||||||
|
@SuppressLint("ContextCastToActivity")
|
||||||
@Composable
|
@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,
|
DashboardScreen2(
|
||||||
wavePayEnabled = true,
|
onNavigateAmount = { action ->
|
||||||
onAmountClick = { action ->
|
if(action == "Sale"){
|
||||||
navController.navigate(Routes.Amount.createRoute(action))
|
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 = {}
|
},
|
||||||
|
onNavigateSeeMore = {
|
||||||
|
navController.navigate(Routes.SeeMore.route) {
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNavigateSettlement = {
|
||||||
|
navController.navigate(Routes.Settlement.route) {
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNavigateVersion = {
|
||||||
|
navController.navigate(Routes.Version.route)
|
||||||
|
},
|
||||||
|
onNavigateFunctions = {
|
||||||
|
navController.navigate(Routes.Functions.route) {
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(Routes.SeeMore.route) {
|
||||||
|
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||||
|
SeeMoreScreen(
|
||||||
|
onBack = { navController.popBackStack() },
|
||||||
|
onNavigateAmount = { action ->
|
||||||
|
when (action) {
|
||||||
|
"Void" -> {
|
||||||
|
sharedViewModel.transactionsType.value = TransactionsType.VOID
|
||||||
|
navController.navigate(Routes.VoidTrace.route) {
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"Pre-Auth Complete Void" -> {
|
||||||
|
sharedViewModel.transactionsType.value = TransactionsType.PRE_AUTH_COMPLETE_VOID
|
||||||
|
navController.navigate(Routes.VoidTrace.route) {
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
navController.navigate(Routes.Amount.createRoute(action)) {
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(Routes.Version.route){
|
||||||
|
val deviceInfoViewModel: DeviceInfoViewModel = hiltViewModel();
|
||||||
|
VersionScreen(
|
||||||
|
onBack = {navController.popBackStack()},
|
||||||
|
deviceInfoViewModel = deviceInfoViewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(Routes.Functions.route) {
|
||||||
|
FunctionsScreen(
|
||||||
|
onBack = { navController.popBackStack() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(Routes.VoidTrace.route) {
|
||||||
|
val voidViewModel: VoidViewModel = hiltViewModel()
|
||||||
|
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||||
|
VoidTraceScreen(
|
||||||
|
sharedViewModel = sharedViewModel,
|
||||||
|
voidViewModel = voidViewModel,
|
||||||
|
onNavigateTranDetail = { trace ->
|
||||||
|
navController.navigate(Routes.VoidTranDetail.createRoute(trace)) {
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onBack = { navController.popBackStack() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(Routes.Settlement.route) {
|
||||||
|
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||||
|
val settlementViewMode: SettlementViewModel = hiltViewModel(activity)
|
||||||
|
|
||||||
|
SettlementScreen(
|
||||||
|
sharedViewModel = sharedViewModel,
|
||||||
|
settlementViewMode = settlementViewMode,
|
||||||
|
onBack = { navController.popBackStack(Routes.Dashboard.route,false) },
|
||||||
|
onStartSettlement = {
|
||||||
|
sharedViewModel.transactionsType.value = TransactionsType.SETTLEMENT
|
||||||
|
navController.navigate(Routes.Processing.route) {
|
||||||
|
popUpTo(Routes.Settlement.route) {
|
||||||
|
inclusive = true
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = Routes.VoidTranDetail.route,
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("trace") {
|
||||||
|
type = NavType.StringType
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
val voidViewModel: VoidViewModel = hiltViewModel()
|
||||||
|
val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity)
|
||||||
|
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||||
|
val trace = backStackEntry.arguments?.getString("trace").orEmpty()
|
||||||
|
|
||||||
|
TranDetailPage(
|
||||||
|
voidViewModel = voidViewModel,
|
||||||
|
trace = trace,
|
||||||
|
onBack = { navController.popBackStack() },
|
||||||
|
onProceedVoid = { payDetail ->
|
||||||
|
if(sharedViewModel.transactionsType.value == TransactionsType.VOID) {
|
||||||
|
transProcessViewModel.setTransType(TransactionsType.VOID)
|
||||||
|
payDetail.transType = TransactionsType.VOID.name
|
||||||
|
payDetail.transactionType = TransactionsType.VOID.value
|
||||||
|
} else {
|
||||||
|
transProcessViewModel.setTransType(TransactionsType.PRE_AUTH_COMPLETE_VOID)
|
||||||
|
payDetail.transType = TransactionsType.PRE_AUTH_COMPLETE_VOID.name
|
||||||
|
payDetail.transactionType = TransactionsType.PRE_AUTH_COMPLETE_VOID.value
|
||||||
|
}
|
||||||
|
|
||||||
|
val tradeData = TradeData()
|
||||||
|
tradeData.payDetail = payDetail
|
||||||
|
transProcessViewModel.setTradeData(tradeData)
|
||||||
|
sharedViewModel.payDetail.value = payDetail
|
||||||
|
navController.navigate(Routes.Processing.route) {
|
||||||
|
popUpTo(Routes.VoidTranDetail.route) {
|
||||||
|
inclusive = true
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(Routes.SignOn.route) {
|
||||||
|
SignOnRoute(
|
||||||
|
onBack = { navController.popBackStack() },
|
||||||
|
onNavigateResult = { isSuccess, message ->
|
||||||
|
navController.navigate(Routes.SignOnResult.createRoute(isSuccess, message)) {
|
||||||
|
popUpTo(Routes.SignOn.route) {
|
||||||
|
inclusive = true
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = Routes.SignOnResult.route,
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("isSuccess") {
|
||||||
|
type = NavType.BoolType
|
||||||
|
},
|
||||||
|
navArgument("message") {
|
||||||
|
type = NavType.StringType
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
val isSuccess = backStackEntry.arguments?.getBoolean("isSuccess") ?: false
|
||||||
|
val message = backStackEntry.arguments?.getString("message").orEmpty()
|
||||||
|
|
||||||
|
SignOnResultScreen(
|
||||||
|
isSuccess = isSuccess,
|
||||||
|
message = message,
|
||||||
|
onBack = { navController.popBackStack() },
|
||||||
|
onDone = {
|
||||||
|
navController.navigate(Routes.Dashboard.route) {
|
||||||
|
popUpTo(Routes.Dashboard.route) {
|
||||||
|
inclusive = false
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRetry = {
|
||||||
|
navController.navigate(Routes.SignOn.route) {
|
||||||
|
popUpTo(Routes.SignOnResult.route) {
|
||||||
|
inclusive = true
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,10 +291,232 @@ fun AppNavGraph(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
) { backStackEntry ->
|
) { 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() },
|
||||||
|
onNavigateMain = {
|
||||||
|
navController.navigate(Routes.Dashboard.route) {
|
||||||
|
popUpTo(Routes.Dashboard.route) {
|
||||||
|
inclusive = false
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(Routes.InputRrn.route) {
|
||||||
|
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||||
|
val rrnViewModel: RRNViewModel = hiltViewModel(activity)
|
||||||
|
val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity)
|
||||||
|
InputRrnRoute(
|
||||||
|
transProcessViewModel = transProcessViewModel,
|
||||||
|
rrnViewModel = rrnViewModel,
|
||||||
|
sharedViewModel = sharedViewModel,
|
||||||
|
onBack = { navController.popBackStack() },
|
||||||
|
onNavigateProcessing = {
|
||||||
|
navController.navigate(Routes.Processing.route) {
|
||||||
|
popUpTo(Routes.InputRrn.route) {
|
||||||
|
inclusive = true
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(Routes.Processing.route) {
|
||||||
|
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||||
|
val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity)
|
||||||
|
val settlementViewModel: SettlementViewModel = hiltViewModel(activity)
|
||||||
|
ProcessingRoute(
|
||||||
|
sharedViewModel = sharedViewModel,
|
||||||
|
transProcessViewModel = transProcessViewModel,
|
||||||
|
settlementViewModel = settlementViewModel,
|
||||||
|
onNavigateTransactionResult = {
|
||||||
|
navController.navigate(Routes.TransactionResult.route) {
|
||||||
|
popUpTo(Routes.Processing.route) {
|
||||||
|
inclusive = true
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNavigateMain = {
|
||||||
|
navController.navigate(Routes.Dashboard.route) {
|
||||||
|
popUpTo(Routes.Dashboard.route) {
|
||||||
|
inclusive = false
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(Routes.TransactionResult.route) {
|
||||||
|
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||||
|
val transResultViewModel: TransactionResultViewModel = hiltViewModel(activity)
|
||||||
|
TransactionResultRoute(
|
||||||
|
viewModel = transResultViewModel,
|
||||||
|
sharedViewModel = sharedViewModel,
|
||||||
|
onNavigateMain = {
|
||||||
|
navController.navigate(Routes.Dashboard.route) {
|
||||||
|
popUpTo(Routes.Dashboard.route) {
|
||||||
|
inclusive = false
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNavigatePrintReceipt = {
|
||||||
|
navController.navigate(Routes.PrintReceipt.route) {
|
||||||
|
popUpTo(Routes.TransactionResult.route) {
|
||||||
|
inclusive = false
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onShowError = {},
|
||||||
|
onShowSuccess = {},
|
||||||
|
onShowPrinterDialog = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(Routes.PrintReceipt.route) {
|
||||||
|
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||||
|
val transResultViewModel: TransactionResultViewModel = hiltViewModel(activity)
|
||||||
|
PrintReceiptScreen(
|
||||||
|
sharedViewModel = sharedViewModel,
|
||||||
|
transactionResultViewModel = transResultViewModel,
|
||||||
|
onPrint = {
|
||||||
|
transResultViewModel.onEvent(
|
||||||
|
TransactionResultEvent.RetryPrint,
|
||||||
|
sharedViewModel
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDone = {
|
||||||
|
navController.navigate(Routes.Dashboard.route) {
|
||||||
|
popUpTo(Routes.Dashboard.route) {
|
||||||
|
inclusive = false
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun formatAmountForDisplay(amount: String?): String {
|
||||||
|
val normalizedAmount = amount.orEmpty()
|
||||||
|
val value = normalizedAmount.toLongOrNull() ?: return normalizedAmount.ifBlank { "0" }
|
||||||
|
return "%,d".format(value)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,8 +1,32 @@
|
|||||||
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")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,93 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.pinpad
|
||||||
|
|
||||||
|
import android.text.TextUtils
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.mob.utsmyanmar.model.PinPadStatus
|
||||||
|
import com.mob.utsmyanmar.viewmodel.SharedViewModel
|
||||||
|
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
|
||||||
|
import com.utsmyanmar.paylibs.Constant
|
||||||
|
import com.utsmyanmar.paylibs.model.TradeData
|
||||||
|
import com.utsmyanmar.paylibs.utils.POSUtil
|
||||||
|
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PinPadRoute(
|
||||||
|
pinPadViewModel: PinPadViewModel,
|
||||||
|
sharedViewModel: SharedViewModel,
|
||||||
|
transProcessViewModel: TransProcessViewModel,
|
||||||
|
onNavigateInputRrn: () -> Unit,
|
||||||
|
onNavigateProcessing: () -> Unit,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onNavigateMain: () -> Unit,
|
||||||
|
) {
|
||||||
|
val pinText by pinPadViewModel.pinText.collectAsStateWithLifecycle()
|
||||||
|
val alertMsg by pinPadViewModel.alertMsg.collectAsStateWithLifecycle()
|
||||||
|
val pinStatus by pinPadViewModel.pinStatus.collectAsStateWithLifecycle()
|
||||||
|
val canGoBack = sharedViewModel.transactionsType.value != TransactionsType.SALE
|
||||||
|
|
||||||
|
BackHandler(enabled = canGoBack) {
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
|
||||||
|
val tradeData: TradeData = pinPadViewModel.getTradeData()!!
|
||||||
|
val payDetail = tradeData.getPayDetail()
|
||||||
|
|
||||||
|
val processCode = sharedViewModel.processCode.getValue()
|
||||||
|
val amount = sharedViewModel.amount.getValue()
|
||||||
|
payDetail.setAmount(POSUtil.getInstance().convertAmount(amount))
|
||||||
|
if (!TextUtils.equals(processCode, "")) {
|
||||||
|
payDetail.setProcessCode(processCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(pinStatus) {
|
||||||
|
when (pinStatus) {
|
||||||
|
PinPadStatus.ON_CONFIRM,
|
||||||
|
PinPadStatus.ON_NEXT_SCREEN -> {
|
||||||
|
val payDetail = pinPadViewModel.getPayDetail()
|
||||||
|
payDetail?.tradeAnswerCode = Constant.ANSWER_CODE_APPROVED
|
||||||
|
sharedViewModel.payDetail.value = payDetail
|
||||||
|
transProcessViewModel.resetTransactionStatus()
|
||||||
|
if (sharedViewModel.transactionsType.value == TransactionsType.REFUND || sharedViewModel.transactionsType.value == TransactionsType.PRE_AUTH_VOID || sharedViewModel.transactionsType.value == TransactionsType.PRE_AUTH_COMPLETE) {
|
||||||
|
onNavigateInputRrn()
|
||||||
|
} else {
|
||||||
|
onNavigateProcessing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PinPadStatus.ON_CANCEL,
|
||||||
|
PinPadStatus.ON_TIMEOUT,
|
||||||
|
PinPadStatus.ON_CARD_REMOVED -> {
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
PinPadStatus.ON_ERROR -> {
|
||||||
|
onNavigateMain()
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
pinPadViewModel.cancelPinPad()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PinPadScreen(
|
||||||
|
pinText = pinText,
|
||||||
|
alertMessage = alertMsg,
|
||||||
|
canGoBack = canGoBack,
|
||||||
|
onBack = onBack,
|
||||||
|
onKeyboardReady = { keyboard ->
|
||||||
|
pinPadViewModel.startPinPadProcess(keyboard)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
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,565 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.pinpad
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.os.RemoteException
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewTreeObserver
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.mob.utsmyanmar.config.Constants
|
||||||
|
import com.mob.utsmyanmar.config.SunmiPayManager
|
||||||
|
import com.mob.utsmyanmar.model.PinPadStatus
|
||||||
|
import com.sunmi.pay.hardware.aidl.AidlConstants
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.AidlErrorCodeV2
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.bean.PinPadConfigV2
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.bean.PinPadDataV2
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.pinpad.PinPadListenerV2
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.pinpad.PinPadOptV2
|
||||||
|
import com.utsmyanmar.baselib.ui.CustomPinPadKeyboard
|
||||||
|
import com.utsmyanmar.checkxread.sdk.SunmiSDK
|
||||||
|
import com.utsmyanmar.paylibs.model.PayDetail
|
||||||
|
import com.utsmyanmar.paylibs.model.TradeData
|
||||||
|
import com.utsmyanmar.paylibs.utils.core_utils.ByteUtil
|
||||||
|
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
|
||||||
|
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import sunmi.sunmiui.utils.LogUtil
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class PinPadViewModel @Inject constructor(
|
||||||
|
private val sunmiPayManager: SunmiPayManager
|
||||||
|
) : ViewModel() {
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "PinPadViewModel"
|
||||||
|
private const val PIN_CONFIRM_SUCCESS_STATUS = 0
|
||||||
|
private const val ON_CONFIRM_CLICK = 2
|
||||||
|
private const val ON_CANCEL_CLICK = 3
|
||||||
|
private const val ON_ERROR_PIN_PAD = 4
|
||||||
|
private const val ON_NUMBER_CLICK = 5
|
||||||
|
private const val ON_EMPTY_PIN_BLOCK = 6
|
||||||
|
private const val ON_TIMEOUT_PIN_PAD = 7
|
||||||
|
private const val ON_ERROR_DUKPT = 8
|
||||||
|
}
|
||||||
|
|
||||||
|
private var mPinPadOptV2: PinPadOptV2? = null
|
||||||
|
private var mPinType = 0
|
||||||
|
private var mWidth = 239
|
||||||
|
private var mHeight = 130
|
||||||
|
private var mInterval = 1
|
||||||
|
private var mKeyboardCoordinate = intArrayOf(0, 661)
|
||||||
|
private var mCancelWidth = 112
|
||||||
|
private var mCancelHeight = 112
|
||||||
|
private var mCancelCoordinate = intArrayOf(0, 48)
|
||||||
|
private var dukptIndex = 0
|
||||||
|
private var tmkIndex = 9
|
||||||
|
private val PIK_INDEX = 11
|
||||||
|
private var tradeData: TradeData? = null
|
||||||
|
private var payDetail: PayDetail? = null
|
||||||
|
private var pan: String = ""
|
||||||
|
|
||||||
|
/*
|
||||||
|
* UI States
|
||||||
|
*/
|
||||||
|
|
||||||
|
private val _pinText = MutableStateFlow("")
|
||||||
|
val pinText = _pinText.asStateFlow()
|
||||||
|
|
||||||
|
private val _alertMsg = MutableStateFlow<String?>(null)
|
||||||
|
val alertMsg = _alertMsg.asStateFlow()
|
||||||
|
|
||||||
|
private val _errorCode = MutableStateFlow<Int?>(null)
|
||||||
|
val errorCode = _errorCode.asStateFlow()
|
||||||
|
|
||||||
|
private val _transType = MutableStateFlow<TransactionsType?>(null)
|
||||||
|
val transType = _transType.asStateFlow()
|
||||||
|
|
||||||
|
private val _pinStatus =
|
||||||
|
MutableStateFlow<PinPadStatus?>(null)
|
||||||
|
|
||||||
|
val pinStatus = _pinStatus.asStateFlow()
|
||||||
|
private var isTerminalStateReached = false
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Trade Data
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun setTradeData(tradeData: TradeData) {
|
||||||
|
this.tradeData = tradeData
|
||||||
|
|
||||||
|
payDetail = tradeData.payDetail
|
||||||
|
|
||||||
|
pan = payDetail?.cardNo ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTradeData(): TradeData? {
|
||||||
|
return tradeData
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPayDetail(payDetail: PayDetail) {
|
||||||
|
this.payDetail = payDetail
|
||||||
|
|
||||||
|
tradeData = TradeData().apply {
|
||||||
|
this.payDetail = payDetail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPayDetail(): PayDetail? {
|
||||||
|
return payDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTransType(type: TransactionsType?) {
|
||||||
|
_transType.value = type
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetSessionState() {
|
||||||
|
_pinText.value = ""
|
||||||
|
_alertMsg.value = null
|
||||||
|
_errorCode.value = null
|
||||||
|
_pinStatus.value = null
|
||||||
|
isTerminalStateReached = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Pin Pad
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun startPinPadProcess(
|
||||||
|
customPinPadKeyboard: CustomPinPadKeyboard
|
||||||
|
) {
|
||||||
|
resetSessionState()
|
||||||
|
|
||||||
|
// dukptIndex = SystemParamsOperation.getInstance().tmkIndex.toInt()
|
||||||
|
if(!sunmiPayManager.isReady()){
|
||||||
|
_alertMsg.value =
|
||||||
|
"Sunmi Pay SDK is not ready"
|
||||||
|
|
||||||
|
_pinStatus.value =
|
||||||
|
PinPadStatus.ON_ERROR
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
initData()
|
||||||
|
// testInjectPIK()
|
||||||
|
initPinPad(customPinPadKeyboard)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initData() {
|
||||||
|
|
||||||
|
mPinPadOptV2 = sunmiPayManager.pinPadOptV2
|
||||||
|
|
||||||
|
if (mPinPadOptV2 == null) {
|
||||||
|
Log.d(TAG, "PinPad service are not ready!")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
val result = mPinPadOptV2?.setAntiExhaustiveProtectionMode(3)
|
||||||
|
|
||||||
|
if ((result ?: -1) >= 0) {
|
||||||
|
LogUtil.d(
|
||||||
|
TAG,
|
||||||
|
"Pin anti exhaustive result:$result"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LogUtil.d(
|
||||||
|
TAG, "Pin Anti Exhaustive failed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: RemoteException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelPinPad() {
|
||||||
|
try {
|
||||||
|
if (!isTerminalStateReached) {
|
||||||
|
mPinPadOptV2?.cancelInputPin()
|
||||||
|
Log.d(TAG, "PinPad Canceled")
|
||||||
|
}
|
||||||
|
} catch (e: RemoteException) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Init Pin Pad
|
||||||
|
*/
|
||||||
|
|
||||||
|
private fun initPinPad(
|
||||||
|
customPinPadKeyboard: CustomPinPadKeyboard
|
||||||
|
) {
|
||||||
|
LogUtil.e(TAG, "Init Pin Pad PAN:$pan")
|
||||||
|
|
||||||
|
var timeout = Constants.PIN_PAD_TIMEOUT
|
||||||
|
val pinPadOrder = !SystemParamsOperation.getInstance().isRandomPinPad
|
||||||
|
|
||||||
|
if (SunmiSDK.getInstance().checkCardExist() != 2) {
|
||||||
|
timeout = Constants.TIMEOUT
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val config = PinPadConfigV2().apply {
|
||||||
|
maxInput = 6
|
||||||
|
minInput = 0
|
||||||
|
pinPadType = 1 // custom keyboard
|
||||||
|
algorithmType = 0
|
||||||
|
pinType = mPinType
|
||||||
|
this.timeout = timeout * 1000
|
||||||
|
isOrderNumKey = pinPadOrder
|
||||||
|
keySystem = AidlConstants.Security.SEC_MKSK
|
||||||
|
pinKeyIndex = PIK_INDEX
|
||||||
|
pinblockFormat = AidlConstants.PinBlockFormat.SEC_PIN_BLK_ISO_FMT0
|
||||||
|
this.pan = getPanBytes(this@PinPadViewModel.pan)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = mPinPadOptV2?.initPinPad(config, mPinPadListener)
|
||||||
|
|
||||||
|
LogUtil.e(TAG, "pinpad result:$result")
|
||||||
|
|
||||||
|
if (result.isNullOrEmpty()) {
|
||||||
|
_alertMsg.value = "PinPad init failed"
|
||||||
|
_pinStatus.value = PinPadStatus.ON_ERROR
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getKeyboardCoordinate(result, customPinPadKeyboard)
|
||||||
|
|
||||||
|
_pinText.value = ""
|
||||||
|
customPinPadKeyboard.keepScreenOn = true
|
||||||
|
customPinPadKeyboard.setKeyBoard(result)
|
||||||
|
customPinPadKeyboard.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
_alertMsg.value = e.message
|
||||||
|
_pinStatus.value = PinPadStatus.ON_ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testInjectPIK() {
|
||||||
|
try {
|
||||||
|
val securityOptV2 = sunmiPayManager.securityOptV2 ?: return
|
||||||
|
|
||||||
|
val pik = ByteUtil.hexStr2Bytes(
|
||||||
|
"33DD20C9A0B5B861F2914D44BC2AF055"
|
||||||
|
)
|
||||||
|
|
||||||
|
val kcv = ByteUtil.hexStr2Bytes(
|
||||||
|
"28DBDB489D28BC92"
|
||||||
|
)
|
||||||
|
|
||||||
|
val code = securityOptV2.savePlaintextKey(
|
||||||
|
AidlConstants.Security.KEY_TYPE_PIK,
|
||||||
|
pik,
|
||||||
|
kcv,
|
||||||
|
AidlConstants.Security.KEY_ALG_TYPE_3DES,
|
||||||
|
PIK_INDEX
|
||||||
|
)
|
||||||
|
|
||||||
|
LogUtil.e(TAG, "saveTestPIK result:$code")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Keyboard
|
||||||
|
*/
|
||||||
|
|
||||||
|
private fun getKeyboardCoordinate(
|
||||||
|
keyBoardText: String,
|
||||||
|
customPinPadKeyboard: CustomPinPadKeyboard
|
||||||
|
) {
|
||||||
|
|
||||||
|
customPinPadKeyboard
|
||||||
|
.viewTreeObserver
|
||||||
|
.addOnGlobalLayoutListener(
|
||||||
|
object : ViewTreeObserver.OnGlobalLayoutListener {
|
||||||
|
|
||||||
|
override fun onGlobalLayout() {
|
||||||
|
|
||||||
|
customPinPadKeyboard
|
||||||
|
.viewTreeObserver
|
||||||
|
.removeOnGlobalLayoutListener(this)
|
||||||
|
|
||||||
|
val textView: TextView =
|
||||||
|
customPinPadKeyboard.key_0
|
||||||
|
|
||||||
|
textView.getLocationOnScreen(
|
||||||
|
mKeyboardCoordinate
|
||||||
|
)
|
||||||
|
|
||||||
|
mWidth = textView.width
|
||||||
|
mHeight = textView.height
|
||||||
|
|
||||||
|
mInterval = 1
|
||||||
|
|
||||||
|
importPinPadData(keyBoardText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun importPinPadData(text: String) {
|
||||||
|
|
||||||
|
val pinPadData = PinPadDataV2()
|
||||||
|
|
||||||
|
pinPadData.numX = mKeyboardCoordinate[0]
|
||||||
|
pinPadData.numY = mKeyboardCoordinate[1]
|
||||||
|
|
||||||
|
pinPadData.numW = mWidth
|
||||||
|
pinPadData.numH = mHeight
|
||||||
|
|
||||||
|
pinPadData.lineW = mInterval
|
||||||
|
|
||||||
|
pinPadData.cancelX = mCancelCoordinate[0]
|
||||||
|
pinPadData.cancelY = mCancelCoordinate[1]
|
||||||
|
|
||||||
|
pinPadData.cancelW = mCancelWidth
|
||||||
|
pinPadData.cancelH = mCancelHeight
|
||||||
|
|
||||||
|
pinPadData.lineW = 0
|
||||||
|
|
||||||
|
pinPadData.rows = 5
|
||||||
|
pinPadData.clos = 3
|
||||||
|
|
||||||
|
keyMap(text, pinPadData)
|
||||||
|
|
||||||
|
try {
|
||||||
|
mPinPadOptV2?.importPinPadData(pinPadData)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun keyMap(
|
||||||
|
str: String,
|
||||||
|
data: PinPadDataV2
|
||||||
|
) {
|
||||||
|
|
||||||
|
data.keyMap = ByteArray(64)
|
||||||
|
|
||||||
|
var j = 0
|
||||||
|
|
||||||
|
for (i in 0 until 15) {
|
||||||
|
|
||||||
|
when (i) {
|
||||||
|
|
||||||
|
9, 12 -> {
|
||||||
|
data.keyMap[i] = 0x1B
|
||||||
|
}
|
||||||
|
|
||||||
|
13 -> {
|
||||||
|
data.keyMap[i] = 0x0C
|
||||||
|
}
|
||||||
|
|
||||||
|
11, 14 -> {
|
||||||
|
data.keyMap[i] = 0x0D
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
data.keyMap[i] =
|
||||||
|
str[j].code.toByte()
|
||||||
|
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Password View
|
||||||
|
*/
|
||||||
|
|
||||||
|
private fun showPasswordView(len: Int) {
|
||||||
|
|
||||||
|
val sb = StringBuilder()
|
||||||
|
|
||||||
|
repeat(len) {
|
||||||
|
sb.append("*")
|
||||||
|
}
|
||||||
|
|
||||||
|
_pinText.value = sb.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Handler
|
||||||
|
*/
|
||||||
|
|
||||||
|
private val handler =
|
||||||
|
Handler(Looper.getMainLooper()) { msg ->
|
||||||
|
when (msg.what) {
|
||||||
|
ON_NUMBER_CLICK -> {
|
||||||
|
showPasswordView(msg.arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
ON_CONFIRM_CLICK -> {
|
||||||
|
LogUtil.d(TAG, "ON CLICK CONFIRM")
|
||||||
|
|
||||||
|
if (
|
||||||
|
transType.value == TransactionsType.PRE_AUTH_COMPLETE ||
|
||||||
|
transType.value == TransactionsType.PRE_AUTH_VOID ||
|
||||||
|
transType.value == TransactionsType.REFUND
|
||||||
|
) {
|
||||||
|
isTerminalStateReached = true
|
||||||
|
_pinStatus.value = PinPadStatus.ON_NEXT_SCREEN
|
||||||
|
} else {
|
||||||
|
isTerminalStateReached = true
|
||||||
|
_pinStatus.value = PinPadStatus.ON_CONFIRM
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ON_CANCEL_CLICK -> {
|
||||||
|
LogUtil.d(TAG, "ON CLICK CANCEL")
|
||||||
|
if (!isTerminalStateReached) {
|
||||||
|
_pinStatus.value = PinPadStatus.ON_CANCEL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ON_ERROR_PIN_PAD -> {
|
||||||
|
LogUtil.d(TAG, "ON ERROR CODE: ${msg.arg1}")
|
||||||
|
_errorCode.value = msg.arg1
|
||||||
|
isTerminalStateReached = true
|
||||||
|
_pinStatus.value = PinPadStatus.ON_ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
ON_EMPTY_PIN_BLOCK -> {
|
||||||
|
_pinStatus.value = PinPadStatus.ON_EMPTY
|
||||||
|
}
|
||||||
|
|
||||||
|
ON_TIMEOUT_PIN_PAD -> {
|
||||||
|
isTerminalStateReached = true
|
||||||
|
_pinStatus.value = PinPadStatus.ON_TIMEOUT
|
||||||
|
}
|
||||||
|
|
||||||
|
ON_ERROR_DUKPT -> {
|
||||||
|
_alertMsg.value = "Try Again!"
|
||||||
|
isTerminalStateReached = true
|
||||||
|
_pinStatus.value = PinPadStatus.ON_ERROR_DUKPT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Listener
|
||||||
|
*/
|
||||||
|
|
||||||
|
private val mPinPadListener =
|
||||||
|
object : PinPadListenerV2.Stub() {
|
||||||
|
|
||||||
|
override fun onPinLength(len: Int) {
|
||||||
|
handler.obtainMessage(
|
||||||
|
ON_NUMBER_CLICK,
|
||||||
|
len,
|
||||||
|
0
|
||||||
|
).sendToTarget()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfirm(
|
||||||
|
status: Int,
|
||||||
|
pinBlock: ByteArray?
|
||||||
|
) {
|
||||||
|
|
||||||
|
LogUtil.e(
|
||||||
|
TAG,
|
||||||
|
"onConfirm status:$status and pinblock :${ByteUtil.bytes2HexStr(pinBlock)}"
|
||||||
|
)
|
||||||
|
if (status != PIN_CONFIRM_SUCCESS_STATUS) {
|
||||||
|
_alertMsg.value = "PinPad confirm failed: $status"
|
||||||
|
_pinStatus.value = PinPadStatus.ON_ERROR
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// val isCardValid = SunmiSDK.getInstance().checkCardExist() == 2 ||
|
||||||
|
// payDetail?.cardType == AidlConstants.CardType.MAGNETIC.getValue() ||
|
||||||
|
// payDetail?.cardType == -9
|
||||||
|
//
|
||||||
|
// if (!isCardValid) {
|
||||||
|
// isTerminalStateReached = true
|
||||||
|
// _pinStatus.value = PinPadStatus.ON_CARD_REMOVED
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (pinBlock != null) {
|
||||||
|
payDetail?.pinCipher = ByteUtil.bytes2HexStr(pinBlock)
|
||||||
|
} else {
|
||||||
|
payDetail?.pinCipher = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
handler.obtainMessage(
|
||||||
|
ON_CONFIRM_CLICK
|
||||||
|
).sendToTarget()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCancel() {
|
||||||
|
if (!isTerminalStateReached) {
|
||||||
|
handler.obtainMessage(
|
||||||
|
ON_CANCEL_CLICK
|
||||||
|
).sendToTarget()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(code: Int) {
|
||||||
|
|
||||||
|
val msg =
|
||||||
|
AidlErrorCodeV2
|
||||||
|
.valueOf(code)
|
||||||
|
.msg
|
||||||
|
|
||||||
|
LogUtil.d(
|
||||||
|
TAG,
|
||||||
|
"error code:$code - message :$msg"
|
||||||
|
)
|
||||||
|
|
||||||
|
when (code) {
|
||||||
|
|
||||||
|
-60001 -> {
|
||||||
|
handler.obtainMessage(
|
||||||
|
ON_TIMEOUT_PIN_PAD
|
||||||
|
).sendToTarget()
|
||||||
|
}
|
||||||
|
|
||||||
|
-3025 -> {
|
||||||
|
handler.obtainMessage(
|
||||||
|
ON_ERROR_DUKPT
|
||||||
|
).sendToTarget()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
handler.obtainMessage(
|
||||||
|
ON_ERROR_PIN_PAD,
|
||||||
|
code,
|
||||||
|
code,
|
||||||
|
code
|
||||||
|
).sendToTarget()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPanBytes(pan: String): ByteArray {
|
||||||
|
|
||||||
|
if (pan.length < 13) {
|
||||||
|
return ByteArray(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pan
|
||||||
|
.substring(pan.length - 13, pan.length - 1)
|
||||||
|
.toByteArray(StandardCharsets.US_ASCII)
|
||||||
|
}
|
||||||
|
}
|
||||||
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=720px,height=1600px,dpi=270",
|
||||||
|
showBackground = true,
|
||||||
|
showSystemUi = true
|
||||||
|
)
|
||||||
|
annotation class P3Preview
|
||||||
@ -0,0 +1,281 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.print_receipt
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||||
|
import com.mob.utsmyanmar.ui.theme.Color
|
||||||
|
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultEvent
|
||||||
|
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultState
|
||||||
|
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultViewModel
|
||||||
|
import com.mob.utsmyanmar.viewmodel.SharedViewModel
|
||||||
|
import com.utsmyanmar.paylibs.print.PrintReceipt
|
||||||
|
import com.utsmyanmar.paylibs.utils.POSUtil
|
||||||
|
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PrintReceiptScreen(
|
||||||
|
transactionResultViewModel: TransactionResultViewModel,
|
||||||
|
sharedViewModel: SharedViewModel,
|
||||||
|
onPrint: () -> Unit,
|
||||||
|
onDone: () -> Unit
|
||||||
|
) {
|
||||||
|
val state by transactionResultViewModel.state.collectAsStateWithLifecycle()
|
||||||
|
val scope = rememberCoroutineScope();
|
||||||
|
val receiptOffsetY = remember {
|
||||||
|
Animatable(0f);
|
||||||
|
}
|
||||||
|
val animationDuration = 3000;
|
||||||
|
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
AppBar(title = "Receipt")
|
||||||
|
},
|
||||||
|
containerColor = Color.IvoryBeige
|
||||||
|
) { paddingValues ->
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
|
||||||
|
ReceiptPreview(
|
||||||
|
state = state,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.graphicsLayer{
|
||||||
|
translationY = receiptOffsetY.value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = onDone,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(50.dp),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Cancel",
|
||||||
|
style = MaterialTheme.typography.bodyLarge.copy(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(50.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color.LegacyRed,
|
||||||
|
contentColor = Color.White,
|
||||||
|
disabledContainerColor = Color.LegacyRed.copy(alpha = 0.5f),
|
||||||
|
disabledContentColor = Color.White.copy(alpha = 0.5f),
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
onClick = {
|
||||||
|
onPrint()
|
||||||
|
// try {
|
||||||
|
// PrintReceipt.getInstance().printNow()
|
||||||
|
// scope.launch {
|
||||||
|
// launch {
|
||||||
|
// receiptOffsetY.animateTo(
|
||||||
|
// targetValue = -1500f,
|
||||||
|
// animationSpec = tween(
|
||||||
|
// durationMillis = animationDuration,
|
||||||
|
// easing = FastOutSlowInEasing
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// delay(5000)
|
||||||
|
// launch {
|
||||||
|
// receiptOffsetY.animateTo(
|
||||||
|
// targetValue = 0f,
|
||||||
|
// animationSpec = tween(
|
||||||
|
// durationMillis = 0,
|
||||||
|
// easing = FastOutSlowInEasing
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } catch (e: Exception) {
|
||||||
|
//
|
||||||
|
// Log.d("PrintReceipt", "error with $e")
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Print",
|
||||||
|
style = MaterialTheme.typography.bodyLarge.copy(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ReceiptPreview(
|
||||||
|
state: TransactionResultState,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(Color.White)
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = Color.Gray,
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
)
|
||||||
|
.padding(20.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "MOB POS",
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontFamily = FontFamily.Monospace
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "Transaction Receipt",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontFamily = FontFamily.Monospace
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
ReceiptDivider()
|
||||||
|
ReceiptRow("Merchant", state.payDetail?.merchantName.toString() )
|
||||||
|
ReceiptRow("Terminal ID", state.payDetail?.terminalNo.toString() )
|
||||||
|
ReceiptRow("Trace No", state.payDetail?.voucherNo.toString() )
|
||||||
|
ReceiptRow("Transaction", state.payDetail?.transType.toString())
|
||||||
|
ReceiptRow("Amount", POSUtil.getInstance().getDecimalAmountSeparatorFormat(state.payDetail?.amount?:0))
|
||||||
|
ReceiptRow("Card Type", state.payDetail?.accountType.toString())
|
||||||
|
ReceiptRow("Card No", state.payDetail?.cardNo.toString())
|
||||||
|
ReceiptRow("Status", POSUtil.getInstance().getResponse(state.payDetail?.tradeAnswerCode?: "-"))
|
||||||
|
ReceiptRow("Date", state.payDetail?.transDate.toString() )
|
||||||
|
ReceiptRow("Time", state.payDetail?.transTime.toString())
|
||||||
|
ReceiptRow("Ref No", state.payDetail?.referNo.toString())
|
||||||
|
ReceiptDivider()
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "Thank You",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontFamily = FontFamily.Monospace
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Please keep this receipt",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontFamily = FontFamily.Monospace
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ReceiptRow(
|
||||||
|
label: String,
|
||||||
|
value: String
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 6.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = value,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontFamily = FontFamily.Monospace
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ReceiptDivider() {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 10.dp),
|
||||||
|
text = "--------------------------------",
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, showSystemUi = true)
|
||||||
|
@Composable
|
||||||
|
fun PreviewPrintReceiptScreen() {
|
||||||
|
MaterialTheme {
|
||||||
|
PrintReceiptScreen(
|
||||||
|
sharedViewModel = hiltViewModel(),
|
||||||
|
transactionResultViewModel = hiltViewModel(),
|
||||||
|
onPrint = {},
|
||||||
|
onDone = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,175 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.refund_rrn
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.mob.utsmyanmar.ui.components.NumericEntryScreen
|
||||||
|
import com.mob.utsmyanmar.viewmodel.SharedViewModel
|
||||||
|
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
|
||||||
|
import com.utsmyanmar.paylibs.model.PayDetail
|
||||||
|
import com.utsmyanmar.paylibs.model.TradeData
|
||||||
|
import com.utsmyanmar.paylibs.utils.POSUtil
|
||||||
|
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||||
|
import sunmi.sunmiui.utils.LogUtil
|
||||||
|
|
||||||
|
private const val RRN_MAX_LENGTH = 12
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InputRrnRoute(
|
||||||
|
transProcessViewModel: TransProcessViewModel,
|
||||||
|
rrnViewModel: RRNViewModel,
|
||||||
|
sharedViewModel: SharedViewModel,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onNavigateProcessing: () -> Unit
|
||||||
|
) {
|
||||||
|
var rrn by rememberSaveable {
|
||||||
|
mutableStateOf(sharedViewModel.rrNNo.value.orEmpty())
|
||||||
|
}
|
||||||
|
var errorMessage by rememberSaveable { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
rrn = ""
|
||||||
|
errorMessage = null
|
||||||
|
}
|
||||||
|
|
||||||
|
val payDetailState by rrnViewModel.payDetailState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
|
||||||
|
fun processPreAuthComp(payDetail: PayDetail) {
|
||||||
|
val newPay = transProcessViewModel.payDetail
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
newPay?.setReferNo(payDetail.referNo)
|
||||||
|
newPay?.setApprovalCode(payDetail.getApprovalCode())
|
||||||
|
newPay?.setTradeTime(payDetail.getTradeTime())
|
||||||
|
newPay?.setTradeDate(payDetail.getTradeDate())
|
||||||
|
|
||||||
|
|
||||||
|
val tradeData = TradeData()
|
||||||
|
tradeData.payDetail = newPay
|
||||||
|
transProcessViewModel.setOldTransPayDetail(payDetail)
|
||||||
|
transProcessViewModel.setTradeData(tradeData)
|
||||||
|
|
||||||
|
rrnViewModel.resetState()
|
||||||
|
onNavigateProcessing()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun processPreAuthVoid(payDetail: PayDetail) {
|
||||||
|
val newPay = transProcessViewModel.payDetail
|
||||||
|
|
||||||
|
newPay?.setReferNo(payDetail.referNo)
|
||||||
|
|
||||||
|
|
||||||
|
val tradeData = TradeData()
|
||||||
|
tradeData.payDetail = newPay
|
||||||
|
transProcessViewModel.setOldTransPayDetail(payDetail)
|
||||||
|
transProcessViewModel.setTradeData(tradeData)
|
||||||
|
|
||||||
|
rrnViewModel.resetState()
|
||||||
|
onNavigateProcessing()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
LaunchedEffect(payDetailState) {
|
||||||
|
LogUtil.d("RRN","paydetail state: $payDetailState")
|
||||||
|
when (val state = payDetailState) {
|
||||||
|
|
||||||
|
is RRNViewModel.PayDetailState.Success -> {
|
||||||
|
|
||||||
|
|
||||||
|
sharedViewModel.payDetail.value = state.data
|
||||||
|
sharedViewModel.rrNNo.value = state.data.referNo
|
||||||
|
|
||||||
|
if(sharedViewModel.transactionsType.value == TransactionsType.PRE_AUTH_COMPLETE) {
|
||||||
|
processPreAuthComp(state.data)
|
||||||
|
} else if(sharedViewModel.transactionsType.value == TransactionsType.PRE_AUTH_VOID) {
|
||||||
|
processPreAuthVoid(state.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
is RRNViewModel.PayDetailState.Error -> {
|
||||||
|
errorMessage = state.message
|
||||||
|
rrnViewModel.resetState()
|
||||||
|
// show dialog
|
||||||
|
// navigate to home
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NumericEntryScreen(
|
||||||
|
title = "Input RRN",
|
||||||
|
prompt = "Enter RRN",
|
||||||
|
displayValue = rrn.ifEmpty { "0" },
|
||||||
|
supportingText = errorMessage ?: "Enter the retrieval reference number to continue",
|
||||||
|
confirmText = "Next",
|
||||||
|
onBackClick = onBack,
|
||||||
|
onCancelClick = onBack,
|
||||||
|
onConfirmClick = {
|
||||||
|
val trimmedRrn = rrn.trim()
|
||||||
|
|
||||||
|
if (POSUtil.getInstance().checkNumberField(trimmedRrn)) {
|
||||||
|
errorMessage = "Invalid RRN"
|
||||||
|
return@NumericEntryScreen
|
||||||
|
}
|
||||||
|
sharedViewModel.rrNNo.value = trimmedRrn
|
||||||
|
|
||||||
|
|
||||||
|
if(sharedViewModel.transactionsType.value == TransactionsType.REFUND ) {
|
||||||
|
val newPay = transProcessViewModel.payDetail
|
||||||
|
|
||||||
|
newPay?.setReferNo(trimmedRrn)
|
||||||
|
val tradeData = TradeData()
|
||||||
|
tradeData.payDetail = newPay
|
||||||
|
transProcessViewModel.setTradeData(tradeData)
|
||||||
|
rrnViewModel.resetState()
|
||||||
|
onNavigateProcessing()
|
||||||
|
} else{
|
||||||
|
rrnViewModel.searchPayDetail(
|
||||||
|
trimmedRrn,
|
||||||
|
sharedViewModel.payDetail.value?.cardNo?:""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
onKeyClick = { value ->
|
||||||
|
rrn = appendRrnValue(rrn, value)
|
||||||
|
errorMessage = null
|
||||||
|
},
|
||||||
|
onDeleteClick = {
|
||||||
|
rrn = rrn.dropLast(1)
|
||||||
|
errorMessage = null
|
||||||
|
},
|
||||||
|
confirmEnabled = rrn.isNotEmpty(),
|
||||||
|
canDelete = rrn.isNotEmpty(),
|
||||||
|
keys = listOf(
|
||||||
|
listOf("1", "2", "3"),
|
||||||
|
listOf("4", "5", "6"),
|
||||||
|
listOf("7", "8", "9"),
|
||||||
|
listOf("", "0", "00")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun appendRrnValue(current: String, value: String): String {
|
||||||
|
if (value.isBlank()) {
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
|
||||||
|
val remainingDigits = RRN_MAX_LENGTH - current.length
|
||||||
|
if (remainingDigits <= 0) {
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
|
||||||
|
return current + value.take(remainingDigits)
|
||||||
|
}
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.refund_rrn
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.asFlow
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.utsmyanmar.baselib.repo.Repository
|
||||||
|
import com.utsmyanmar.paylibs.model.PayDetail
|
||||||
|
import com.utsmyanmar.paylibs.system.SingleLiveEvent
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class RRNViewModel @Inject constructor(
|
||||||
|
private val repository: Repository
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
sealed class PayDetailState {
|
||||||
|
object Idle : PayDetailState()
|
||||||
|
object Loading : PayDetailState()
|
||||||
|
data class Success(val data: PayDetail) : PayDetailState()
|
||||||
|
data class Error(val message: String) : PayDetailState()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _payDetailState = MutableStateFlow<PayDetailState>(PayDetailState.Idle)
|
||||||
|
val payDetailState: StateFlow<PayDetailState> = _payDetailState
|
||||||
|
|
||||||
|
fun searchPayDetail(refNo: String, cardNo: String) {
|
||||||
|
_payDetailState.value = PayDetailState.Loading
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
repository.searchPayDetail(cardNo, refNo)
|
||||||
|
.asFlow()
|
||||||
|
.filterNotNull()
|
||||||
|
.first()
|
||||||
|
.let { result ->
|
||||||
|
_payDetailState.value = PayDetailState.Success(result)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_payDetailState.value = PayDetailState.Error(e.message ?: "Search failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetState() {
|
||||||
|
_payDetailState.value = PayDetailState.Idle
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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,322 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.sale_void
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.rounded.Backspace
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||||
|
import com.mob.utsmyanmar.ui.theme.Color
|
||||||
|
import com.mob.utsmyanmar.viewmodel.SharedViewModel
|
||||||
|
import com.utsmyanmar.paylibs.model.PayDetail
|
||||||
|
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||||
|
import sunmi.sunmiui.utils.LogUtil
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VoidTraceScreen(
|
||||||
|
sharedViewModel: SharedViewModel,
|
||||||
|
voidViewModel: VoidViewModel,
|
||||||
|
onNavigateTranDetail: (String) -> Unit,
|
||||||
|
onBack: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val tag = "VoidTraceScreen"
|
||||||
|
var traceNumber by remember { mutableStateOf("") }
|
||||||
|
var searchedTrace by remember { mutableStateOf("") }
|
||||||
|
var lastNavigatedTrace by remember { mutableStateOf<String?>(null) }
|
||||||
|
val displayTraceNumber = traceNumber.padStart(6, '0').ifEmpty { "000000" }
|
||||||
|
val recentTransactions by voidViewModel.getLastThreeTransactions().observeAsState(emptyList())
|
||||||
|
|
||||||
|
val transactionSource: LiveData<PayDetail?> = remember(searchedTrace) {
|
||||||
|
val transType = if(sharedViewModel.transactionsType.value == TransactionsType.VOID) {
|
||||||
|
TransactionsType.SALE.value
|
||||||
|
} else {
|
||||||
|
TransactionsType.PRE_AUTH_COMPLETE.value;
|
||||||
|
}
|
||||||
|
LogUtil.d("void screen","transaction type : $transType and trace number : $searchedTrace")
|
||||||
|
if (searchedTrace.isBlank()) {
|
||||||
|
MutableLiveData<PayDetail?>(null)
|
||||||
|
} else {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
voidViewModel.getVoidTrans(
|
||||||
|
transType,
|
||||||
|
searchedTrace
|
||||||
|
) as LiveData<PayDetail?>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val transaction by transactionSource.observeAsState()
|
||||||
|
val hasSearched = searchedTrace.isNotBlank()
|
||||||
|
val headerMessage = if (hasSearched && transaction == null) {
|
||||||
|
"Warning: no previous sale transaction found for trace $searchedTrace."
|
||||||
|
} else {
|
||||||
|
"Input trace number to find previous sale transaction."
|
||||||
|
}
|
||||||
|
val headerColor = if (hasSearched && transaction == null) {
|
||||||
|
Color.LegacyRed
|
||||||
|
} else {
|
||||||
|
Color.Black
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(searchedTrace) {
|
||||||
|
if (searchedTrace != lastNavigatedTrace) {
|
||||||
|
lastNavigatedTrace = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(transaction?.voucherNo) {
|
||||||
|
val matchedTrace = transaction?.voucherNo ?: return@LaunchedEffect
|
||||||
|
if (lastNavigatedTrace == matchedTrace) return@LaunchedEffect
|
||||||
|
lastNavigatedTrace = matchedTrace
|
||||||
|
onNavigateTranDetail(matchedTrace)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
LaunchedEffect(searchedTrace, transaction) {
|
||||||
|
Log.d(
|
||||||
|
tag,
|
||||||
|
"Recent DB traces: ${
|
||||||
|
recentTransactions.joinToString { detail ->
|
||||||
|
"${detail.voucherNo}:${detail.transType}:${detail.tradeAnswerCode}"
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
)
|
||||||
|
if (searchedTrace.isNotBlank()) {
|
||||||
|
Log.d(
|
||||||
|
tag,
|
||||||
|
"Search trace=$searchedTrace result=${transaction?.voucherNo ?: "NOT_FOUND"}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
containerColor = Color.IvoryBeige,
|
||||||
|
topBar = {
|
||||||
|
AppBar(
|
||||||
|
title = "Void",
|
||||||
|
icon = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
onIconClick = onBack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = headerMessage,
|
||||||
|
color = headerColor,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(28.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Trace Number",
|
||||||
|
color = Color.Gray,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = displayTraceNumber,
|
||||||
|
color = Color.LegacyRed,
|
||||||
|
fontSize = 32.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Enter 6-digit trace number",
|
||||||
|
color = Color.Gray,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
TraceNumberKeypad(
|
||||||
|
onNumberClick = { value ->
|
||||||
|
if (traceNumber.length < 6) {
|
||||||
|
traceNumber = (traceNumber + value).take(6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 18.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = onBack,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(56.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color.White,
|
||||||
|
contentColor = Color.LegacyRed
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
searchedTrace = traceNumber.padStart(6, '0')
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(56.dp),
|
||||||
|
enabled = traceNumber.isNotBlank(),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color.LegacyRed,
|
||||||
|
contentColor = Color.White,
|
||||||
|
disabledContainerColor = Color.LegacyRed.copy(alpha = 0.5f),
|
||||||
|
disabledContentColor = Color.White
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Enter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(top = 110.dp, end = 16.dp)
|
||||||
|
.clickable(enabled = traceNumber.isNotEmpty()) {
|
||||||
|
traceNumber = traceNumber.dropLast(1)
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(18.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.Icon(
|
||||||
|
imageVector = Icons.Rounded.Backspace,
|
||||||
|
contentDescription = "Delete",
|
||||||
|
tint = if (traceNumber.isNotEmpty()) Color.LegacyRed else Color.Gray,
|
||||||
|
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TraceNumberKeypad(
|
||||||
|
onNumberClick: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val keys = listOf(
|
||||||
|
listOf("1", "2", "3"),
|
||||||
|
listOf("4", "5", "6"),
|
||||||
|
listOf("7", "8", "9"),
|
||||||
|
listOf("", "0", "00")
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
keys.forEach { row ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
row.forEach { key ->
|
||||||
|
if (key.isBlank()) {
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(66.dp)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
TraceKeypadButton(
|
||||||
|
text = key,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
onClick = { onNumberClick(key) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TraceKeypadButton(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.height(66.dp)
|
||||||
|
.shadow(
|
||||||
|
elevation = 2.dp,
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
clip = false
|
||||||
|
)
|
||||||
|
.background(
|
||||||
|
color = Color.White,
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.clickable { onClick() },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = Color.LegacyRed,
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,90 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.sending_to_host
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.mob.utsmyanmar.model.TransResultStatus
|
||||||
|
import com.mob.utsmyanmar.viewmodel.SharedViewModel
|
||||||
|
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultState
|
||||||
|
import com.mob.utsmyanmar.viewmodel.ProcessingTransaction
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import com.mob.utsmyanmar.model.TransactionStatus
|
||||||
|
import com.mob.utsmyanmar.ui.settlement.SettlementViewModel
|
||||||
|
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
|
||||||
|
private const val MOCK_HOST_DELAY_MS = 2000L
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ProcessingRoute(
|
||||||
|
sharedViewModel: SharedViewModel,
|
||||||
|
transProcessViewModel: TransProcessViewModel,
|
||||||
|
settlementViewModel: SettlementViewModel,
|
||||||
|
onNavigateTransactionResult: () -> Unit,
|
||||||
|
onNavigateMain: ()-> Unit
|
||||||
|
) {
|
||||||
|
|
||||||
|
val transStatus by transProcessViewModel.transResultStatus.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
val state by transProcessViewModel.state.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
val settlementStatus by settlementViewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
if(sharedViewModel.transactionsType.value == TransactionsType.SETTLEMENT) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
settlementViewModel.startSettlementProcess()
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(settlementStatus.status) {
|
||||||
|
when(settlementStatus.status) {
|
||||||
|
TransactionStatus.ON_ERROR,
|
||||||
|
TransactionStatus.ON_SUCCESS -> {
|
||||||
|
sharedViewModel.payDetail.value = settlementViewModel.getPayDetail()
|
||||||
|
onNavigateTransactionResult()
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
transProcessViewModel.resetTransactionStatus()
|
||||||
|
transProcessViewModel.startOnlineProcess()
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(transStatus) {
|
||||||
|
when(transStatus) {
|
||||||
|
TransResultStatus.SUCCESS -> {
|
||||||
|
sharedViewModel.payDetail.value = transProcessViewModel.payDetailResult.value
|
||||||
|
onNavigateTransactionResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
TransResultStatus.FAIL -> {
|
||||||
|
sharedViewModel.payDetail.value = transProcessViewModel.payDetailResult.value
|
||||||
|
onNavigateTransactionResult()
|
||||||
|
}
|
||||||
|
TransResultStatus.REVERSAL_SUCCESS -> onNavigateMain()
|
||||||
|
TransResultStatus.REVERSAL_FAIL -> onNavigateMain()
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if(sharedViewModel.transactionsType.value == TransactionsType.SETTLEMENT) {
|
||||||
|
ProcessingScreen(settlementStatus.processingState)
|
||||||
|
} else {
|
||||||
|
ProcessingScreen(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@ -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,504 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.settlement
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.asFlow
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.mob.utsmyanmar.config.Constants
|
||||||
|
import com.mob.utsmyanmar.model.SettlementType
|
||||||
|
import com.mob.utsmyanmar.model.TransactionStatus
|
||||||
|
import com.mob.utsmyanmar.ui.sending_to_host.ProcessingState
|
||||||
|
import com.utsmyanmar.baselib.repo.Repository
|
||||||
|
import com.utsmyanmar.paylibs.Constant
|
||||||
|
import com.utsmyanmar.paylibs.batch_upload.BatchListener
|
||||||
|
import com.utsmyanmar.paylibs.batch_upload.BatchUploadProcess
|
||||||
|
import com.utsmyanmar.paylibs.isobuilder.ISOMode
|
||||||
|
import com.utsmyanmar.paylibs.isobuilder.builderx.ISOMsgX
|
||||||
|
import com.utsmyanmar.paylibs.isobuilder.builderx.ISOVersion
|
||||||
|
import com.utsmyanmar.paylibs.model.MsgField
|
||||||
|
import com.utsmyanmar.paylibs.model.PayDetail
|
||||||
|
import com.utsmyanmar.paylibs.model.SettleData
|
||||||
|
import com.utsmyanmar.paylibs.model.TradeData
|
||||||
|
import com.utsmyanmar.paylibs.network.ISOCallback
|
||||||
|
import com.utsmyanmar.paylibs.network.ISOSocket
|
||||||
|
import com.utsmyanmar.paylibs.utils.MessageType
|
||||||
|
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
|
||||||
|
import com.utsmyanmar.paylibs.utils.enums.HostName
|
||||||
|
import com.utsmyanmar.paylibs.utils.iso_utils.BitmapConfig
|
||||||
|
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionType
|
||||||
|
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||||
|
import com.utsmyanmar.paylibs.utils.params.Params
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import sunmi.sunmiui.utils.LogUtil
|
||||||
|
import java.util.Locale
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
data class SettlementUiState(
|
||||||
|
val saleCount: Int = 0,
|
||||||
|
val saleAmount: Long = 0L,
|
||||||
|
|
||||||
|
val preCount: Int = 0,
|
||||||
|
val preAmount: Long = 0L,
|
||||||
|
|
||||||
|
val refundCount: Int = 0,
|
||||||
|
val refundAmount: Long = 0L,
|
||||||
|
|
||||||
|
val caCount: Int = 0,
|
||||||
|
val caAmount: Long = 0L,
|
||||||
|
|
||||||
|
val settlementType: SettlementType? = null,
|
||||||
|
val isNoData: Boolean = false,
|
||||||
|
val isSentData: Boolean = false,
|
||||||
|
val bottomLayout: Int = 0,
|
||||||
|
|
||||||
|
val status: TransactionStatus? = null,
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val processingState: ProcessingState = ProcessingState("Processing")
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface SettlementEvent {
|
||||||
|
data class ShowStatus(val status: TransactionStatus) : SettlementEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class SettlementViewModel @Inject constructor(
|
||||||
|
private val repository: Repository
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = SettlementViewModel::class.java.simpleName
|
||||||
|
}
|
||||||
|
|
||||||
|
private var payDetail: PayDetail? = null
|
||||||
|
private var payDetails: List<PayDetail>? = null
|
||||||
|
|
||||||
|
private val deleteTrans = arrayListOf<PayDetail>()
|
||||||
|
|
||||||
|
private var flag = false
|
||||||
|
private var errorFlag = false
|
||||||
|
private var isSecondCall = false
|
||||||
|
private var batchIndex = 0
|
||||||
|
|
||||||
|
private var bitmap = ""
|
||||||
|
|
||||||
|
val records: LiveData<List<PayDetail>> = getSettlementPOS()
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
records.asFlow().collect { list ->
|
||||||
|
setPayDetails(list)
|
||||||
|
setSettlementData(list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val isoMsgX: ISOMsgX =
|
||||||
|
ISOMsgX.ISOMsgXBuilder(
|
||||||
|
ISOVersion.VERSION_1993,
|
||||||
|
ISOMode.ONLY_HEADER,
|
||||||
|
HostName.MPU
|
||||||
|
).build()
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow(SettlementUiState())
|
||||||
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
private val _events = Channel<SettlementEvent>(Channel.BUFFERED)
|
||||||
|
val events = _events.receiveAsFlow()
|
||||||
|
|
||||||
|
fun getLastSettlement(voucherNo: String): LiveData<List<PayDetail>> {
|
||||||
|
return repository.getLastSettlement(voucherNo)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSettlement(): LiveData<List<PayDetail>> {
|
||||||
|
return repository.getSettlement()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSettlementPOS(): LiveData<List<PayDetail>> {
|
||||||
|
return repository.getSettlementPOS()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDeleteTrans(batchNo: String): LiveData<List<PayDetail>> {
|
||||||
|
return repository.getDeleteTrans(batchNo)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAdditionalSettlementPOS(): LiveData<List<PayDetail>> {
|
||||||
|
return repository.getAdditionalSettlementPOS(
|
||||||
|
SystemParamsOperation.getInstance().getCurrentBatchNum()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPayDetails(list: List<PayDetail>) {
|
||||||
|
payDetails = list
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSettlementData(list: List<PayDetail>) {
|
||||||
|
val newState = list.fold(_uiState.value) { state, payDetail ->
|
||||||
|
val transType = payDetail.transactionType
|
||||||
|
val transName = payDetail.transType
|
||||||
|
|
||||||
|
when {
|
||||||
|
(transType == TransactionType.SALE || transName.equals("SALE", ignoreCase = true))
|
||||||
|
&& !payDetail.isCanceled ->
|
||||||
|
state.copy(
|
||||||
|
saleCount = state.saleCount + 1,
|
||||||
|
saleAmount = state.saleAmount + payDetail.amount
|
||||||
|
)
|
||||||
|
|
||||||
|
(transType == TransactionType.PRE_SALE_COMPLETE
|
||||||
|
|| transName.equals("PREAUTH COMPLETION", ignoreCase = true)
|
||||||
|
|| transName.equals("PRE_AUTH_COMPLETE", ignoreCase = true))
|
||||||
|
&& !payDetail.isCanceled ->
|
||||||
|
state.copy(
|
||||||
|
preCount = state.preCount + 1,
|
||||||
|
preAmount = state.preAmount + payDetail.amount
|
||||||
|
)
|
||||||
|
|
||||||
|
transType == TransactionType.REFUND
|
||||||
|
|| transName.equals("REFUND", ignoreCase = true) ->
|
||||||
|
state.copy(
|
||||||
|
refundCount = state.refundCount + 1,
|
||||||
|
refundAmount = state.refundAmount + payDetail.amount
|
||||||
|
)
|
||||||
|
|
||||||
|
transType == TransactionType.CASH_ADVANCE
|
||||||
|
|| transName.equals("CASH_OUT", ignoreCase = true)
|
||||||
|
|| transName.equals("CASH_ADVANCE", ignoreCase = true) ->
|
||||||
|
state.copy(
|
||||||
|
caCount = state.caCount + 1,
|
||||||
|
caAmount = state.caAmount + payDetail.amount
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LogUtil.d(TAG,"sale count ${newState.saleCount} and sale amount ${newState.saleAmount}")
|
||||||
|
_uiState.update { newState }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSettlementSummary(
|
||||||
|
saleCount: Int,
|
||||||
|
saleAmount: Long,
|
||||||
|
preCount: Int,
|
||||||
|
preAmount: Long,
|
||||||
|
refundCount: Int,
|
||||||
|
refundAmount: Long,
|
||||||
|
caCount: Int,
|
||||||
|
caAmount: Long
|
||||||
|
) {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
saleCount = saleCount,
|
||||||
|
saleAmount = saleAmount,
|
||||||
|
preCount = preCount,
|
||||||
|
preAmount = preAmount,
|
||||||
|
refundCount = refundCount,
|
||||||
|
refundAmount = refundAmount,
|
||||||
|
caCount = caCount,
|
||||||
|
caAmount = caAmount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPayDetail() = payDetail
|
||||||
|
fun setSettlementType(type: SettlementType) {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(settlementType = type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updatePayDetail(payDetail: PayDetail) {
|
||||||
|
repository.updatePayDetail(payDetail)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun insertPayDetail(payDetail: PayDetail) {
|
||||||
|
repository.insertPayDetail(payDetail)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateDB() {
|
||||||
|
payDetails?.forEach { pay ->
|
||||||
|
repository.deletePayDetail(pay)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteTrans.forEach { pay ->
|
||||||
|
repository.deletePayDetail(pay)
|
||||||
|
}
|
||||||
|
|
||||||
|
payDetails = emptyList()
|
||||||
|
deleteTrans.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun startSettlementProcess() {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(isLoading = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemParamsOperation.getInstance().getIncrementBatchNo()
|
||||||
|
|
||||||
|
requestOnlineProcessSettlement()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startPrintSettlementProcess() {
|
||||||
|
// PrintReceipt.getInstance().printSettlementReceiptPOS(...)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testServiceClass() {
|
||||||
|
LogUtil.d(TAG, "SettlementViewModel works!")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requestOnlineProcessSettlement() {
|
||||||
|
val state = _uiState.value
|
||||||
|
|
||||||
|
val hostName = HostName.MPU
|
||||||
|
val field60 = SystemParamsOperation.getInstance().getCurrentBatchNum()
|
||||||
|
|
||||||
|
val sale2Count = state.saleCount + state.preCount
|
||||||
|
val sale2Amount = state.saleAmount + state.preAmount
|
||||||
|
|
||||||
|
LogUtil.d(TAG,"Sale count :$sale2Count and Sale amount :$sale2Amount")
|
||||||
|
|
||||||
|
val totalSaleCount = String.format(Locale.getDefault(), "%03d", sale2Count)
|
||||||
|
val totalSaleAmount = String.format(Locale.getDefault(), "%010d00", sale2Amount)
|
||||||
|
|
||||||
|
val totalRefundCount = String.format(Locale.getDefault(), "%03d", state.refundCount)
|
||||||
|
val totalRefundAmount = String.format(
|
||||||
|
Locale.getDefault(),
|
||||||
|
"%010d00",
|
||||||
|
if (state.refundAmount == 0L) 0 else state.refundAmount
|
||||||
|
)
|
||||||
|
|
||||||
|
val totalDebitSaleCount = String.format(Locale.getDefault(), "%03d", state.caCount)
|
||||||
|
val totalDebitSaleAmount = String.format(
|
||||||
|
Locale.getDefault(),
|
||||||
|
"%010d00",
|
||||||
|
if (state.caAmount == 0L) 0 else state.caAmount
|
||||||
|
)
|
||||||
|
|
||||||
|
val totalERefundCount = String.format(Locale.getDefault(), "%03d", 0)
|
||||||
|
val totalERefundAmount = String.format(Locale.getDefault(), "%010d00", 0)
|
||||||
|
|
||||||
|
val tradeData = Params.newTrade(true)
|
||||||
|
val currentPayDetail = tradeData.payDetail
|
||||||
|
|
||||||
|
payDetail = currentPayDetail
|
||||||
|
|
||||||
|
bitmap = BitmapConfig.MPU_NEW_SETTLE
|
||||||
|
|
||||||
|
currentPayDetail.transType = TransactionsType.SETTLEMENT.name
|
||||||
|
currentPayDetail.transactionType = TransactionType.SETTLEMENT
|
||||||
|
|
||||||
|
if (!flag) {
|
||||||
|
currentPayDetail.processCode = TransactionsType.SETTLEMENT.processCode
|
||||||
|
} else {
|
||||||
|
bitmap = BitmapConfig.MPU_NEW_SETTLE
|
||||||
|
currentPayDetail.processCode = "960000"
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPayDetail.batchNo = SystemParamsOperation.getInstance().getCurrentBatchNum()
|
||||||
|
|
||||||
|
currentPayDetail.settleList =
|
||||||
|
"${state.saleCount}:${state.saleAmount}-" +
|
||||||
|
"${state.caCount}:${state.caAmount}-" +
|
||||||
|
"${state.refundCount}:${state.refundAmount}-" +
|
||||||
|
"${state.preCount}:${state.preAmount}"
|
||||||
|
|
||||||
|
val settleData = SettleData(
|
||||||
|
state.saleCount,
|
||||||
|
state.saleAmount,
|
||||||
|
state.preCount,
|
||||||
|
state.preAmount,
|
||||||
|
state.refundCount,
|
||||||
|
state.refundAmount,
|
||||||
|
state.caCount,
|
||||||
|
state.caAmount
|
||||||
|
)
|
||||||
|
|
||||||
|
currentPayDetail.settleDataObj = settleData
|
||||||
|
|
||||||
|
if (hostName == HostName.BPC) {
|
||||||
|
val totalAmount =
|
||||||
|
state.saleAmount + state.preAmount + state.refundAmount + state.caAmount
|
||||||
|
|
||||||
|
val settlementData = if (state.refundAmount != 0L) {
|
||||||
|
val creditTotal = state.saleAmount + state.preAmount + state.caAmount
|
||||||
|
val subTotal = creditTotal - state.refundAmount
|
||||||
|
|
||||||
|
if (subTotal < 0L) {
|
||||||
|
"D" + String.format(Locale.getDefault(), "%012d", kotlin.math.abs(subTotal))
|
||||||
|
} else {
|
||||||
|
"C" + String.format(Locale.getDefault(), "%012d", subTotal)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"C" + String.format(Locale.getDefault(), "%012d", totalAmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPayDetail.settleData = settlementData
|
||||||
|
currentPayDetail.amount = totalAmount
|
||||||
|
} else {
|
||||||
|
currentPayDetail.settleData =
|
||||||
|
totalSaleCount +
|
||||||
|
totalSaleAmount +
|
||||||
|
totalRefundCount +
|
||||||
|
totalRefundAmount +
|
||||||
|
totalDebitSaleCount +
|
||||||
|
totalDebitSaleAmount +
|
||||||
|
totalERefundCount +
|
||||||
|
totalERefundAmount
|
||||||
|
}
|
||||||
|
|
||||||
|
payDetail = currentPayDetail
|
||||||
|
tradeData.payDetail = currentPayDetail
|
||||||
|
tradeData.field60 = field60
|
||||||
|
|
||||||
|
val sendBytes = isoMsgX.buildISOPackets(
|
||||||
|
tradeData,
|
||||||
|
bitmap,
|
||||||
|
MessageType.SETTLEMENT
|
||||||
|
)
|
||||||
|
|
||||||
|
LogUtil.d(TAG, "Starting SETTLEMENT process...")
|
||||||
|
|
||||||
|
ISOSocket.getInstance().enqueue(
|
||||||
|
sendBytes,
|
||||||
|
sendBytes.size,
|
||||||
|
false,
|
||||||
|
object : ISOCallback {
|
||||||
|
|
||||||
|
override fun onReceive(bytes: ByteArray, length: Int) {
|
||||||
|
val responseMap: Map<String, MsgField>? =
|
||||||
|
isoMsgX.parseISOPackets(bytes, length)
|
||||||
|
|
||||||
|
if (responseMap != null) {
|
||||||
|
val resultStr = try {
|
||||||
|
responseMap["F039"]?.dataStr.orEmpty()
|
||||||
|
} catch (e: NullPointerException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
currentPayDetail.isNeedReversal = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPayDetail.tradeAnswerCode = resultStr
|
||||||
|
|
||||||
|
when {
|
||||||
|
resultStr == Constant.ANSWER_CODE_ACCEPT ||
|
||||||
|
resultStr == Constant.ANSWER_CODE_APPROVED -> {
|
||||||
|
currentPayDetail.isNeedReversal = false
|
||||||
|
}
|
||||||
|
|
||||||
|
resultStr == "95" || resultStr == "095" -> {
|
||||||
|
currentPayDetail.isNeedReversal = !flag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errorFlag = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(msg: String) {
|
||||||
|
if (msg != Constants.REVERSAL) {
|
||||||
|
if (!isSecondCall) {
|
||||||
|
ISOSocket.getInstance().switchIp()
|
||||||
|
|
||||||
|
postStatus(TransactionStatus.ON_SECONDARY)
|
||||||
|
|
||||||
|
isSecondCall = true
|
||||||
|
requestOnlineProcessSettlement()
|
||||||
|
} else {
|
||||||
|
postStatus(TransactionStatus.ON_ERROR)
|
||||||
|
|
||||||
|
currentPayDetail.isNeedReversal = true
|
||||||
|
isSecondCall = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
postStatus(TransactionStatus.ON_ERROR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onComplete() {
|
||||||
|
if (currentPayDetail.isNeedReversal) {
|
||||||
|
flag = true
|
||||||
|
batchUploadProcess()
|
||||||
|
} else {
|
||||||
|
flag = false
|
||||||
|
batchIndex = 0
|
||||||
|
|
||||||
|
updateDB()
|
||||||
|
insertPayDetail(currentPayDetail)
|
||||||
|
|
||||||
|
if (errorFlag) {
|
||||||
|
postStatus(TransactionStatus.ON_ERROR)
|
||||||
|
} else {
|
||||||
|
postStatus(TransactionStatus.ON_SUCCESS)
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(isLoading = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun batchUploadProcess() {
|
||||||
|
_uiState.value = _uiState.value.copy(processingState = ProcessingState("Processing Batch Upload"))
|
||||||
|
postStatus(TransactionStatus.ON_BATCH_UPLOAD)
|
||||||
|
|
||||||
|
val currentPayDetails = payDetails
|
||||||
|
|
||||||
|
if (currentPayDetails.isNullOrEmpty()) {
|
||||||
|
requestOnlineProcessSettlement()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val uploadPayDetail = currentPayDetails[batchIndex]
|
||||||
|
|
||||||
|
val tradeData = TradeData().apply {
|
||||||
|
payDetail = uploadPayDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
BatchUploadProcess.getInstance()
|
||||||
|
.enqueue(tradeData)
|
||||||
|
.startBatchUpload(object : BatchListener {
|
||||||
|
|
||||||
|
override fun onSuccessBatch() {
|
||||||
|
if (batchIndex < currentPayDetails.size - 1) {
|
||||||
|
LogUtil.d(TAG, "Pay detail size: ${currentPayDetails.size}")
|
||||||
|
LogUtil.d(TAG, "Count value: $batchIndex")
|
||||||
|
|
||||||
|
batchIndex++
|
||||||
|
batchUploadProcess()
|
||||||
|
} else {
|
||||||
|
requestOnlineProcessSettlement()
|
||||||
|
}
|
||||||
|
|
||||||
|
LogUtil.e(TAG, "Batch Upload Success")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailBatch() {
|
||||||
|
LogUtil.e(TAG, "Batch Upload Fail")
|
||||||
|
postStatus(TransactionStatus.ON_ERROR)
|
||||||
|
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(isLoading = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun postStatus(status: TransactionStatus) {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(status = status)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
_events.send(SettlementEvent.ShowStatus(status))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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,169 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.tms_setup
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.mob.utsmyanmar.model.sirius.SiriusResponse
|
||||||
|
import com.mob.utsmyanmar.model.sirius.TMSUpdate
|
||||||
|
import com.mob.utsmyanmar.model.sirius.ValidityStatus
|
||||||
|
import com.mob.utsmyanmar.utils.tms.TMSSetupsImpl
|
||||||
|
import com.mob.utsmyanmar.utils.tms.TMSUtil
|
||||||
|
import com.utsmyanmar.baselib.BaseApplication
|
||||||
|
import com.utsmyanmar.baselib.emv.EmvParamOperation
|
||||||
|
import com.utsmyanmar.baselib.network.model.sirius.SiriusRequest
|
||||||
|
import com.utsmyanmar.baselib.repo.Repository
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import sunmi.sunmiui.utils.LogUtil
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
data class TmsSetupUiState(
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val statusText: String = "Initializing...",
|
||||||
|
val isError: Boolean = false,
|
||||||
|
val errorMessage: String = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class TmsSetupViewModel @Inject constructor(
|
||||||
|
private val repository: Repository,
|
||||||
|
private val emvParamOperation: EmvParamOperation
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow(TmsSetupUiState())
|
||||||
|
val uiState: StateFlow<TmsSetupUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
private val _navigateToDashboard = MutableSharedFlow<Unit>()
|
||||||
|
val navigateToDashboard: SharedFlow<Unit> = _navigateToDashboard.asSharedFlow()
|
||||||
|
|
||||||
|
private val disposables = CompositeDisposable()
|
||||||
|
private val tmsSetups = TMSSetupsImpl()
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
waitForHardware()
|
||||||
|
TMSUtil.getInstance().generateFinalVersion()
|
||||||
|
downloadConfigs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadConfigs() {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(isLoading = true, isError = false, statusText = "Connecting to TMS server...")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
val disposable = repository.getParams(buildRequest())
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{ response ->
|
||||||
|
_uiState.update { it.copy(statusText = "Applying configuration...") }
|
||||||
|
|
||||||
|
val appResponse = SiriusResponse(
|
||||||
|
serial = response.serial.orEmpty(),
|
||||||
|
ecrKey = response.ecrKey.orEmpty(),
|
||||||
|
address = response.address.orEmpty(),
|
||||||
|
merchant = response.merchant,
|
||||||
|
hosts = response.hosts,
|
||||||
|
properties = response.properties
|
||||||
|
)
|
||||||
|
|
||||||
|
tmsSetups.initParams(appResponse, TMSUpdate.UPDATE, emvParamOperation)
|
||||||
|
onConfigApplied()
|
||||||
|
},
|
||||||
|
{ error ->
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
isError = true,
|
||||||
|
statusText = "Download failed",
|
||||||
|
errorMessage = formatNetworkError(error)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
disposables.add(disposable)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun waitForHardware() {
|
||||||
|
_uiState.update { it.copy(isLoading = true, statusText = "Starting hardware...") }
|
||||||
|
var elapsed = 0
|
||||||
|
while (BaseApplication.basicOptV2 == null && elapsed < 10_000) {
|
||||||
|
delay(500)
|
||||||
|
elapsed += 500
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onConfigApplied() {
|
||||||
|
val validity = TMSUtil.getInstance().checkParams()
|
||||||
|
if (validity.status == ValidityStatus.SUCCESS) {
|
||||||
|
_uiState.update { it.copy(isLoading = false, statusText = "Ready.") }
|
||||||
|
viewModelScope.launch { _navigateToDashboard.emit(Unit) }
|
||||||
|
} else {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
isError = true,
|
||||||
|
statusText = "Configuration incomplete",
|
||||||
|
errorMessage = validity.message ?: "Invalid TMS configuration"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun skipDownload() {
|
||||||
|
viewModelScope.launch { _navigateToDashboard.emit(Unit) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatNetworkError(error: Throwable): String {
|
||||||
|
return when (error) {
|
||||||
|
is javax.net.ssl.SSLHandshakeException ->
|
||||||
|
"SSL handshake failed: ${error.message ?: "Certificate or protocol mismatch"}"
|
||||||
|
is javax.net.ssl.SSLException ->
|
||||||
|
"SSL/TLS error: ${error.message ?: "Secure connection could not be established"}"
|
||||||
|
is java.security.cert.CertificateException ->
|
||||||
|
"Server certificate error: ${error.message ?: "Certificate is invalid or untrusted"}"
|
||||||
|
is java.net.UnknownHostException ->
|
||||||
|
"Host not found: ${error.message ?: "Check server URL and network connection"}"
|
||||||
|
is java.net.ConnectException ->
|
||||||
|
"Connection refused: ${error.message ?: "Server is unreachable"}"
|
||||||
|
is java.net.SocketTimeoutException ->
|
||||||
|
"Connection timed out: ${error.message ?: "Server did not respond in time"}"
|
||||||
|
is retrofit2.HttpException ->
|
||||||
|
"HTTP ${error.code()} ${error.message()}"
|
||||||
|
else ->
|
||||||
|
error.message ?: "Unknown network error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
private fun buildRequest(): SiriusRequest {
|
||||||
|
return try {
|
||||||
|
val tranTime: Long = System.currentTimeMillis()
|
||||||
|
TMSUtil.getInstance().generateRequestParams("...", tranTime)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtil.e("TmsSetupViewModel", e.message)
|
||||||
|
SiriusRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
disposables.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,332 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.transaction_result
|
||||||
|
|
||||||
|
import android.R
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.CalendarMonth
|
||||||
|
import androidx.compose.material.icons.filled.Numbers
|
||||||
|
import androidx.compose.material.icons.filled.Print
|
||||||
|
import androidx.compose.material.icons.rounded.Check
|
||||||
|
import androidx.compose.material.icons.rounded.Close
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.mob.utsmyanmar.ui.preview.P2Preview
|
||||||
|
import com.mob.utsmyanmar.ui.theme.Color
|
||||||
|
import com.utsmyanmar.paylibs.model.PayDetail
|
||||||
|
import com.utsmyanmar.paylibs.print.PrintReceipt
|
||||||
|
import com.utsmyanmar.paylibs.utils.POSUtil
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TransactionResultScreen(
|
||||||
|
state: TransactionResultState,
|
||||||
|
canGoBack: Boolean,
|
||||||
|
onEvent: (TransactionResultEvent) -> Unit,
|
||||||
|
) {
|
||||||
|
BackHandler(enabled = canGoBack) {
|
||||||
|
onEvent(TransactionResultEvent.BackClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
containerColor = Color.IvoryBeige
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.IvoryBeige)
|
||||||
|
.padding(paddingValues)
|
||||||
|
.statusBarsPadding()
|
||||||
|
.navigationBarsPadding()
|
||||||
|
.padding(horizontal = 20.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
if(state.payDetail?.tradeAnswerCode.equals("00")) SuccessIcon()
|
||||||
|
else FailIcon()
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = state.title,
|
||||||
|
color = Color.LegacyRed,
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = state.message.ifBlank { "Your payment has been processed successfully." },
|
||||||
|
color = Color.Gray,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(28.dp))
|
||||||
|
|
||||||
|
AmountCard(amount = POSUtil.getInstance().getDecimalAmountSeparatorFormat(state.payDetail?.amount?:0))
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
InfoCard(
|
||||||
|
date = "26 May 2026",
|
||||||
|
time = "12:06 PM",
|
||||||
|
transactionId = state.payDetail?.referNo.toString()
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SuccessIcon() {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.size(132.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(132.dp)
|
||||||
|
.background(
|
||||||
|
color = Color.Success.copy(alpha = 0.12f),
|
||||||
|
shape = CircleShape
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(96.dp)
|
||||||
|
.background(
|
||||||
|
color = Color.Success,
|
||||||
|
shape = CircleShape
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Check,
|
||||||
|
contentDescription = "Success",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(64.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FailIcon() {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.size(132.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(132.dp)
|
||||||
|
.background(
|
||||||
|
color = Color.Error.copy(alpha = 0.12f),
|
||||||
|
shape = CircleShape
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(96.dp)
|
||||||
|
.background(
|
||||||
|
color = Color.Error,
|
||||||
|
shape = CircleShape
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Close,
|
||||||
|
contentDescription = "Failed",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(64.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AmountCard(
|
||||||
|
amount: String
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(130.dp)
|
||||||
|
.shadow(
|
||||||
|
elevation = 6.dp,
|
||||||
|
shape = RoundedCornerShape(22.dp),
|
||||||
|
clip = false
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(22.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = Color.White
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(vertical = 18.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "PAID AMOUNT",
|
||||||
|
color = Color.Gray,
|
||||||
|
fontSize = 15.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = amount,
|
||||||
|
color = Color.LegacyRed,
|
||||||
|
fontSize = 30.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "MMK",
|
||||||
|
color = Color.LegacyRed,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun InfoCard(
|
||||||
|
date: String,
|
||||||
|
time: String,
|
||||||
|
transactionId: String
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(130.dp)
|
||||||
|
.shadow(
|
||||||
|
elevation = 4.dp,
|
||||||
|
shape = RoundedCornerShape(18.dp),
|
||||||
|
clip = false
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(18.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = Color.White
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 18.dp),
|
||||||
|
verticalArrangement = Arrangement.SpaceAround
|
||||||
|
) {
|
||||||
|
InfoItem(
|
||||||
|
icon = Icons.Default.CalendarMonth,
|
||||||
|
title = "Date & Time",
|
||||||
|
value = date,
|
||||||
|
subValue = time,
|
||||||
|
// modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
InfoItem(
|
||||||
|
icon = Icons.Default.Numbers,
|
||||||
|
title = "Transaction ID",
|
||||||
|
value = transactionId,
|
||||||
|
subValue = "",
|
||||||
|
// modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun InfoItem(
|
||||||
|
icon: ImageVector,
|
||||||
|
title: String,
|
||||||
|
value: String,
|
||||||
|
subValue: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(46.dp)
|
||||||
|
.background(
|
||||||
|
color = Color.LegacyRed.copy(alpha = 0.1f),
|
||||||
|
shape = CircleShape
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.LegacyRed,
|
||||||
|
modifier = Modifier.size(25.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
color = Color.Gray,
|
||||||
|
fontSize = 11.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = value,
|
||||||
|
color = Color.Black,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
|
||||||
|
if (subValue.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = subValue,
|
||||||
|
color = Color.Black,
|
||||||
|
fontSize = 11.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@P2Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewTransactionResultScreen() {
|
||||||
|
TransactionResultScreen(
|
||||||
|
state = TransactionResultState(
|
||||||
|
"Success",
|
||||||
|
payDetail = PayDetail()
|
||||||
|
),
|
||||||
|
canGoBack = true,
|
||||||
|
onEvent = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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,287 @@
|
|||||||
|
package com.mob.utsmyanmar.ui.transaction_result
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.mob.utsmyanmar.model.ecr.ECRResultStatus
|
||||||
|
import com.mob.utsmyanmar.utils.CoreUtils
|
||||||
|
import com.mob.utsmyanmar.viewmodel.SharedViewModel
|
||||||
|
import com.utsmyanmar.baselib.emv.EmvParamOperation
|
||||||
|
import com.utsmyanmar.baselib.network.model.sirius.SiriusRequest
|
||||||
|
import com.utsmyanmar.ecr.ECRHelper
|
||||||
|
import com.utsmyanmar.ecr.ECRProcess
|
||||||
|
import com.utsmyanmar.ecr.data.model.TransactionsResp
|
||||||
|
import com.utsmyanmar.paylibs.Constant
|
||||||
|
import com.utsmyanmar.paylibs.model.PayDetail
|
||||||
|
import com.utsmyanmar.paylibs.print.PaperRollStatusCallback
|
||||||
|
import com.utsmyanmar.paylibs.print.PrintHelper
|
||||||
|
import com.utsmyanmar.paylibs.utils.PrintStatus
|
||||||
|
import com.utsmyanmar.paylibs.utils.core_utils.ByteUtil
|
||||||
|
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
|
||||||
|
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import sunmi.sunmiui.utils.LogUtil
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class TransactionResultViewModel @Inject constructor(
|
||||||
|
private val emvParamOperation: EmvParamOperation
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "TransactionResultVM"
|
||||||
|
private const val RESULT_TIMEOUT = 3000L
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow(TransactionResultState())
|
||||||
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
private val _uiEvent = Channel<TransactionResultUiEvent>()
|
||||||
|
val uiEvent = _uiEvent.receiveAsFlow()
|
||||||
|
|
||||||
|
private var started = false
|
||||||
|
|
||||||
|
fun onEvent(
|
||||||
|
event: TransactionResultEvent,
|
||||||
|
sharedViewModel: SharedViewModel
|
||||||
|
) {
|
||||||
|
when (event) {
|
||||||
|
TransactionResultEvent.Start -> {
|
||||||
|
started = false
|
||||||
|
start(sharedViewModel)
|
||||||
|
}
|
||||||
|
TransactionResultEvent.BackClick -> isCardInside()
|
||||||
|
TransactionResultEvent.RetryPrint -> startPrintProcess(sharedViewModel, false)
|
||||||
|
TransactionResultEvent.PrintLater -> isCardInside()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun start(sharedViewModel: SharedViewModel) {
|
||||||
|
|
||||||
|
if (started) return
|
||||||
|
started = true
|
||||||
|
|
||||||
|
_state.value = _state.value.copy(payDetail = sharedViewModel.payDetail.value)
|
||||||
|
|
||||||
|
sharedViewModel.setPrintStatus(PrintStatus.FIRST_PRINT)
|
||||||
|
sharedViewModel.printXStatus.value = null
|
||||||
|
|
||||||
|
updateTitle(sharedViewModel)
|
||||||
|
observePrintStatus(sharedViewModel)
|
||||||
|
|
||||||
|
if (sharedViewModel.isEcr.value == true) {
|
||||||
|
ecrAction(sharedViewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
startResultTimeout(sharedViewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateTitle(sharedViewModel: SharedViewModel) {
|
||||||
|
val payDetail = sharedViewModel.payDetail.value
|
||||||
|
val resultCode = payDetail?.tradeAnswerCode.orEmpty()
|
||||||
|
val qrStatus = payDetail?.qrTransStatus ?: -100
|
||||||
|
|
||||||
|
val isDemoMode = SystemParamsOperation.getInstance().demoStatus
|
||||||
|
|
||||||
|
val isSuccess = isDemoMode ||
|
||||||
|
resultCode == Constant.ANSWER_CODE_ACCEPT ||
|
||||||
|
resultCode == Constant.ANSWER_CODE_APPROVED
|
||||||
|
|
||||||
|
val title = when {
|
||||||
|
isSuccess -> "Transaction Success"
|
||||||
|
qrStatus == 1 || qrStatus == -1 || qrStatus == 2 || qrStatus == 3 -> "QR Pay"
|
||||||
|
else -> "Error"
|
||||||
|
}
|
||||||
|
|
||||||
|
val message = when {
|
||||||
|
isSuccess -> "Transaction approved"
|
||||||
|
payDetail?.tradeAnswerCode?.isNotBlank() == true -> payDetail.tradeAnswerCode
|
||||||
|
else -> "Transaction failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
_state.value = _state.value.copy(
|
||||||
|
title = title,
|
||||||
|
message = message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startResultTimeout(sharedViewModel: SharedViewModel) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
delay(RESULT_TIMEOUT)
|
||||||
|
|
||||||
|
val payDetail = sharedViewModel.payDetail.value
|
||||||
|
|
||||||
|
if (payDetail == null) {
|
||||||
|
navigateMain()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val transactionType = sharedViewModel.transactionsType.value
|
||||||
|
|
||||||
|
when {
|
||||||
|
isNonApprovedTrade(payDetail) &&
|
||||||
|
isNonWavepayTransaction(transactionType) -> {
|
||||||
|
startPrintProcess(sharedViewModel, false)
|
||||||
|
isCardInside()
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionType == TransactionsType.SETTLEMENT -> {
|
||||||
|
startPrintProcess(sharedViewModel, true)
|
||||||
|
sendUiEvent(
|
||||||
|
TransactionResultUiEvent.ShowSuccess(
|
||||||
|
"Configs are updated"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
navigateMain()
|
||||||
|
}
|
||||||
|
|
||||||
|
isWavePayNonSuccessTransaction(transactionType, payDetail) -> {
|
||||||
|
startPrintProcess(sharedViewModel, false)
|
||||||
|
navigateMain()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
SystemParamsOperation.getInstance()
|
||||||
|
.setLastSuccessTrnx(payDetail.voucherNo)
|
||||||
|
|
||||||
|
emvParamOperation.loadEmvTerminalParam()
|
||||||
|
navigatePrint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isNonApprovedTrade(payDetail: PayDetail): Boolean {
|
||||||
|
return payDetail.tradeAnswerCode != Constant.ANSWER_CODE_APPROVED &&
|
||||||
|
payDetail.tradeAnswerCode != Constant.ANSWER_CODE_ACCEPT
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isNonWavepayTransaction(type: TransactionsType?): Boolean {
|
||||||
|
return type != TransactionsType.WAVEPAY &&
|
||||||
|
type != TransactionsType.WAVE_INQUIRY_STATUS &&
|
||||||
|
type != TransactionsType.WAVEPAY_REFUND
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isWavePayNonSuccessTransaction(
|
||||||
|
type: TransactionsType?,
|
||||||
|
payDetail: PayDetail
|
||||||
|
): Boolean {
|
||||||
|
return (type == TransactionsType.WAVEPAY && payDetail.qrTransStatus != 1) ||
|
||||||
|
(type == TransactionsType.WAVE_INQUIRY_STATUS && payDetail.qrTransStatus != 1) ||
|
||||||
|
(type == TransactionsType.WAVEPAY_REFUND && payDetail.qrTransStatus != 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observePrintStatus(sharedViewModel: SharedViewModel) {
|
||||||
|
sharedViewModel.printXStatus.observeForever { status ->
|
||||||
|
when (status) {
|
||||||
|
PrintStatus.EMPTY_PAPER_ROLL -> {
|
||||||
|
startPrintProcess(sharedViewModel, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
PrintStatus.DONE_PRINT -> {
|
||||||
|
isCardInside()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startPrintProcess(
|
||||||
|
sharedViewModel: SharedViewModel,
|
||||||
|
isSettlement: Boolean
|
||||||
|
) {
|
||||||
|
PrintHelper.getInstance().checkPaperRollStatus(
|
||||||
|
object : PaperRollStatusCallback {
|
||||||
|
|
||||||
|
override fun paperRollIsReady() {
|
||||||
|
if (isSettlement) {
|
||||||
|
sharedViewModel.startPrintProcessSettlement()
|
||||||
|
} else {
|
||||||
|
sharedViewModel.startPrintProcess()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun paperRollIsEmpty() {
|
||||||
|
sendUiEvent(
|
||||||
|
TransactionResultUiEvent.ShowPrinterDialog(
|
||||||
|
"Paper roll not ready"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun paperRollLipIsOpened() {
|
||||||
|
sendUiEvent(
|
||||||
|
TransactionResultUiEvent.ShowPrinterDialog(
|
||||||
|
"Printer lip is opened. Please close lip."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unknownStatusOccur(code: Int) {
|
||||||
|
sendUiEvent(
|
||||||
|
TransactionResultUiEvent.ShowError(
|
||||||
|
"Check printer status: $code"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getECRResponseMessage(
|
||||||
|
sharedViewModel: SharedViewModel
|
||||||
|
): String {
|
||||||
|
val resp: TransactionsResp =
|
||||||
|
CoreUtils.getInstance(sharedViewModel).generateResponseMsg()
|
||||||
|
|
||||||
|
return ECRProcess.generateECRResponse(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getECRResponseCMHL(
|
||||||
|
sharedViewModel: SharedViewModel
|
||||||
|
): ByteArray {
|
||||||
|
return CoreUtils.getInstance(sharedViewModel)
|
||||||
|
.generateCMHLResponse(ECRResultStatus.RESPONSE_RECEIVED)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ecrAction(sharedViewModel: SharedViewModel) {
|
||||||
|
if (SystemParamsOperation.getInstance().isCMHLEnabled) {
|
||||||
|
val response = getECRResponseCMHL(sharedViewModel)
|
||||||
|
|
||||||
|
LogUtil.d(TAG, "ECR Response: ${ByteUtil.bytes2HexStr(response)}")
|
||||||
|
|
||||||
|
ECRHelper.send(response)
|
||||||
|
CoreUtils.getInstance(sharedViewModel).responseACKCMHL()
|
||||||
|
} else {
|
||||||
|
val response = getECRResponseMessage(sharedViewModel)
|
||||||
|
|
||||||
|
LogUtil.d(TAG, "ECR Response: $response")
|
||||||
|
|
||||||
|
ECRHelper.send(response.toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isCardInside() {
|
||||||
|
navigateMain()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateMain() {
|
||||||
|
sendUiEvent(TransactionResultUiEvent.NavigateMain)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigatePrint() {
|
||||||
|
sendUiEvent(TransactionResultUiEvent.NavigatePrintReceipt)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendUiEvent(event: TransactionResultUiEvent) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiEvent.send(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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
|
||||||
|
|
||||||
|
}
|
||||||
275
app/src/main/java/com/mob/utsmyanmar/utils/tms/TMSSetupsImpl.kt
Normal file
275
app/src/main/java/com/mob/utsmyanmar/utils/tms/TMSSetupsImpl.kt
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
package com.mob.utsmyanmar.utils.tms
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.mob.utsmyanmar.model.sirius.SiriusResponse
|
||||||
|
import com.mob.utsmyanmar.model.sirius.TMSUpdate
|
||||||
|
import com.utsmyanmar.baselib.BaseApplication
|
||||||
|
import com.utsmyanmar.baselib.emv.EmvParamOperation
|
||||||
|
import com.utsmyanmar.baselib.network.model.sirius.SiriusHost
|
||||||
|
import com.utsmyanmar.baselib.network.model.sirius.SiriusMerchant
|
||||||
|
import com.utsmyanmar.baselib.network.model.sirius.SiriusProperty
|
||||||
|
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
|
||||||
|
import com.utsmyanmar.paylibs.utils.enums.CurrencyType
|
||||||
|
import sunmi.sunmiui.utils.LogUtil
|
||||||
|
|
||||||
|
class TMSSetupsImpl : TMSSetups {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "TMSSetupsImpl"
|
||||||
|
private const val UPI = "UPI"
|
||||||
|
private const val JCB = "JCB"
|
||||||
|
private const val VISA = "VISA"
|
||||||
|
private const val MASTERCARD = "MASTERCARD"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun currencyTextToCurrencyType(currencyText: String): CurrencyType {
|
||||||
|
return try {
|
||||||
|
CurrencyType.valueOf(currencyText)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
CurrencyType.MMK
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun currencyTextToCode(currencyText: String): String {
|
||||||
|
return when (currencyText) {
|
||||||
|
"USD" -> "804"
|
||||||
|
"CNY" -> "156"
|
||||||
|
"THB" -> "764"
|
||||||
|
"RUB" -> "643"
|
||||||
|
else -> "104"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun currencyCodeToText(currencyCode: String): String {
|
||||||
|
return when (currencyCode) {
|
||||||
|
"804" -> "USD"
|
||||||
|
"156" -> "CNY"
|
||||||
|
"764" -> "THB"
|
||||||
|
"643" -> "RUB"
|
||||||
|
else -> "MMK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun init() {
|
||||||
|
SystemParamsOperation.getInstance().apply {
|
||||||
|
hostName = ""
|
||||||
|
terminalId = ""
|
||||||
|
merchantId = ""
|
||||||
|
ipAddress = ""
|
||||||
|
secIpAddress = ""
|
||||||
|
|
||||||
|
secHostName = ""
|
||||||
|
secHostTerminalId = ""
|
||||||
|
secHostMerchantId = ""
|
||||||
|
secHostIpAddress = ""
|
||||||
|
secHostSecIpAddress = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun initParams(
|
||||||
|
siriusResponse: SiriusResponse,
|
||||||
|
tmsUpdate: TMSUpdate,
|
||||||
|
emvParamOperation: EmvParamOperation
|
||||||
|
) {
|
||||||
|
val siriusMerchant: SiriusMerchant = siriusResponse.merchant
|
||||||
|
val siriusHosts: List<SiriusHost> = siriusResponse.hosts
|
||||||
|
val siriusProperty: List<SiriusProperty> = siriusResponse.properties
|
||||||
|
val imgUrls = mutableListOf<String>()
|
||||||
|
val ops = SystemParamsOperation.getInstance()
|
||||||
|
|
||||||
|
if (tmsUpdate == TMSUpdate.UPDATE) ops.isNeedSettlement = false
|
||||||
|
|
||||||
|
siriusResponse.address.takeIf { it.isEmpty() }?.let { ops.merchantAddress = "" }
|
||||||
|
|
||||||
|
init()
|
||||||
|
|
||||||
|
siriusMerchant.let { m ->
|
||||||
|
ops.merchantName = m.name
|
||||||
|
ops.merchantAddress = m.address
|
||||||
|
ops.merchantPhoneNo = m.phone
|
||||||
|
}
|
||||||
|
|
||||||
|
siriusResponse.address.takeIf { it.isNotEmpty() }?.let { ops.merchantAddress = it }
|
||||||
|
//host
|
||||||
|
if (siriusHosts.isNotEmpty()) {
|
||||||
|
for (host in siriusHosts) {
|
||||||
|
val isMMQR = listOf(host.name, host.description).any {
|
||||||
|
it.lowercase().run { contains("mmqr") }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMMQR) {
|
||||||
|
ops.secHostName = host.name
|
||||||
|
ops.secHostTerminalId = host.tid
|
||||||
|
ops.secHostMerchantId = host.mid
|
||||||
|
|
||||||
|
host.secondaryIP.trim().let { ip ->
|
||||||
|
ops.secHostIpAddress = if (ip.contains(":")) "$ip/" else ""
|
||||||
|
}
|
||||||
|
host.currency.takeIf { it.isNotEmpty() }?.let {
|
||||||
|
ops.secHostCurrency = currencyTextToCode(it)
|
||||||
|
}
|
||||||
|
if (host.tid.isEmpty()) ops.secHostTerminalId = ""
|
||||||
|
if (host.mid.isEmpty()) ops.secHostTerminalId = ""
|
||||||
|
} else {
|
||||||
|
ops.hostName = host.name
|
||||||
|
ops.terminalId = host.tid
|
||||||
|
ops.merchantId = host.mid
|
||||||
|
|
||||||
|
host.primaryIP.trim().let { ip ->
|
||||||
|
ops.ipAddress = if (ip.contains(":")) ip else ""
|
||||||
|
}
|
||||||
|
host.secondaryIP.trim().let { ip ->
|
||||||
|
ops.secIpAddress = if (ip.contains(":")) ip else ""
|
||||||
|
}
|
||||||
|
host.currency.takeIf { it.isNotEmpty() }?.let {
|
||||||
|
ops.currencyType = currencyTextToCurrencyType(it)
|
||||||
|
}
|
||||||
|
if (host.tid.isEmpty()) ops.terminalId = ""
|
||||||
|
if (host.mid.isEmpty()) ops.merchantId = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Properties
|
||||||
|
for (prop in siriusProperty) {
|
||||||
|
val name = prop.name
|
||||||
|
val data = prop.property
|
||||||
|
|
||||||
|
when (name) {
|
||||||
|
//image
|
||||||
|
"carousel_img_1",
|
||||||
|
"carousel_img_2",
|
||||||
|
"carousel_img_3",
|
||||||
|
"carousel_img_4",
|
||||||
|
"carousel_img_5",
|
||||||
|
"carousel_img_6" -> imgUrls.add(data)
|
||||||
|
//host
|
||||||
|
"host_timeout",
|
||||||
|
"host_connect_timeout" -> ops.hostResponseTimeout = data
|
||||||
|
"host_read_timeout" -> ops.hostReadTimeout = data
|
||||||
|
"reversal_delay" -> ops.reversalDelay = data
|
||||||
|
"key_index" -> ops.tmkIndex = data
|
||||||
|
"receipt_footer" -> ops.receiptFooter = data
|
||||||
|
"time_out" -> ops.setTmsTimeout(data)
|
||||||
|
"manual_update" -> ops.manualUpdate = parseBoolean(data)
|
||||||
|
"emv_enable" -> ops.setEmvEnable(parseBoolean(data))
|
||||||
|
"hostport" -> ops.portAddress = data.toInt()
|
||||||
|
"pre_auth_enable" -> ops.preAuthStatus = parseBoolean(data)
|
||||||
|
"void_enable" -> ops.voidStatus = parseBoolean(data)
|
||||||
|
"cash_advance_enable" -> ops.cashAdvanceStatus = parseBoolean(data)
|
||||||
|
"refund_enable" -> ops.refundStatus = parseBoolean(data)
|
||||||
|
"settlement_enable" -> ops.settlementStatus = parseBoolean(data)
|
||||||
|
"system_password" -> ops.systemPassword = data.take(6).ifEmpty { data }
|
||||||
|
"settlement_password" -> ops.settlementPassword = data.take(6).ifEmpty { data }
|
||||||
|
"setting_password" -> ops.settingPassword = data.take(6).ifEmpty { data }
|
||||||
|
"terminal_enable" -> ops.isActive = parseBoolean(data)
|
||||||
|
"terminal_enable_msg" -> ops.disabledMsg = data
|
||||||
|
"ssl_enable" -> ops.setSslSwitchStatus(parseBoolean(data))
|
||||||
|
"wave_pay_inquiry_status_enable" -> ops.wavePayInquiryStatus = parseBoolean(data)
|
||||||
|
"tips_adjustment_enable" -> ops.tipsAdjustmentStatus = parseBoolean(data)
|
||||||
|
"wave_enable" -> ops.wavePayStatus = parseBoolean(data)
|
||||||
|
"print_iso_enable" -> ops.printISOStatus = parseBoolean(data)
|
||||||
|
"receipt_header" -> ops.receiptHeader = data
|
||||||
|
"random_pin_pad_enable" -> ops.isRandomPinPad = parseBoolean(data)
|
||||||
|
"clear_batch_time" -> ops.clearBatchTime = data
|
||||||
|
"alert_sound_enable" -> ops.isAlertSound = parseBoolean(data)
|
||||||
|
"auto_print_enable" -> ops.isAutoPrintCustomerCopy = parseBoolean(data)
|
||||||
|
"ecr_enable" -> ops.ecrStatus = parseBoolean(data)
|
||||||
|
"manual_entry_enable" -> ops.setManualEntyrStatus(parseBoolean(data))
|
||||||
|
"mmqr_interval_waiting_time" -> ops.waveIntervalTime = data
|
||||||
|
"full_void_preauth_enable" -> ops.fullVoidPreauthStatus = parseBoolean(data)
|
||||||
|
"partial_void_preauth_enable" -> ops.partialVoidPreauthStatus = parseBoolean(data)
|
||||||
|
"clear_batch_day" -> ops.clearBatchDay = data
|
||||||
|
"qr_min_amount" -> ops.minAmount = data
|
||||||
|
"qr_max_amount" -> ops.maxAmount = data
|
||||||
|
"mmqr_auth_token" -> ops.authToken = data
|
||||||
|
"mmqr_grant_type" -> ops.grantType = data
|
||||||
|
"mmqr_token_host_address" -> ops.tokenHostAddress = "${data.trim()}/"
|
||||||
|
"mmpay_enable" -> ops.isMMPayEnabled = parseBoolean(data)
|
||||||
|
"fallback_enable" -> ops.fallbackEnabled = parseBoolean(data)
|
||||||
|
"magstripe_enable" -> ops.isMagStripeEnabled = parseBoolean(data)
|
||||||
|
"nfc_enable" -> ops.isNfcEnabled = parseBoolean(data)
|
||||||
|
"cvv_bypass_enable" -> ops.cvvBypassStatus = parseBoolean(data)
|
||||||
|
"upi_chip_cvm" -> emvParamOperation.updateChipCVM(UPI, data.toLong())
|
||||||
|
"upi_contactless_cvm" -> {
|
||||||
|
val limit = data.toLong()
|
||||||
|
ops.upiCvmLimit = limit
|
||||||
|
emvParamOperation.updateUpiCVM(limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
"upi_currency_code" -> emvParamOperation.updateUpiCurrencyCode(data)
|
||||||
|
"jcb_chip_cvm" -> emvParamOperation.updateChipCVM(JCB, data.toLong())
|
||||||
|
"visa_chip_cvm" -> emvParamOperation.updateChipCVM(VISA, data.toLong())
|
||||||
|
"visa_contactless_cvm" -> emvParamOperation.updatePayWaveCVM(data.toLong())
|
||||||
|
"visa_currency_code" -> emvParamOperation.updatePayWaveCurrencyCode(data)
|
||||||
|
"master_chip_cvm" -> emvParamOperation.updateChipCVM(MASTERCARD, data.toLong())
|
||||||
|
"master_contactless_cvm" -> emvParamOperation.updatePayPassCVM(data.toLong())
|
||||||
|
"master_currency_code" -> emvParamOperation.updatePayPassCurrencyCode(data)
|
||||||
|
"terminal_capability" -> {
|
||||||
|
if (data.isNotEmpty()) ops.setTerminalCapability(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
"upi_ttq" -> emvParamOperation.updateQuickPassTTQ(data)
|
||||||
|
"visa_ttq" -> emvParamOperation.updatePayWaveTTQ(data)
|
||||||
|
"master_ttq" -> emvParamOperation.updatePayPassTTQ(data)
|
||||||
|
"upi_tc_enabled" -> ops.setUpiTCEnabled(parseBoolean(data))
|
||||||
|
"debug_feature_enable" -> ops.setDebugFeatureEnabled(parseBoolean(data))
|
||||||
|
"master_terminal_capability" -> {
|
||||||
|
if (data.isNotEmpty()) emvParamOperation.updatePayPassTerminalCapability(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
"speedup_contactless_enable" -> ops.setSpeedUpContactless(parseBoolean(data))
|
||||||
|
"manual_entry_pin_enable" -> ops.isManualEntryPinEnable = parseBoolean(data)
|
||||||
|
"cmhl_enabled" -> ops.setCMHLEnable(parseBoolean(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ops.carouselUrls = imgUrls.joinToString(",")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun initParams(json: String) {
|
||||||
|
val response = Gson().fromJson<SiriusResponse>(json, SiriusResponse::class.java)
|
||||||
|
response.properties.forEach { prop ->
|
||||||
|
LogUtil.d(TAG, "name : ${prop.name}")
|
||||||
|
LogUtil.d(TAG, "value: ${prop.property}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun convertToArray(string: String): ArrayList<String> {
|
||||||
|
if (string.isEmpty()) return ArrayList()
|
||||||
|
return ArrayList(string.split(",").filter { it.isNotEmpty() })
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPayHardwareVersion(): String {
|
||||||
|
return try {
|
||||||
|
BaseApplication.getInstance().applicationContext.packageManager
|
||||||
|
.getPackageInfo("com.sunmi.pay.hardware_v3", 0)
|
||||||
|
.versionName ?: "?"
|
||||||
|
} catch (e: Exception) {
|
||||||
|
"?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRomVersion(): String {
|
||||||
|
return try {
|
||||||
|
android.os.Build.VERSION.RELEASE
|
||||||
|
} catch (e: Exception) {
|
||||||
|
"UNKNOWN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun generateFinalVersion(): String {
|
||||||
|
val phv = getPayHardwareVersion()
|
||||||
|
val rv = getRomVersion()
|
||||||
|
val sv = try {
|
||||||
|
BaseApplication.getInstance().applicationContext.packageManager
|
||||||
|
.getPackageInfo(BaseApplication.getInstance().packageName, 0)
|
||||||
|
.versionName ?: "?"
|
||||||
|
} catch (e: Exception) {
|
||||||
|
"?"
|
||||||
|
}
|
||||||
|
SystemParamsOperation.getInstance().setFinalVersion("PHV$phv-RV$rv-SV$sv")
|
||||||
|
return "PHV$phv-RV$rv-SV$sv"
|
||||||
|
}
|
||||||
|
private fun parseBoolean(data: String): Boolean =
|
||||||
|
data.toIntOrNull()?.let { it == 1 } ?: data.toBoolean();
|
||||||
|
}
|
||||||
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,393 @@
|
|||||||
|
package com.mob.utsmyanmar.viewmodel
|
||||||
|
|
||||||
|
import android.text.TextUtils
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.mob.utsmyanmar.model.TransResultStatus
|
||||||
|
import com.mob.utsmyanmar.ui.sending_to_host.ProcessingState
|
||||||
|
import com.utsmyanmar.baselib.repo.Repository
|
||||||
|
import com.utsmyanmar.paylibs.model.PayDetail
|
||||||
|
import com.utsmyanmar.paylibs.model.TradeData
|
||||||
|
import com.utsmyanmar.paylibs.network.ISOSocket
|
||||||
|
import com.utsmyanmar.paylibs.reversal.ReversalAction
|
||||||
|
import com.utsmyanmar.paylibs.reversal.ReversalListener
|
||||||
|
import com.utsmyanmar.paylibs.system.SystemDateTime
|
||||||
|
import com.utsmyanmar.paylibs.transactions.TransactionsOperation
|
||||||
|
import com.utsmyanmar.paylibs.transactions.TransactionsOperationListener
|
||||||
|
import com.utsmyanmar.paylibs.utils.PrintStatus
|
||||||
|
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
|
||||||
|
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class TransProcessViewModel @Inject constructor(
|
||||||
|
private val repository: Repository
|
||||||
|
) : ViewModel(), ProcessingTransaction {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val RC_APPROVED_V1 = "00"
|
||||||
|
private const val RC_APPROVED_V2 = "000"
|
||||||
|
private const val TRY_SECONDARY = "TRY_SECONDARY"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var pan: String = ""
|
||||||
|
|
||||||
|
private lateinit var tradeData: TradeData
|
||||||
|
|
||||||
|
var payDetail: PayDetail? = null
|
||||||
|
|
||||||
|
private var oldTransPayDetail: PayDetail? = null
|
||||||
|
|
||||||
|
private var isSecondCall = false
|
||||||
|
private var isThirdCall = false
|
||||||
|
|
||||||
|
/*
|
||||||
|
* States
|
||||||
|
*/
|
||||||
|
|
||||||
|
private val _transResultStatus =
|
||||||
|
MutableStateFlow<TransResultStatus?>(null)
|
||||||
|
|
||||||
|
val transResultStatus =
|
||||||
|
_transResultStatus.asStateFlow()
|
||||||
|
|
||||||
|
private val _transType = MutableStateFlow<TransactionsType?>(null)
|
||||||
|
|
||||||
|
val transType =_transType.asStateFlow()
|
||||||
|
|
||||||
|
fun setTransType(type: TransactionsType?) {
|
||||||
|
_transType.value = type
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetTransactionResultStatus() {
|
||||||
|
_transResultStatus.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _printStatus =
|
||||||
|
MutableStateFlow(PrintStatus.FIRST_PRINT)
|
||||||
|
|
||||||
|
val printStatus =
|
||||||
|
_printStatus.asStateFlow()
|
||||||
|
|
||||||
|
private val _payDetailResult =
|
||||||
|
MutableStateFlow<PayDetail?>(null)
|
||||||
|
|
||||||
|
val payDetailResult =
|
||||||
|
_payDetailResult.asStateFlow()
|
||||||
|
|
||||||
|
private val _errorMessage =
|
||||||
|
MutableStateFlow<String?>(null)
|
||||||
|
|
||||||
|
val errorMessage =
|
||||||
|
_errorMessage.asStateFlow()
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Setup
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun setTradeData(tradeData: TradeData) {
|
||||||
|
this.tradeData = tradeData
|
||||||
|
payDetail = tradeData.payDetail
|
||||||
|
pan = payDetail?.cardNo ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTradeData(): TradeData {
|
||||||
|
return tradeData
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updatePayDetail(payDetail: PayDetail) {
|
||||||
|
this.payDetail = payDetail
|
||||||
|
|
||||||
|
tradeData = TradeData().apply {
|
||||||
|
this.payDetail = payDetail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOldTransPayDetail(payDetail: PayDetail) {
|
||||||
|
oldTransPayDetail = payDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setNewPayDetail(payDetail: PayDetail) {
|
||||||
|
this.payDetail = payDetail
|
||||||
|
tradeData = TradeData()
|
||||||
|
tradeData.payDetail = payDetail
|
||||||
|
}
|
||||||
|
private val _state = MutableStateFlow(ProcessingState())
|
||||||
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Transaction
|
||||||
|
*/
|
||||||
|
|
||||||
|
override fun startOnlineProcess() {
|
||||||
|
|
||||||
|
_state.value = _state.value.copy("Processing")
|
||||||
|
|
||||||
|
TransactionsOperation.getInstance()
|
||||||
|
.getStartOperation(
|
||||||
|
tradeData,
|
||||||
|
transType.value
|
||||||
|
)
|
||||||
|
.checkOperation(object : TransactionsOperationListener {
|
||||||
|
|
||||||
|
override fun onSuccess(tradeData: TradeData) {
|
||||||
|
|
||||||
|
val payDetailRes =
|
||||||
|
tradeData.payDetail
|
||||||
|
|
||||||
|
if (
|
||||||
|
TextUtils.equals(
|
||||||
|
payDetailRes.tradeAnswerCode,
|
||||||
|
RC_APPROVED_V1
|
||||||
|
) ||
|
||||||
|
TextUtils.equals(
|
||||||
|
payDetailRes.tradeAnswerCode,
|
||||||
|
RC_APPROVED_V2
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
|
||||||
|
payDetailRes.invoiceNo =
|
||||||
|
SystemParamsOperation
|
||||||
|
.getInstance()
|
||||||
|
.incrementInvoiceNum
|
||||||
|
|
||||||
|
when (transType.value) {
|
||||||
|
|
||||||
|
TransactionsType.VOID -> {
|
||||||
|
processVoidDB(payDetailRes)
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionsType.REFUND -> {
|
||||||
|
processRefundDB(payDetailRes)
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionsType.PRE_AUTH_VOID -> {
|
||||||
|
processPreVoidDb(payDetailRes)
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionsType.PRE_AUTH_COMPLETE -> {
|
||||||
|
processPreCompDb(payDetailRes)
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionsType.PRE_AUTH_COMPLETE_VOID -> {
|
||||||
|
processPreCompVoidDb(payDetailRes)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
insertDB(payDetailRes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_payDetailResult.value =
|
||||||
|
payDetailRes
|
||||||
|
|
||||||
|
_transResultStatus.value =
|
||||||
|
TransResultStatus.SUCCESS
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
_payDetailResult.value =
|
||||||
|
payDetailRes
|
||||||
|
|
||||||
|
_transResultStatus.value =
|
||||||
|
TransResultStatus.FAIL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReversal(
|
||||||
|
tradeData: TradeData
|
||||||
|
) {
|
||||||
|
|
||||||
|
_transResultStatus.value =
|
||||||
|
TransResultStatus.REVERSAL_PREPARE
|
||||||
|
|
||||||
|
_state.value = _state.value.copy("Preparing Reversal")
|
||||||
|
callReversal(tradeData)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(message: String) {
|
||||||
|
|
||||||
|
if (message == TRY_SECONDARY) {
|
||||||
|
|
||||||
|
_transResultStatus.value =
|
||||||
|
TransResultStatus.SECONDARY
|
||||||
|
|
||||||
|
startOnlineProcess()
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
_errorMessage.value = message
|
||||||
|
|
||||||
|
_transResultStatus.value =
|
||||||
|
TransResultStatus.ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Database
|
||||||
|
*/
|
||||||
|
|
||||||
|
override fun insertDB(payResult: PayDetail) {
|
||||||
|
|
||||||
|
payDetail?.pinCipher = ""
|
||||||
|
|
||||||
|
repository.insertPayDetail(
|
||||||
|
payDetail
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun processVoidDB(payResult: PayDetail) {
|
||||||
|
|
||||||
|
payDetail?.isCanceled = true
|
||||||
|
|
||||||
|
payDetail?.let {
|
||||||
|
repository.updatePayDetail(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
repository.insertPayDetail(
|
||||||
|
updateCurrentDateAndTime(payResult)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun processRefundDB(payResult: PayDetail) {
|
||||||
|
|
||||||
|
oldTransPayDetail?.apply {
|
||||||
|
isReturnGood = true
|
||||||
|
isCanceled = true
|
||||||
|
|
||||||
|
repository.updatePayDetail(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
repository.insertPayDetail(
|
||||||
|
updateCurrentDateAndTime(payResult)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun processPreVoidDb(payResult: PayDetail) {
|
||||||
|
oldTransPayDetail?.apply {
|
||||||
|
isCanceled = true
|
||||||
|
repository.updatePayDetail(oldTransPayDetail!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
repository.insertPayDetail(updateCurrentDateAndTime(payResult))
|
||||||
|
}
|
||||||
|
override fun processPreCompDb(payResult: PayDetail) {
|
||||||
|
if (oldTransPayDetail?.getAmount() == payResult.getAmount()) {
|
||||||
|
oldTransPayDetail?.apply {
|
||||||
|
isCanceled = true
|
||||||
|
repository.updatePayDetail(this)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
oldTransPayDetail?.apply {
|
||||||
|
isCanceled = false
|
||||||
|
repository.updatePayDetail(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
repository.insertPayDetail(updateCurrentDateAndTime(payResult))
|
||||||
|
}
|
||||||
|
override fun processPreCompVoidDb(payResult: PayDetail) {
|
||||||
|
payDetail?.apply {
|
||||||
|
isCanceled = true
|
||||||
|
repository.updatePayDetail(this)
|
||||||
|
}
|
||||||
|
repository.insertPayDetail(updateCurrentDateAndTime(payResult))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Reversal
|
||||||
|
*/
|
||||||
|
|
||||||
|
private fun callReversal(tradeData: TradeData) {
|
||||||
|
|
||||||
|
_state.value = _state.value.copy("Processing Reversal")
|
||||||
|
payDetail = tradeData.payDetail
|
||||||
|
|
||||||
|
ReversalAction.getInstance()
|
||||||
|
.setData(tradeData)
|
||||||
|
.enqueue()
|
||||||
|
.startReversal(object : ReversalListener {
|
||||||
|
|
||||||
|
override fun onSuccessReversal() {
|
||||||
|
|
||||||
|
_transResultStatus.value =
|
||||||
|
TransResultStatus.REVERSAL_SUCCESS
|
||||||
|
|
||||||
|
payDetail?.let {
|
||||||
|
repository.insertPayDetail(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNetworkFail(msg: String) {
|
||||||
|
|
||||||
|
SystemParamsOperation
|
||||||
|
.getInstance()
|
||||||
|
.setSecondHostEnable(true)
|
||||||
|
|
||||||
|
if (
|
||||||
|
SystemParamsOperation
|
||||||
|
.getInstance()
|
||||||
|
.isSecondHostEnabled
|
||||||
|
) {
|
||||||
|
|
||||||
|
if (!isSecondCall) {
|
||||||
|
|
||||||
|
_transResultStatus.value =
|
||||||
|
TransResultStatus.REVERSAL_SECONDARY
|
||||||
|
|
||||||
|
ISOSocket.getInstance()
|
||||||
|
.switchIp()
|
||||||
|
|
||||||
|
isSecondCall = true
|
||||||
|
|
||||||
|
callReversal(tradeData)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
_transResultStatus.value =
|
||||||
|
TransResultStatus.REVERSAL_FAIL
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
_transResultStatus.value =
|
||||||
|
TransResultStatus.REVERSAL_FAIL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailReversal(msg: String) {
|
||||||
|
|
||||||
|
_transResultStatus.value =
|
||||||
|
TransResultStatus.REVERSAL_FAIL
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Utils
|
||||||
|
*/
|
||||||
|
|
||||||
|
private fun updateCurrentDateAndTime(
|
||||||
|
payDetail: PayDetail
|
||||||
|
): PayDetail {
|
||||||
|
|
||||||
|
payDetail.tradeDate =
|
||||||
|
SystemDateTime.getMMDD()
|
||||||
|
|
||||||
|
payDetail.tradeTime =
|
||||||
|
SystemDateTime.getHHmmss()
|
||||||
|
|
||||||
|
return payDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resetTransactionStatus() {
|
||||||
|
_transResultStatus.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTransStatus() = TODO()
|
||||||
|
}
|
||||||
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>
|
||||||
12
app/src/main/res/drawable/ic_circle_download_arrow.xml
Normal file
12
app/src/main/res/drawable/ic_circle_download_arrow.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="800dp"
|
||||||
|
android:height="800dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<path
|
||||||
|
android:pathData="M512.8,1002.4c-65.6,0 -129.6,-12.8 -189.6,-38.4 -58.4,-24.8 -110.4,-60 -155.2,-104.8S88,761.6 64,703.2c-25.6,-60 -38.4,-124 -38.4,-190.4 0,-65.6 12.8,-130.4 38.4,-190.4 24,-57.6 59.2,-110.4 104,-155.2s96.8,-80 155.2,-104.8C383.2,36.8 447.2,24 512.8,24s129.6,12.8 189.6,38.4c58.4,24.8 110.4,60 155.2,104.8 44.8,44.8 80,96.8 104.8,155.2 25.6,60 38.4,124 38.4,190.4 0,65.6 -12.8,130.4 -38.4,190.4 -24.8,58.4 -60,110.4 -104.8,155.2 -44.8,44.8 -96.8,80 -155.2,104.8 -60,25.6 -123.2,39.2 -189.6,39.2zM512.8,71.2c-242.4,0 -440,198.4 -440,441.6s197.6,441.6 440.8,441.6c243.2,0 440.8,-198.4 440.8,-441.6S756,71.2 512.8,71.2z"
|
||||||
|
android:fillColor="#000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M512,811.2c-5.6,0 -12,-2.4 -16,-6.4 -1.6,-0.8 -2.4,-1.6 -4,-3.2L280,589.6c-9.6,-9.6 -9.6,-24 0,-33.6 4.8,-4.8 10.4,-7.2 16.8,-7.2s12,2.4 16.8,7.2L488,731.2V236c0,-12.8 10.4,-24 24,-24 12.8,0 24,10.4 24,24v495.2l175.2,-175.2c4.8,-4.8 10.4,-7.2 16.8,-7.2s12,2.4 16.8,7.2c9.6,9.6 9.6,24 0,33.6l-212,212c-1.6,1.6 -2.4,2.4 -4,3.2 -5.6,4 -11.2,6.4 -16.8,6.4z"
|
||||||
|
android:fillColor="#000000"/>
|
||||||
|
</vector>
|
||||||
37
app/src/main/res/drawable/ic_circle_wrong.xml
Normal file
37
app/src/main/res/drawable/ic_circle_wrong.xml
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<!--
|
||||||
|
~ Copyright (C) 2026 The Android Open Source Project
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="14dp"
|
||||||
|
android:height="14dp"
|
||||||
|
android:viewportWidth="14"
|
||||||
|
android:viewportHeight="14">
|
||||||
|
<path
|
||||||
|
android:pathData="M9.078,4.669L4.669,9.078M4.669,4.669L9.078,9.078"
|
||||||
|
android:strokeAlpha="0.8"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#6F0D1E"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M6.873,12.997C10.255,12.997 12.997,10.255 12.997,6.873C12.997,3.491 10.255,0.75 6.873,0.75C3.491,0.75 0.75,3.491 0.75,6.873C0.75,10.255 3.491,12.997 6.873,12.997Z"
|
||||||
|
android:strokeAlpha="0.8"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#6F0D1E"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/ic_clear.xml
Normal file
9
app/src/main/res/drawable/ic_clear.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="128dp"
|
||||||
|
android:height="128dp"
|
||||||
|
android:viewportWidth="128"
|
||||||
|
android:viewportHeight="128">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="m15.3,13.52c-4.61,2.66 -6.21,8.52 -3.54,13.14L32.21,62.09 15.73,71.6c-4.61,2.66 -6.18,8.51 -3.52,13.12l5.59,9.68 8.03,-4.64c3.18,4.61 7.29,10.53 11,15.48 2.79,3.72 3.86,6.62 7.09,8.89 1.62,1.13 3.79,1.7 5.91,1.63 2.11,-0.07 4.28,-0.6 7.03,-1.47a3.88,3.88 0,0 0,0.44 -0.15L104.38,92.85c2.94,-1.33 5.3,-2.31 7.09,-3.05 1.79,-0.74 2.7,-0.95 4.13,-1.93 0.36,-0.25 0.86,-0.47 1.46,-1.65 0.3,-0.59 0.6,-1.57 0.4,-2.58 -0.19,-1.01 -0.81,-1.8 -1.29,-2.23 -0.95,-0.86 -1.46,-0.9 -1.88,-1.01 -0.42,-0.11 -0.73,-0.15 -1.06,-0.19 -1.32,-0.17 -2.92,-0.2 -5.33,-0.32L95.76,79.25C92.9,79.1 88.82,75.69 86.64,72.48L77.95,59.68 83.07,56.73 77.48,47.04c-2.66,-4.61 -8.53,-6.16 -13.15,-3.5l-15.46,8.92 -20.45,-35.43c-2.66,-4.61 -8.51,-6.18 -13.12,-3.52zM32.55,85.89 L71.21,63.58 80.23,76.84c2.98,4.38 7.96,9.79 15.14,10.16l2.78,0.15 -43.6,19.75c-0.03,0.01 -0.04,-0.01 -0.07,0 -2.38,0.74 -4.04,1.09 -4.91,1.12 -0.89,0.03 -0.87,-0.04 -1.15,-0.24 -0.58,-0.4 -2.22,-2.97 -5.36,-7.16C39.67,96.11 35.71,90.46 32.55,85.89z"/>
|
||||||
|
</vector>
|
||||||
20
app/src/main/res/drawable/ic_code.xml
Normal file
20
app/src/main/res/drawable/ic_code.xml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<!--
|
||||||
|
~ Copyright (C) 2026 The Android Open Source Project
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="16" android:viewportWidth="16" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M4.573,5.227L1.96,7.84L4.573,10.453M11.107,5.227L13.72,7.84L11.107,10.453M9.147,2.613L6.533,13.067" android:strokeColor="#6F0D1E" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
52
app/src/main/res/drawable/ic_database_config.xml
Normal file
52
app/src/main/res/drawable/ic_database_config.xml
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="800dp"
|
||||||
|
android:height="800dp"
|
||||||
|
android:viewportWidth="48"
|
||||||
|
android:viewportHeight="48">
|
||||||
|
<path
|
||||||
|
android:pathData="M0,0h48v48h-48z"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:fillAlpha="0.01"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M34,12V20V21C31.045,21 28.389,22.282 26.559,24.32C24.968,26.091 24,28.432 24,31C24,31.579 24.049,32.146 24.144,32.698C24.658,35.705 26.514,38.253 29.074,39.705C26.412,40.51 22.878,41 19,41C10.716,41 4,38.761 4,36V28V20V12"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="4"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M34,12C34,14.761 27.284,17 19,17C10.716,17 4,14.761 4,12C4,9.239 10.716,7 19,7C27.284,7 34,9.239 34,12Z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="4"
|
||||||
|
android:fillColor="#CD2029"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M4,28C4,30.761 10.716,33 19,33C20.807,33 22.539,32.894 24.144,32.698"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="4"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M4,20C4,22.761 10.716,25 19,25C21.756,25 24.339,24.752 26.559,24.32"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="4"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M44,31C44,36.523 39.523,41 34,41C32.209,41 30.528,40.529 29.074,39.705C26.514,38.253 24.658,35.705 24.144,32.698C24.049,32.146 24,31.579 24,31C24,28.432 24.968,26.091 26.559,24.32C28.389,22.282 31.045,21 34,21C39.523,21 44,25.477 44,31Z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="4"
|
||||||
|
android:fillColor="#CD2029"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M34,27V31M37.464,29L34,31M37.464,33L34,31M34,35V31M30.536,33L34,31M30.536,29L34,31"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="4"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#ffffff"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
||||||
28
app/src/main/res/drawable/ic_device_info.xml
Normal file
28
app/src/main/res/drawable/ic_device_info.xml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="800dp"
|
||||||
|
android:height="800dp"
|
||||||
|
android:viewportWidth="192"
|
||||||
|
android:viewportHeight="192">
|
||||||
|
<path
|
||||||
|
android:pathData="M96.01,63.63c-3.32,0 -6.12,2.71 -6.12,6.1a6.12,6.12 0,0 0,6.12 6.12c3.38,0 6.1,-2.8 6.1,-6.12s-2.78,-6.1 -6.1,-6.1z"
|
||||||
|
android:strokeAlpha="0.98"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="3.78002"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:fillAlpha="0.98"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M83.43,122.37h25.13M87.48,90.96h8.54v31.41"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="12"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M54,46h84M60,22h72c5.54,0 10,4.46 10,10v128c0,5.54 -4.46,10 -10,10L60,170c-5.54,0 -10,-4.46 -10,-10L50,32c0,-5.54 4.46,-10 10,-10zM54,146h84"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="12"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"/>
|
||||||
|
</vector>
|
||||||
20
app/src/main/res/drawable/ic_four_vertical_bars.xml
Normal file
20
app/src/main/res/drawable/ic_four_vertical_bars.xml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<!--
|
||||||
|
~ Copyright (C) 2026 The Android Open Source Project
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="12" android:viewportWidth="12" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="#6F0D1E" android:fillType="evenOdd" android:pathData="M11.025,-0C11.22,-0 11.407,0.077 11.545,0.215C11.683,0.353 11.76,0.54 11.76,0.735L11.76,11.025C11.76,11.22 11.683,11.407 11.545,11.545C11.407,11.683 11.22,11.76 11.025,11.76C10.83,11.76 10.643,11.683 10.505,11.545C10.367,11.407 10.29,11.22 10.29,11.025L10.29,0.735C10.29,0.54 10.367,0.353 10.505,0.215C10.643,0.077 10.83,-0 11.025,-0ZM0.735,-0C0.93,-0 1.117,0.077 1.255,0.215C1.393,0.353 1.47,0.54 1.47,0.735L1.47,11.025C1.47,11.22 1.393,11.407 1.255,11.545C1.117,11.683 0.93,11.76 0.735,11.76C0.54,11.76 0.353,11.683 0.215,11.545C0.077,11.407 -0,11.22 -0,11.025L-0,0.735C-0,0.54 0.077,0.353 0.215,0.215C0.353,0.077 0.54,-0 0.735,-0ZM7.595,-0C7.79,-0 7.977,0.077 8.115,0.215C8.253,0.353 8.33,0.54 8.33,0.735L8.33,11.025C8.33,11.22 8.253,11.407 8.115,11.545C7.977,11.683 7.79,11.76 7.595,11.76C7.4,11.76 7.213,11.683 7.075,11.545C6.937,11.407 6.86,11.22 6.86,11.025L6.86,0.735C6.86,0.54 6.937,0.353 7.075,0.215C7.213,0.077 7.4,-0 7.595,-0ZM4.165,-0C4.36,-0 4.547,0.077 4.685,0.215C4.823,0.353 4.9,0.54 4.9,0.735L4.9,11.025C4.9,11.22 4.823,11.407 4.685,11.545C4.547,11.683 4.36,11.76 4.165,11.76C3.97,11.76 3.783,11.683 3.645,11.545C3.507,11.407 3.43,11.22 3.43,11.025L3.43,0.735C3.43,0.54 3.507,0.353 3.645,0.215C3.783,0.077 3.97,-0 4.165,-0Z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
25
app/src/main/res/drawable/ic_function.xml
Normal file
25
app/src/main/res/drawable/ic_function.xml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<!--
|
||||||
|
~ Copyright (C) 2026 The Android Open Source Project
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="12dp"
|
||||||
|
android:height="12dp"
|
||||||
|
android:viewportWidth="12"
|
||||||
|
android:viewportHeight="12">
|
||||||
|
<path
|
||||||
|
android:pathData="M0,0.653C0,0.48 0.069,0.314 0.191,0.191C0.314,0.069 0.48,0 0.653,0H4.573C4.747,0 4.913,0.069 5.035,0.191C5.158,0.314 5.227,0.48 5.227,0.653V4.573C5.227,4.747 5.158,4.913 5.035,5.035C4.913,5.158 4.747,5.227 4.573,5.227H0.653C0.48,5.227 0.314,5.158 0.191,5.035C0.069,4.913 0,4.747 0,4.573V0.653ZM0,7.187C0,7.013 0.069,6.847 0.191,6.725C0.314,6.602 0.48,6.533 0.653,6.533H4.573C4.747,6.533 4.913,6.602 5.035,6.725C5.158,6.847 5.227,7.013 5.227,7.187V11.107C5.227,11.28 5.158,11.446 5.035,11.569C4.913,11.691 4.747,11.76 4.573,11.76H0.653C0.48,11.76 0.314,11.691 0.191,11.569C0.069,11.446 0,11.28 0,11.107V7.187ZM6.533,0.653C6.533,0.48 6.602,0.314 6.725,0.191C6.847,0.069 7.013,0 7.187,0H11.107C11.28,0 11.446,0.069 11.569,0.191C11.691,0.314 11.76,0.48 11.76,0.653V4.573C11.76,4.747 11.691,4.913 11.569,5.035C11.446,5.158 11.28,5.227 11.107,5.227H7.187C7.013,5.227 6.847,5.158 6.725,5.035C6.602,4.913 6.533,4.747 6.533,4.573V0.653ZM6.533,7.187C6.533,7.013 6.602,6.847 6.725,6.725C6.847,6.602 7.013,6.533 7.187,6.533H11.107C11.28,6.533 11.446,6.602 11.569,6.725C11.691,6.847 11.76,7.013 11.76,7.187V11.107C11.76,11.28 11.691,11.446 11.569,11.569C11.446,11.691 11.28,11.76 11.107,11.76H7.187C7.013,11.76 6.847,11.691 6.725,11.569C6.602,11.446 6.533,11.28 6.533,11.107V7.187Z"
|
||||||
|
android:fillColor="#6F0D1E"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
</vector>
|
||||||
20
app/src/main/res/drawable/ic_gear.xml
Normal file
20
app/src/main/res/drawable/ic_gear.xml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<!--
|
||||||
|
~ Copyright (C) 2026 The Android Open Source Project
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="16" android:viewportWidth="16" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="#6F0D1E" android:pathData="M15.595,7.53C15.642,7.538 15.68,7.585 15.68,7.632V9.614C15.679,9.638 15.67,9.661 15.655,9.68C15.64,9.698 15.619,9.711 15.595,9.716L13.717,10.066C13.691,10.072 13.668,10.084 13.649,10.101C13.629,10.118 13.615,10.14 13.606,10.165L13.019,11.629C13.009,11.653 13.004,11.679 13.006,11.705C13.008,11.731 13.016,11.757 13.03,11.779L14.097,13.333C14.11,13.354 14.115,13.378 14.113,13.402C14.11,13.426 14.1,13.449 14.084,13.467L12.683,14.867C12.665,14.883 12.643,14.893 12.619,14.896C12.595,14.898 12.571,14.892 12.55,14.88L11.023,13.832C11.001,13.818 10.976,13.811 10.95,13.81C10.925,13.809 10.899,13.815 10.877,13.826L10.204,14.186C10.194,14.191 10.183,14.194 10.172,14.195C10.161,14.196 10.149,14.195 10.139,14.191C10.128,14.187 10.119,14.181 10.111,14.173C10.103,14.165 10.097,14.156 10.093,14.145L8.704,10.79C8.696,10.768 8.695,10.743 8.702,10.72C8.71,10.698 8.725,10.678 8.745,10.665L8.913,10.562C8.945,10.542 8.987,10.509 9.02,10.48C9.419,10.225 9.725,9.847 9.892,9.403C10.058,8.96 10.076,8.474 9.943,8.019C9.81,7.564 9.534,7.165 9.154,6.88C8.775,6.596 8.314,6.442 7.84,6.442C7.366,6.442 6.905,6.596 6.526,6.88C6.146,7.165 5.87,7.564 5.737,8.019C5.604,8.474 5.622,8.96 5.788,9.403C5.955,9.847 6.261,10.225 6.66,10.48C6.693,10.51 6.735,10.543 6.767,10.562L6.935,10.665C6.955,10.678 6.97,10.698 6.978,10.72C6.985,10.743 6.984,10.768 6.976,10.79L5.587,14.145C5.583,14.155 5.577,14.165 5.569,14.173C5.561,14.181 5.552,14.187 5.541,14.191C5.531,14.194 5.519,14.196 5.508,14.195C5.497,14.194 5.486,14.191 5.476,14.186L4.803,13.826C4.781,13.815 4.755,13.809 4.73,13.81C4.704,13.811 4.679,13.818 4.657,13.831L3.129,14.88C3.109,14.892 3.085,14.898 3.061,14.896C3.037,14.894 3.014,14.884 2.996,14.867L1.595,13.467C1.579,13.449 1.568,13.426 1.566,13.402C1.564,13.378 1.57,13.354 1.582,13.333L2.649,11.778C2.663,11.756 2.671,11.731 2.673,11.705C2.675,11.679 2.671,11.653 2.66,11.629L2.074,10.165C2.065,10.14 2.05,10.119 2.031,10.102C2.012,10.085 1.988,10.073 1.963,10.066L0.085,9.716C0.061,9.71 0.04,9.698 0.025,9.679C0.009,9.66 0.001,9.637 0,9.613L0,7.632C0,7.585 0.039,7.538 0.085,7.53L2.01,7.172C2.036,7.166 2.06,7.154 2.079,7.137C2.099,7.12 2.115,7.098 2.125,7.074L2.715,5.696C2.725,5.673 2.729,5.647 2.728,5.621C2.726,5.595 2.719,5.57 2.705,5.548L1.584,3.913C1.571,3.892 1.566,3.868 1.568,3.844C1.57,3.82 1.58,3.798 1.596,3.78L2.997,2.378C3.014,2.361 3.037,2.351 3.061,2.349C3.086,2.347 3.11,2.353 3.13,2.366L4.794,3.508C4.833,3.535 4.9,3.54 4.943,3.519L6.278,2.973C6.324,2.958 6.367,2.907 6.376,2.86L6.746,0.869C6.752,0.845 6.765,0.824 6.783,0.809C6.802,0.794 6.825,0.785 6.849,0.783H8.831C8.879,0.783 8.925,0.822 8.934,0.868L9.304,2.86C9.31,2.885 9.322,2.908 9.339,2.928C9.356,2.948 9.378,2.963 9.402,2.973L10.737,3.519C10.761,3.529 10.787,3.534 10.813,3.532C10.839,3.53 10.865,3.522 10.887,3.508L12.55,2.366C12.571,2.353 12.595,2.348 12.619,2.35C12.643,2.352 12.666,2.363 12.683,2.379L14.084,3.78C14.118,3.813 14.123,3.874 14.097,3.913L12.974,5.548C12.961,5.57 12.954,5.595 12.952,5.621C12.95,5.647 12.955,5.673 12.965,5.696L13.555,7.074C13.566,7.098 13.581,7.119 13.601,7.136C13.621,7.153 13.644,7.165 13.67,7.172L15.595,7.53Z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
24
app/src/main/res/drawable/ic_history.xml
Normal file
24
app/src/main/res/drawable/ic_history.xml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<!--
|
||||||
|
~ Copyright (C) 2026 The Android Open Source Project
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="30dp"
|
||||||
|
android:height="30dp"
|
||||||
|
android:viewportWidth="30"
|
||||||
|
android:viewportHeight="30">
|
||||||
|
<path
|
||||||
|
android:pathData="M14.625,29.25C10.887,29.25 7.631,28.011 4.856,25.534C2.08,23.056 0.489,19.962 0.081,16.25H3.412C3.792,19.067 5.045,21.396 7.171,23.237C9.298,25.079 11.782,26 14.625,26C17.794,26 20.482,24.897 22.69,22.69C24.898,20.483 26.001,17.795 26,14.625C25.999,11.455 24.896,8.767 22.69,6.562C20.484,4.356 17.796,3.252 14.625,3.25C12.756,3.25 11.009,3.683 9.384,4.55C7.759,5.417 6.392,6.608 5.281,8.125H9.75V11.375H0V1.625H3.25V5.444C4.631,3.71 6.317,2.37 8.309,1.422C10.3,0.474 12.405,0 14.625,0C16.656,0 18.559,0.386 20.334,1.159C22.108,1.931 23.652,2.973 24.965,4.285C26.278,5.597 27.321,7.141 28.093,8.916C28.865,10.692 29.251,12.595 29.25,14.625C29.249,16.655 28.863,18.558 28.093,20.334C27.323,22.109 26.28,23.653 24.965,24.965C23.65,26.277 22.106,27.319 20.334,28.093C18.561,28.867 16.658,29.252 14.625,29.25ZM19.175,21.45L13,15.275V6.5H16.25V13.975L21.45,19.175L19.175,21.45Z"
|
||||||
|
android:fillColor="#6F0D1E"/>
|
||||||
|
</vector>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user