Compare commits

..

No commits in common. "emv_latest" and "main" have entirely different histories.

311 changed files with 5145 additions and 20204 deletions

3
.gitignore vendored
View File

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

3
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

View File

@ -4,10 +4,10 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-09-15T06:58:26.019801800Z"> <DropdownSelection timestamp="2026-05-07T16:25:07.233027800Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=PB13216J53652" /> <DeviceId pluginId="PhysicalDevice" identifier="serial=PB0219AQ60954" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>

View File

@ -0,0 +1,61 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
</profile>
</component>

View File

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?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" project-jdk-name="jbr-21" project-jdk-type="JavaSDK" /> <component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project> </project>

View File

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

View File

@ -1,8 +1,6 @@
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 {
@ -14,9 +12,8 @@ android {
} }
defaultConfig { defaultConfig {
applicationId = "com.mob.utsmyanmar" applicationId = "com.mob.ustmm"
minSdk = 23 minSdk = 24
//noinspection OldTargetApi
targetSdk = 36 targetSdk = 36
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
@ -24,33 +21,6 @@ android {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
flavorDimensions += "env"
productFlavors {
create("sit"){
dimension = "env"
applicationIdSuffix = ".sit"
versionNameSuffix = "-SIT"
versionCode = 1
versionName = "1.0"
buildConfigField("String", "ENV", "\"SIT\"")
resValue("string", "app_name", "MOB-SIT")
}
create("uat"){
dimension = "env"
applicationIdSuffix = ".uat"
versionNameSuffix = "-UAT"
versionCode = 19
versionName = "3.7"
buildConfigField("String", "ENV", "\"UAT\"")
resValue("string", "app_name", "MOB-UAT")
}
create("production"){
dimension = "env"
buildConfigField("String", "ENV", "\"PROD\"")
}
}
buildTypes { buildTypes {
release { release {
isMinifyEnabled = false isMinifyEnabled = false
@ -59,14 +29,6 @@ android {
"proguard-rules.pro" "proguard-rules.pro"
) )
} }
debug {
isMinifyEnabled = false
isDebuggable = true
// proguardFiles(
// getDefaultProguardFile("proguard-android-optimize.txt"),
// "proguard-rules.pro"
// )
}
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
@ -74,8 +36,6 @@ android {
} }
buildFeatures { buildFeatures {
compose = true compose = true
buildConfig = true
resValues = true
} }
} }
@ -84,13 +44,10 @@ 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)
@ -100,31 +57,4 @@ 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"))
implementation("se.warting.signature:signature-pad:0.2.0")
}

View File

@ -1,36 +0,0 @@
## Change Logs For UAT
### version 3.3
* config auto update after transaction
* signature removed on void
* "Enter Password" color to LegacyRed
* Amount and Card Number show in signature pad
* fixed recent transaction only working for top 3
* routed to dashboard after printing receipt
* receipt printing animation re-scale
### version 3.4
* enabled screen sleep
* auto-settlement
* show expiry date as "`**`/`**`"
* align the customer name, in paper slip
* settlement fixed in both UI and system
* amount, date, time fixed in Void Preview
* show reversal status in trans records
* error_slip_print enable/disable in tms
* auto_settlement enable/disable in tms
### version 3.5
* "always with you" in mob receipt logo
* "..." full width in both P2 and P3 preview screens
* JCB logo added in Card Capture Screen
* "-" in void, refund and cash adv amounts
* disable sale button if amount is 0 or 0.00
* changed date formats to dd/mm/yy to dd/mm/yyyy
* changed keyboard design in reprint any transactions
### version 3.6
* qr for user manual
### version 3.7
* auto-settlement for both P2 and P3

View File

@ -2,40 +2,8 @@
<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.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<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:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
@ -43,22 +11,17 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.MOBPOS"> android:theme="@style/Theme.MOBPOS">
<receiver android:name=".auto_settlement.AlarmReceiver" android:exported="false"/>
<receiver android:name=".auto_settlement.BootReceiver" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<service android:name=".auto_settlement.AutoSettleService" android:exported="false" android:foregroundServiceType="dataSync" />
<activity <activity
android:name="com.mob.utsmyanmar.MainActivity" android:name=".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>
</application> </application>
</manifest> </manifest>

View File

@ -1,43 +0,0 @@
## Working agreements
- use Scaffold and AppBar() in every main Screen
- use MVVM design pattern
- separate UiState and ViewModel for screen for better performance and smooth
### Re-usable Screens
- PasswordInput.kt
- InputAmountScreen.kt
-
### Ponytail, lazy senior dev mode
You are a lazy senior developer. Lazy means efficient, not careless. The best code is the code never written.
Before writing any code, stop at the first rung that holds:
Does this need to be built at all? (YAGNI)
Does it already exist in this codebase? Reuse the helper, util, or pattern that's already here, don't re-write it.
Does the standard library already do this? Use it.
Does a native platform feature cover it? Use it.
Does an already-installed dependency solve it? Use it.
Can this be one line? Make it one line.
Only then: write the minimum code that works.
The ladder runs after you understand the problem, not instead of it: read the task and the code it touches, trace the real flow end to end, then climb.
Bug fix = root cause, not symptom: a report names a symptom. Grep every caller of the function you touch and fix the shared function once — one guard there is a smaller diff than one per caller, and patching only the path the ticket names leaves a sibling caller still broken.
Rules:
No abstractions that weren't explicitly requested.
No new dependency if it can be avoided.
No boilerplate nobody asked for.
Deletion over addition. Boring over clever. Fewest files possible.
Shortest working diff wins, but only once you understand the problem. The smallest change in the wrong place isn't lazy, it's a second bug.
Question complex requests: "Do you actually need X, or does Y cover it?"
Pick the edge-case-correct option when two stdlib approaches are the same size, lazy means less code, not the flimsier algorithm.
Mark intentional simplifications with a ponytail: comment. If the shortcut has a known ceiling (global lock, O(n²) scan, naive heuristic), the comment names the ceiling and the upgrade path.
Not lazy about: understanding the problem (read it fully and trace the real flow before picking a rung, a small diff you don't understand is just laziness dressed up as efficiency), input validation at trust boundaries, error handling that prevents data loss, security, accessibility, the calibration real hardware needs (the platform is never the spec ideal, a clock drifts, a sensor reads off), anything explicitly requested. Lazy code without its check is unfinished: non-trivial logic leaves ONE runnable check behind, the smallest thing that fails if the logic breaks (an assert-based demo/self-check or one small test file; no frameworks, no fixtures). Trivial one-liners need no test.
(Yes, this file also applies to agents working on the ponytail repo itself. Especially to them.)
### Don't Do This
- don't run ./grdlew

View File

@ -1,122 +1,22 @@
package com.mob.utsmyanmar package com.mob.utsmyanmar
import android.Manifest
import android.app.AlarmManager
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.mob.utsmyanmar.auto_settlement.SettlementScheduler
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 com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private var autoSettlementRequested by mutableStateOf(false)
private var exactAlarmSettingsOpened = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
autoSettlementRequested = intent?.getBooleanExtra("AUTO_SETTLEMENT", false) == true
requestNotificationPermission()
scheduleSettlementAlarm()
enableEdgeToEdge() enableEdgeToEdge()
hideSystemBars()
// window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
setContent { setContent {
MOBPOSTheme { MOBPOSTheme {
val navController = rememberNavController() val navController = rememberNavController()
AppNavGraph( AppNavGraph(navController = navController)
navController = navController,
autoSettlementRequested = autoSettlementRequested,
onAutoSettlementHandled = { autoSettlementRequested = false }
)
} }
} }
} }
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
if (intent.getBooleanExtra("AUTO_SETTLEMENT", false)) {
autoSettlementRequested = true
}
}
override fun onResume() {
super.onResume()
scheduleSettlementAlarm()
}
private fun requestNotificationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
) {
requestPermissions(
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
NOTIFICATION_PERMISSION_REQUEST_CODE
)
}
}
private fun scheduleSettlementAlarm() {
if (!SystemParamsOperation.getInstance().isAutoSettlement()) {
SettlementScheduler.cancel(this)
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val alarmManager = getSystemService(AlarmManager::class.java)
if (!alarmManager.canScheduleExactAlarms()) {
if (!exactAlarmSettingsOpened) {
exactAlarmSettingsOpened = true
startActivity(
Intent(
Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM,
Uri.parse("package:$packageName")
)
)
}
SettlementScheduler.scheduleFallback(this)
return
}
}
exactAlarmSettingsOpened = false
SettlementScheduler.schedule(this)
}
private companion object {
const val NOTIFICATION_PERMISSION_REQUEST_CODE = 1003
}
private fun hideSystemBars() {
val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
windowInsetsController.hide(WindowInsetsCompat.Type.navigationBars())
windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
windowInsetsController.hide(WindowInsetsCompat.Type.navigationBars())
windowInsetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
hideSystemBars()
}
}
} }

View File

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

View File

@ -1,36 +0,0 @@
package com.mob.utsmyanmar.auto_settlement
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import dagger.hilt.android.AndroidEntryPoint
import sunmi.sunmiui.utils.LogUtil
@AndroidEntryPoint
class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val appContext = context ?: return
// Exact alarms are one-shot, so keep the next daily alarm alive before
// starting the foreground service.
if (!SettlementScheduler.schedule(appContext)) {
LogUtil.e(AlarmReceiver::class.java.simpleName, "Unable to reschedule settlement alarm")
}
val systemParams = SystemParamsOperation.getInstance()
if (!systemParams.isAutoSettlement() || !systemParams.settlementStatus) {
LogUtil.d(AlarmReceiver::class.java.simpleName, "Auto settlement is disabled")
return
}
val serviceIntent = Intent(appContext, AutoSettleService::class.java)
LogUtil.d(AlarmReceiver::class.java.simpleName, "Alert service")
ContextCompat.startForegroundService(appContext, serviceIntent)
LogUtil.d(AlarmReceiver::class.java.simpleName, "Foreground service")
}
}

View File

@ -1,180 +0,0 @@
package com.mob.utsmyanmar.auto_settlement
import android.annotation.SuppressLint
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.media.AudioAttributes
import android.media.Ringtone
import android.media.RingtoneManager
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import androidx.core.app.NotificationCompat
import com.mob.utsmyanmar.MainActivity
import com.mob.utsmyanmar.R
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import dagger.hilt.android.AndroidEntryPoint
import sunmi.sunmiui.utils.LogUtil
@AndroidEntryPoint
class AutoSettleService : Service() {
companion object {
private const val FOREGROUND_CHANNEL_ID = "auto_settlement_foreground_channel"
private const val ALERT_CHANNEL_ID = "auto_settlement_alert_channel"
private const val NOTIFICATION_ID = 1001
private const val ALERT_NOTIFICATION_ID = 1002
private const val ALERT_SOUND_DURATION_MS = 3_000L
}
private val mainHandler = Handler(Looper.getMainLooper())
private var alertRingtone: Ringtone? = null
private val stopServiceRunnable = Runnable {
alertRingtone?.stop()
alertRingtone = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE)
} else {
stopForeground(true)
}
stopSelf()
}
override fun onCreate() {
super.onCreate()
LogUtil.d(AutoSettleService::class.java.simpleName, "auto settle service called")
startForegroundNotification()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val systemParams = SystemParamsOperation.getInstance()
if (!systemParams.isAutoSettlement() || !systemParams.settlementStatus) {
stopSelf()
return START_NOT_STICKY
}
showSettlementAlert()
playAlertSound()
launchSettlement()
mainHandler.removeCallbacks(stopServiceRunnable)
mainHandler.postDelayed(stopServiceRunnable, ALERT_SOUND_DURATION_MS)
return START_NOT_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() {
mainHandler.removeCallbacks(stopServiceRunnable)
alertRingtone?.stop()
alertRingtone = null
super.onDestroy()
}
@SuppressLint("ForegroundServiceType")
private fun startForegroundNotification() {
LogUtil.d(AutoSettleService::class.java.simpleName, "foreground noti called")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
FOREGROUND_CHANNEL_ID,
"AUTO SETTLEMENT",
NotificationManager.IMPORTANCE_LOW
)
channel.description = "Auto Settlement service status"
val manager = getSystemService(NotificationManager::class.java)
manager?.createNotificationChannel(channel)
}
val notification = NotificationCompat.Builder(this, FOREGROUND_CHANNEL_ID)
.setSmallIcon(R.drawable.logo_mob)
.setContentTitle("Auto Settlement")
.setContentText("Preparing settlement reminder...")
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.build()
startForeground(NOTIFICATION_ID, notification)
}
private fun showSettlementAlert() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
ALERT_CHANNEL_ID,
"AUTO SETTLEMENT ALERT",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Daily settlement reminder"
enableVibration(true)
setSound(
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION),
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.build()
)
}
getSystemService(NotificationManager::class.java)?.createNotificationChannel(channel)
}
val pendingIntent = PendingIntent.getActivity(
this,
ALERT_NOTIFICATION_ID,
settlementIntent(),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(this, ALERT_CHANNEL_ID)
.setSmallIcon(R.drawable.logo_mob)
.setContentTitle("Settlement Reminder")
.setContentText("Daily settlement is due. Tap to open settlement.")
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_REMINDER)
.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
.setVibrate(longArrayOf(0, 250, 150, 250))
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build()
getSystemService(NotificationManager::class.java)
?.notify(ALERT_NOTIFICATION_ID, notification)
}
private fun playAlertSound() {
alertRingtone?.stop()
alertRingtone = RingtoneManager.getRingtone(
this,
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
)?.apply {
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.build()
play()
}
}
private fun launchSettlement() {
runCatching {
startActivity(settlementIntent())
}.onFailure {
LogUtil.e(
AutoSettleService::class.java.simpleName,
"Unable to open settlement screen: ${it.message}"
)
}
}
private fun settlementIntent(): Intent = Intent(this, MainActivity::class.java).apply {
addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_SINGLE_TOP or
Intent.FLAG_ACTIVITY_CLEAR_TOP
)
putExtra("AUTO_SETTLEMENT", true)
}
}

View File

@ -1,16 +0,0 @@
package com.mob.utsmyanmar.auto_settlement
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import sunmi.sunmiui.utils.LogUtil
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action != Intent.ACTION_BOOT_COMPLETED) return
val appContext = context ?: return
LogUtil.d(BootReceiver::class.java.simpleName, "Boot completed, rescheduling settlement alarm")
SettlementScheduler.schedule(appContext)
}
}

View File

@ -1,132 +0,0 @@
package com.mob.utsmyanmar.auto_settlement
import android.annotation.SuppressLint
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import java.util.Calendar
object SettlementScheduler {
private const val FALLBACK_ALERT_TIME = "23:30:00"
private const val REQUEST_CODE = 100
fun schedule(context: Context): Boolean {
if (!SystemParamsOperation.getInstance().isAutoSettlement()) {
cancel(context)
return false
}
val alarmManager =
context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
!alarmManager.canScheduleExactAlarms()
) {
return scheduleFallback(context)
}
val calendar = nextAlarmCalendar() ?: return false
val pendingIntent = alarmPendingIntent(context)
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
pendingIntent
)
return true
}
fun scheduleFallback(context: Context): Boolean {
if (!SystemParamsOperation.getInstance().isAutoSettlement()) {
cancel(context)
return false
}
val alarmManager =
context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val calendar = nextAlarmCalendar() ?: return false
val pendingIntent = alarmPendingIntent(context)
alarmManager.setAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
pendingIntent
)
return true
}
private fun nextAlarmCalendar(): Calendar? {
val configuredTime = parseTime(
SystemParamsOperation.getInstance().clearBatchTime
// "15:45:00"
) ?: parseTime(FALLBACK_ALERT_TIME) ?: return null
return Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, configuredTime.first)
set(Calendar.MINUTE, configuredTime.second)
set(Calendar.SECOND, configuredTime.third)
set(Calendar.MILLISECOND, 0)
if (before(Calendar.getInstance())) {
add(Calendar.DAY_OF_MONTH, 1)
}
}
}
private fun alarmPendingIntent(context: Context): PendingIntent {
val intent = Intent(context, AlarmReceiver::class.java)
return PendingIntent.getBroadcast(
context,
REQUEST_CODE,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or
PendingIntent.FLAG_IMMUTABLE
)
}
private fun parseTime(value: String): Triple<Int, Int, Int>? {
val parts = value.trim().split(":")
if (parts.size !in 2..3) return null
val hour = parts[0].toIntOrNull() ?: return null
val minute = parts[1].toIntOrNull() ?: return null
val second = if (parts.size == 3) parts[2].toIntOrNull() ?: return null else 0
if (hour !in 0..23 || minute !in 0..59 || second !in 0..59) return null
return Triple(hour, minute, second)
}
@SuppressLint("ServiceCast")
fun cancel(context: Context) {
val alarmManager =
context.getSystemService(Context.ALARM_SERVICE)
as AlarmManager
val intent = Intent(
context,
AlarmReceiver::class.java
)
val pendingIntent = PendingIntent.getBroadcast(
context,
100,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or
PendingIntent.FLAG_IMMUTABLE
)
alarmManager.cancel(pendingIntent)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,469 +0,0 @@
package com.mob.utsmyanmar.ui.cardwaiting
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.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.MaterialTheme
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.graphics.painter.Painter
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,
style = MaterialTheme.typography.titleMedium,
)
Spacer(modifier = Modifier.height(6.dp))
// Row(verticalAlignment = Alignment.Bottom) {
Text(
text = amount,
color = Color.LegacyRed,
style = MaterialTheme.typography.displayLarge
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = "MMK",
color = Color.LegacyRed,
style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.Medium),
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,
style = MaterialTheme.typography.titleSmall
)
Text(
text = if (uiState.isCardCaptured) {
"Reader captured card data"
} else {
"Hold your card near the reader"
},
color = Color.Black,
style = MaterialTheme.typography.labelSmall.copy(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,
style = MaterialTheme.typography.labelSmall
)
HorizontalDivider(
modifier = Modifier.weight(1f),
color = Color.Gray.copy(alpha = 0.4f)
)
}
Spacer(modifier = Modifier.height(10.dp))
InsertCardRow(onMockClick = onMockClick)
Spacer(modifier = Modifier.height(10.dp))
Text(
text = uiState.alertMessage,
color = Color.Black,
style = MaterialTheme.typography.labelSmall,
textAlign = TextAlign.Center,
lineHeight = 16.sp
)
Spacer(modifier = Modifier.height(10.dp))
Spacer(modifier = Modifier.weight(1f))
ManualEntryAction(onManualEntryClick = onManualEntryClick)
Spacer(modifier = Modifier.height(10.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,
style = MaterialTheme.typography.labelSmall
)
Text(
text = "Chip facing up",
color = Color.Black,
style = MaterialTheme.typography.labelSmall.copy(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,
style = MaterialTheme.typography.labelMedium
)
Text(
text = when {
uiState.isCardCaptured -> "Captured"
uiState.isLoading -> "Initializing"
else -> "Waiting"
},
color = Color.Gray,
style = MaterialTheme.typography.labelSmall
)
}
}
}
@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,
style = MaterialTheme.typography.titleSmall
)
}
}
}
@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.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
CardLogo(
icon = painterResource(R.drawable.logo_mpu),
name = "master",
)
CardLogo(
icon = painterResource(R.drawable.logo_visa),
name = "visa",
)
CardLogo(
icon = painterResource(R.drawable.logo_master),
name = "master",
)
CardLogo(
icon = painterResource(R.drawable.logo_union_pay),
name = "union",
)
CardLogo(
icon = painterResource(R.drawable.logo_jcb),
name = "jcb",
)
}
}
}
@Composable
private fun CardLogo(
icon: Painter,
name: String,
modifier: Modifier = Modifier
) {
Surface(
modifier = modifier,
shape = RoundedCornerShape(4.dp),
color = Color.White
) {
Box(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
contentAlignment = Alignment.Center
) {
Image(
painter = icon,
contentDescription = name,
modifier = Modifier.height(30.dp)
)
}
}
}
@Composable
private fun PoweredByMob() {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "Powered by",
color = Color.Gray,
style = MaterialTheme.typography.labelSmall.copy(fontSize = 8.sp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "MOB",
color = Color.Gray,
style = MaterialTheme.typography.labelSmall.copy(fontSize = 9.sp)
)
}
}
@P2Preview
@Composable
fun PreviewCardWaitingScreen() {
CardWaitingScreenContent(
amount = "50,000",
uiState = CardWaitingUiState(),
onBackClick = {},
onManualEntryClick = {},
onMockClick = {}
)
}

View File

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

View File

@ -1,333 +0,0 @@
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.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
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
_uiState.update {
it.copy(canGoBack = true)
}
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() {
stopCardReading()
viewModelScope.launch {
_events.send(CardWaitingEvent.GoManualEntry)
}
}
fun onMockClick() {
onScreenPause()
cardReadViewModel.setCardTransactionType(CardTransactionType.MOCK)
viewModelScope.launch {
_uiState.update {
it.copy(
alertMessage = "Mock card detected.",
isLoading = false,
isCardCaptured = true
)
}
_events.send(CardWaitingEvent.GoProcessingCard)
}
}
private fun startCardReadWhenReady(isFallback: Boolean) {
readerInitJob?.cancel()
readerInitJob = viewModelScope.launch {
if (cardReadViewModel.isReaderReady()) {
setupCardReadProcess(isFallback)
return@launch
}
_uiState.update {
it.copy(
alertMessage = "Initializing card reader...",
isLoading = true,
isCardCaptured = false
)
}
repeat(10) {
delay(300)
if (cardReadViewModel.isReaderReady()) {
_uiState.update { state ->
state.copy(
alertMessage = if (isFallback) {
"Fallback!\nPlease stripe!"
} else {
"Please insert, tap, or swipe card"
},
isLoading = false,
isCardCaptured = false
)
}
setupCardReadProcess(isFallback)
return@launch
}
}
_uiState.update {
it.copy(
alertMessage = "Card reader unavailable.\nPlease wait and try again.",
isLoading = false
)
}
}
}
private fun setupCardReadProcess(isFallback: Boolean) {
initCheckCard(isFallback)
}
private fun initCheckCard(isFallback: Boolean) {
var allType =
AidlConstants.CardType.NFC.value or
AidlConstants.CardType.IC.value or
AidlConstants.CardType.MAGNETIC.value
if (isFallback) {
allType = AidlConstants.CardType.MAGNETIC.value
} else if (
SystemParamsOperation.getInstance().isMagStripeEnabled &&
!SystemParamsOperation.getInstance().isNfcEnabled
) {
allType =
AidlConstants.CardType.IC.value or
AidlConstants.CardType.MAGNETIC.value
}
cardReadViewModel.startCheckXProcess(
allType,
65,
object : CheckCardResultX {
override fun onSuccess(cardType: CardTypeX, isMPU: Boolean) {
Log.d("CardWaitingViewModel", "on success cardType=$cardType and isMpu=$isMPU")
when {
!isFallback && cardType == CardTypeX.MAG -> {
if (SystemParamsOperation.getInstance().isMagStripeEnabled) {
cardReadViewModel.setCardTransactionType(CardTransactionType.MAG)
} else {
_uiState.update {
it.copy(alertMessage = "Mag stripe not allowed")
}
setupCardReadProcess(false)
return
}
}
isFallback && cardType == CardTypeX.MAG -> {
sharedViewModel.setEmvTrans(false)
cardReadViewModel.setCardTransactionType(CardTransactionType.FALLBACK)
}
cardType == CardTypeX.IC || cardType == CardTypeX.NFC -> {
if (isMPU) {
sharedViewModel.setEmvTrans(false)
cardReadViewModel.setCardTransactionType(CardTransactionType.MPU)
} else {
cardReadViewModel.setCardData(cardType.value)
cardReadViewModel.setCardTransactionType(CardTransactionType.EMV)
}
}
}
viewModelScope.launch {
_uiState.update {
it.copy(
alertMessage = "Card detected.",
isLoading = false,
isCardCaptured = true
)
}
_events.send(CardWaitingEvent.GoProcessingCard)
}
}
override fun onError(code: Int, message: String) {
ecrActionCancel("Transaction cancelled")
viewModelScope.launch {
_uiState.update {
it.copy(
alertMessage = message,
isLoading = false
)
}
_events.send(CardWaitingEvent.GoMain)
}
}
override fun onCommError() {
if (fallbackEnabled && retryCounter < fallbackCounter) {
_uiState.update {
it.copy(
alertMessage = "Card not detected!\nRemain Attempt - ${fallbackCounter - retryCounter}"
)
}
retryCounter++
setupCardReadProcess(false)
return
}
if (retryCounter == fallbackCounter) {
_uiState.update {
it.copy(alertMessage = "Fallback!\nPlease stripe!")
}
setupCardReadProcess(true)
return
}
ecrActionCancel("Transaction cancelled")
viewModelScope.launch {
_uiState.update {
it.copy(
alertMessage = "Chip not detected!",
isLoading = false
)
}
_events.send(CardWaitingEvent.GoMain)
}
}
}
)
}
private fun ecrActionCancel(msg: String) {
if (sharedViewModel.isEcr.value != true) {
return
}
sharedViewModel.isEcr.postValue(false)
sharedViewModel.isEcrFinished.postValue(true)
if (SystemParamsOperation.getInstance().isCMHLEnabled) {
ECRHelper.send(
CoreUtils.getInstance(sharedViewModel)
.generateCMHLResponse(ECRResultStatus.USER_CANCEL)
)
CoreUtils.getInstance(sharedViewModel).responseACKCMHL()
return
}
CoreUtils.getInstance(sharedViewModel).responseRejectMsg(msg)
}
private fun stopCardReading() {
sharedViewModel.setIsFallback(false)
cardReadViewModel.cancelCheckCard()
cardReadViewModel.resetOneTimeFlag()
cardReadViewModel.cancelCheckXProcess()
}
override fun onCleared() {
super.onCleared()
readerInitJob?.cancel()
stopCardReading()
}
}

View File

@ -1,237 +0,0 @@
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.MaterialTheme
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 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,
style = MaterialTheme.typography.labelSmall,
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,
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Medium),
modifier = Modifier.padding(end = 10.dp, bottom = 6.dp)
)
}
Text(
text = displayValue,
style = MaterialTheme.typography.displayLarge.copy(color = Color.LegacyRed)
)
}
Spacer(modifier = Modifier.height(12.dp))
Text(
text = supportingText,
color = if (errorMessage == null) Color.Gray else Color.Error,
style = MaterialTheme.typography.labelSmall,
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,
style = MaterialTheme.typography.titleSmall
)
}
}
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,
style = MaterialTheme.typography.headlineLarge,
textAlign = TextAlign.Center
)
}
}

View File

@ -1,104 +0,0 @@
package com.mob.utsmyanmar.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color as ComposeColor
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.mob.utsmyanmar.ui.theme.Color
@Composable
fun NumericKeypad(
modifier: Modifier = Modifier,
onKeyClick: (String) -> Unit,
keys : List<List<String>>,
) {
// val keys : List<List<String>> = listOf(
// listOf("1", "2", "3"),
// listOf("4", "5", "6"),
// listOf("7", "8", "9"),
// listOf(".", "0", "00")
// )
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
keys.forEach { row ->
Row(
modifier = Modifier.fillMaxWidth().weight(1f),
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
row.forEach { key ->
KeypadButton(
text = key,
modifier = Modifier.weight(1f).fillMaxHeight(),
onClick = { onKeyClick(key) }
)
}
}
}
}
}
@Composable
fun KeypadButton(
text: String,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
val enabled = text.isNotBlank()
Box(
modifier = modifier
.then(
if (enabled) Modifier.shadow(elevation = 2.dp, shape = RoundedCornerShape(8.dp), clip = false)
else Modifier
)
.background(
color = if (enabled) Color.White else ComposeColor.Transparent,
shape = RoundedCornerShape(8.dp)
)
.clickable(enabled = enabled) { onClick() },
contentAlignment = Alignment.Center
) {
if (enabled) {
Text(
text = text,
color = Color.LegacyRed,
fontSize = 24.sp,
fontWeight = FontWeight.Normal,
textAlign = TextAlign.Center
)
}
}
}
@Preview
@Composable
fun PreviewNumericKeypad(){
val keys : List<List<String>> = listOf(
listOf("1", "2", "3"),
listOf("4", "5", "6"),
listOf("7", "8", "9"),
listOf(".", "0", "00")
)
NumericKeypad(
keys = keys,
onKeyClick = {}
)
}

View File

@ -10,9 +10,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.mob.utsmyanmar.ui.theme.MOBPOSTheme import com.mob.utsmyanmar.ui.theme.MOBPOSTheme
import com.mob.utsmyanmar.ui.theme.Primary import com.mob.utsmyanmar.ui.theme.Primary
import com.mob.utsmyanmar.ui.theme.White import com.mob.utsmyanmar.ui.theme.White
@ -64,7 +66,8 @@ fun SquareButton(
Text( Text(
text = title, text = title,
style = MaterialTheme.typography.headlineLarge, fontSize = 24.sp,
fontWeight = FontWeight.SemiBold,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
maxLines = 1 maxLines = 1
) )

View File

@ -1,63 +0,0 @@
package com.mob.utsmyanmar.ui.components.appbar
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
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.tooling.preview.Preview
import androidx.lifecycle.compose.dropUnlessResumed
import com.mob.utsmyanmar.ui.theme.Color
import com.mob.utsmyanmar.ui.theme.White
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppBar(
title: String,
icon: ImageVector? = null,
onIconClick: (() -> Unit)? = null,
actions: @Composable RowScope.() -> Unit = {}
) {
CenterAlignedTopAppBar(
title = {
Text(
text = title,
color = White,
style = MaterialTheme.typography.titleMedium
)
},
navigationIcon = {
if (icon != null && onIconClick != null) {
IconButton(
onClick = dropUnlessResumed { onIconClick() }
) {
Icon(
imageVector = icon,
contentDescription = "App bar icon",
tint = White
)
}
}
},
actions = actions,
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.LegacyRed
)
)
}
@Preview
@Composable
fun PreviewAppBar(){
AppBar(
title = "Title"
)
}

View File

@ -1,525 +0,0 @@
package com.mob.utsmyanmar.ui.dashboard
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BarChart
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.ChevronRight
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.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
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.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import com.mob.utsmyanmar.R
import com.mob.utsmyanmar.ui.theme.Color
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.milliseconds
@Composable
fun AdvertisingArea(
modifier: Modifier = Modifier,
carouselUrls: List<String> = emptyList(),
) {
val imageModels: List<Any> = carouselUrls.ifEmpty {
listOf(R.drawable.sunmi1, R.drawable.sunmi2, R.drawable.sunmi3)
}
val pageState = rememberPagerState(pageCount = { imageModels.size })
LaunchedEffect(pageState) {
while (true) {
delay(10000.milliseconds)
val nextPage = (pageState.currentPage + 1) % imageModels.size
pageState.animateScrollToPage(
page = nextPage, animationSpec = tween(durationMillis = 700)
)
}
}
Card(
modifier = modifier,
shape = RoundedCornerShape(0.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
) {
HorizontalPager(
state = pageState, modifier = Modifier.fillMaxSize()
) { page ->
AsyncImage(
model = imageModels[page],
contentDescription = "ads images",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
}
}
}
@Composable
fun SummaryCard(
onClickLastSync: () -> Unit,
lastSyncTime: Long?,
totalSaleCount: Int,
totalSaleAmount: Long,
settlementStatusText: String,
settlementDateTimeText: String
) {
var now by remember { mutableLongStateOf(System.currentTimeMillis()) }
LaunchedEffect(Unit) {
while (true) {
delay(30_000.milliseconds)
now = System.currentTimeMillis()
}
}
var isRotating by remember { mutableStateOf(false) }
val rotation by animateFloatAsState(
targetValue = if (isRotating) 360f else 0f,
animationSpec = tween(durationMillis = 3000, easing = LinearEasing),
label = "sync rotating",
finishedListener = { isRotating = false }
)
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 = "Total Sale",
value = "MMK ${formatAmount(totalSaleAmount)}",
subtitle = "$totalSaleCount 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 = settlementStatusText,
subtitle = settlementDateTimeText,
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.CenterVertically
) {
Text(
text = "Last Sync", style = MaterialTheme.typography.bodySmall
)
Box(
modifier = Modifier
.size(38.dp)
.clip(CircleShape)
.background(Color.CrimsonRed.copy(alpha = 0.1f))
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
if (!isRotating) isRotating = true
onClickLastSync()
},
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Default.Sync,
contentDescription = null,
tint = Color.CrimsonRed,
modifier = Modifier
.size(22.dp)
.rotate(rotation)
)
}
}
Spacer(Modifier.height(6.dp))
Text(
text = formatRelativeTime(lastSyncTime, now),
style = MaterialTheme.typography.titleMedium,
color = Color.LegacyRed
)
}
}
}
}
@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, style = MaterialTheme.typography.bodySmall)
Text(
value, style = MaterialTheme.typography.titleSmall, color = Color.LegacyRed
)
Text(subtitle, style = MaterialTheme.typography.bodySmall)
}
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
fun MenuPager(
items: List<DashboardMenuItem>,
modifier: Modifier = Modifier
) {
val pages = items.chunked(6)
val pagerState = rememberPagerState(pageCount = { pages.size })
Column(modifier = modifier) {
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxWidth()
) { pageIndex ->
MenuPage(items = pages[pageIndex])
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
repeat(pages.size) { index ->
val selected = pagerState.currentPage == index
Box(
modifier = Modifier
.padding(horizontal = 4.dp)
.size(if (selected) 8.dp else 6.dp)
.background(
color = if (selected) Color.LegacyRed else Color.Gray.copy(alpha = 0.4f),
shape = CircleShape
)
)
}
}
}
}
@Composable
private fun MenuPage(items: List<DashboardMenuItem>) {
Column(
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.padding(horizontal = 16.dp)
) {
items.chunked(3).forEach { rowItems ->
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
rowItems.forEach { item ->
MenuCard(
title = item.title,
icon = item.iconContent,
modifier = Modifier.weight(1f),
onClick = item.onClick
)
}
repeat(3 - rowItems.size) {
Spacer(modifier = Modifier.weight(1f))
}
}
}
}
}
@Composable
private fun MenuCard(
title: String,
icon: @Composable () -> Unit,
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null
) {
Card(
modifier = modifier
.height(100.dp)
.then(
if (onClick != null) {
Modifier.clickable(onClick = onClick)
} else {
Modifier
}
),
shape = RoundedCornerShape(14.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(5.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(6.dp)
) {
Column {
icon()
Spacer(modifier = Modifier.weight(1f))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = title,
style = MaterialTheme.typography.bodySmall,
color = Color.Black,
overflow = TextOverflow.Ellipsis,
maxLines = 2,
modifier = Modifier.weight(1f)
)
Icon(
imageVector = Icons.Default.ChevronRight,
contentDescription = null,
tint = Color.LegacyRed,
modifier = Modifier.size(28.dp)
)
}
}
}
}
}
fun LazyListScope.recentTransactionsItems(
transactions: List<TrnxRecord>,
onTransactionClick: (TrnxRecord) -> Unit = {}
) {
item {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Recent Transactions",
style = MaterialTheme.typography.titleSmall
)
}
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
color = Color.Gray.copy(alpha = 0.2f)
)
}
if (transactions.isEmpty()) {
item {
Text(
text = "No recent transactions",
color = Color.Gray,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 24.dp)
)
}
} else {
items(items = transactions, key = { it.pid }) { record ->
TrnxRow(
record = record,
onClick = { onTransactionClick(record) }
)
}
}
}
@Composable
private fun TrnxRow(
record: TrnxRecord,
onClick: (() -> Unit)? = null
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 4.dp)
.clip(RoundedCornerShape(10.dp))
.background(Color.White)
.then(
if (onClick != null) {
Modifier.clickable(onClick = onClick)
} else {
Modifier
}
)
.padding(horizontal = 12.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(36.dp)
.background(Color.LegacyRed.copy(alpha = 0.1f), CircleShape),
contentAlignment = Alignment.Center
) {
Text(
text = record.typeLabel.take(1),
color = Color.LegacyRed,
style = MaterialTheme.typography.bodyLarge
)
}
Spacer(Modifier.width(10.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = record.typeLabel,
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
color = Color.Black
)
Text(
text = record.maskedCard,
style = MaterialTheme.typography.labelSmall,
color = Color.Gray
)
}
Column(horizontalAlignment = Alignment.End) {
Text(
text = record.amountDisplay,
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold),
color = if (record.isVoided) Color.Gray else Color.LegacyRed
)
Text(
text = record.dateTime,
style = MaterialTheme.typography.labelSmall,
color = Color.Gray
)
}
}
}
@Composable
fun Item(
onClick: () -> Unit,
title: String,
subTitle: String,
leadingIcon: (@Composable () -> Unit)? = null,
trailingIcon: (@Composable () -> Unit)? = null,
) {
Button(
onClick = onClick,
colors = ButtonDefaults.buttonColors(
containerColor = Color.White,
contentColor = Color.Black
),
modifier = Modifier.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
leadingIcon?.let {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(16.dp))
.background(Color.LegacyRed.copy(alpha = 0.1f))
.padding(8.dp)
) {
it()
}
}
Spacer(modifier = Modifier.width(12.dp))
Column(
modifier = Modifier
.weight(1f)
.padding(horizontal = 8.dp)
) {
Text(text = title)
Text(
text = subTitle,
style = MaterialTheme.typography.bodySmall,
color = Color.Gray
)
}
trailingIcon?.invoke()
}
}
}

View File

@ -1,233 +0,0 @@
package com.mob.utsmyanmar.ui.dashboard
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
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.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.mob.utsmyanmar.R
import com.mob.utsmyanmar.ui.theme.Color
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
class DashboardMenuItem(
val title: String,
val iconContent: @Composable () -> Unit,
val onClick: () -> Unit
)
fun buildMenuItems(
onNavigateAmount: (String) -> Unit,
onNavigateSignOn: () -> Unit,
onNavigateSettlement: () -> Unit,
onNavigateHistory: () -> Unit,
onNavigateAction: (String) -> Unit
): List<DashboardMenuItem> {
val sp = SystemParamsOperation.getInstance()
val settlementEnabled = sp.settlementStatus
val voidEnabled = sp.voidStatus
val refundEnabled = sp.refundStatus
val preAuthEnabled = sp.preAuthStatus
val preAuthVoidEnabled = sp.preAuthStatus
val preAuthCompleteEnabled = sp.preAuthStatus
val preAuthCompleteVoidEnabled = sp.preAuthStatus
val cashAdvEnabled = sp.cashAdvanceStatus
val mmqrEnabled = sp.mmqrStatus
return buildList {
add(
DashboardMenuItem(
"Sale",
{
Icon(
painterResource(R.drawable.ic_terminal),
contentDescription = null,
modifier = Modifier.size(40.dp),
tint = Color.LegacyRed
)
}
) { onNavigateAmount("Sale") }
)
if (mmqrEnabled) {
add(
DashboardMenuItem(
"MMQR",
{
Image(
painter = painterResource(R.drawable.ic_mmqr_logo),
contentDescription = null,
modifier = Modifier.height(48.dp)
)
}
) { }
)
}
add(
DashboardMenuItem(
"History",
{
Icon(
painterResource(R.drawable.ic_history),
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = Color.LegacyRed
)
}
) { onNavigateHistory() }
)
add(
DashboardMenuItem(
"Sign On",
{
Icon(
painterResource(R.drawable.ic_sign_on),
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = Color.LegacyRed
)
}
) { onNavigateSignOn() }
)
if (settlementEnabled) {
add(
DashboardMenuItem(
"Settlement",
{
Icon(
painterResource(R.drawable.ic_settlement),
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = Color.LegacyRed
)
}
) { onNavigateSettlement() }
)
}
if (voidEnabled) {
add(
DashboardMenuItem(
"Void",
{
Icon(
Icons.Default.Lock,
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = Color.LegacyRed
)
}
) { onNavigateAction("Void") }
)
}
if (refundEnabled) {
add(
DashboardMenuItem(
"Refund",
{
Icon(
Icons.Default.Replay,
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = Color.LegacyRed
)
}
) { onNavigateAction("Refund") }
)
}
if (preAuthEnabled) {
add(
DashboardMenuItem(
"Pre-Auth",
{
Icon(
Icons.Default.Lock,
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = Color.LegacyRed
)
}
) { onNavigateAmount("Pre-Auth") }
)
}
if (preAuthVoidEnabled) {
add(
DashboardMenuItem(
"Pre-Auth Void",
{
Icon(
Icons.Default.LockOpen,
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = Color.LegacyRed
)
}
) { onNavigateAction("Pre-Auth Void") }
)
}
if (preAuthCompleteEnabled) {
add(
DashboardMenuItem(
"Pre-Auth Complete",
{
Icon(
Icons.Default.CreditCard,
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = Color.LegacyRed
)
}
) { onNavigateAction("Pre-Auth Complete") }
)
}
if (preAuthCompleteVoidEnabled) {
add(
DashboardMenuItem(
"Pre-Auth Comp Void",
{
Icon(
Icons.Default.SwapHoriz,
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = Color.LegacyRed
)
}
) { onNavigateAction("Pre-Auth Complete Void") }
)
}
if (cashAdvEnabled) {
add(
DashboardMenuItem(
"Cash Adv",
{
Icon(
Icons.Default.AccountBalanceWallet,
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = Color.LegacyRed
)
}
) { onNavigateAmount("Cash Adv") }
)
}
}
}

View File

@ -1,482 +1,252 @@
package com.mob.utsmyanmar.ui.dashboard package com.mob.utsmyanmar.ui.dashboard
import android.os.Build
import android.os.Handler
import android.os.Looper
import androidmads.library.qrgenearator.QRGContents
import androidmads.library.qrgenearator.QRGEncoder
import androidx.annotation.RequiresApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.*
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.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material3.*
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.Sync
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight 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.window.Dialog import androidx.compose.ui.unit.sp
import com.mob.utsmyanmar.BuildConfig import com.mob.utsmyanmar.ui.components.SquareButton
import com.mob.utsmyanmar.R import com.mob.utsmyanmar.ui.theme.MOBPOSTheme
import com.mob.utsmyanmar.ui.components.appbar.AppBar import com.mob.utsmyanmar.ui.theme.*
import com.mob.utsmyanmar.ui.device_info.DeviceInfoUiState
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.launch import kotlinx.coroutines.launch
import com.mob.utsmyanmar.R
@RequiresApi(Build.VERSION_CODES.N) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun DashboardScreen2( fun DashboardScreen(
onNavigateAmount: (String) -> Unit = {}, settlementEnabled: Boolean,
onNavigateSignOn: () -> Unit = {}, wavePayEnabled: Boolean,
onNavigateSettlement: () -> Unit = {}, onAmountClick: (String) -> Unit,
onNavigateVersion: () -> Unit = {}, onTransactionClick: () -> Unit,
onNavigateFunctions: () -> Unit = {}, onSettlementClick: () -> Unit,
onNavigateHistory: () -> Unit = {}, onHistoryClick: () -> Unit,
onNavigateAction: (String) -> Unit = {}, onCardClick: () -> Unit,
onNavigateNotifications: () -> Unit = {}, onWavePayClick: () -> Unit
dashboardUiState: DashboardUiState = DashboardUiState(),
deviceInfo: DeviceInfoUiState = DeviceInfoUiState(),
onClickLastSync: () -> Unit = {},
onNavigateRecentTransaction: (TrnxRecord) -> Unit = {},
) { ) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope() 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("") }
var reversalEnabled by remember {
mutableStateOf(
runCatching { SystemParamsOperation.getInstance().isReversalOn }.getOrDefault(false)
)
}
val isOnline by rememberIsOnline()
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)
"Sign-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
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
)
}
if (dashboardUiState.isSyncing) {
Dialog(onDismissRequest = {}) {
Card(
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(8.dp)
) {
Column(
modifier = Modifier.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator(color = Color.LegacyRed)
Spacer(Modifier.height(16.dp))
Text(
text = dashboardUiState.syncStatusText,
style = MaterialTheme.typography.bodyMedium,
color = Color.Black
)
}
}
}
}
ModalNavigationDrawer( ModalNavigationDrawer(
drawerState = drawerState, drawerState = drawerState,
drawerContent = { drawerContent = {
ModalDrawerSheet( ModalDrawerSheet(
modifier = Modifier.fillMaxWidth(0.78f), modifier = Modifier.fillMaxWidth(0.8f),
drawerContainerColor = Color.White, drawerContainerColor = White,
drawerShape = RoundedCornerShape(topEnd = 24.dp, bottomEnd = 24.dp) drawerShape = RoundedCornerShape(topEnd = 20.dp, bottomEnd = 20.dp)
) { ) {
Column( Text(
text = "MOBPOS",
modifier = Modifier.padding(start = 24.dp, top = 28.dp, bottom = 8.dp),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
Text(
text = "Dashboard",
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(Color.IvoryBeige) .clickable {
.padding(horizontal = 20.dp, vertical = 28.dp) scope.launch { drawerState.close() }
) {
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 { .padding(horizontal = 24.dp, vertical = 14.dp),
Text( fontSize = 16.sp,
text = "MOB Merchant", fontWeight = FontWeight.Medium
color = Color.CrimsonRed, )
style = MaterialTheme.typography.labelMedium
)
Text(
text = "S/N:${deviceInfo.serialNumber}",
color = Color.Black,
style = MaterialTheme.typography.labelMedium
)
Text(
text = if (isOnline) "Online" else "Offline",
color = if (isOnline) Color.Success else Color.Error,
style = MaterialTheme.typography.labelMedium
)
}
}
}
Spacer(modifier = Modifier.height(10.dp))
Text( Text(
text = "App Version : ${BuildConfig.VERSION_NAME}", text = "Transactions",
style = MaterialTheme.typography.bodySmall,
color = Color.Gray,
modifier = Modifier.padding(horizontal = 16.dp)
)
Spacer(modifier = Modifier.height(10.dp))
Text(
text = "Released on : 06-Sep-2026",
style = MaterialTheme.typography.bodySmall,
color = Color.Gray,
modifier = Modifier.padding(horizontal = 16.dp)
)
Spacer(modifier = Modifier.height(10.dp))
Text(
text = "Connection Settings",
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(horizontal = 16.dp)
)
Item(
title = "Sign-Off", subTitle = "Sign off from System",
onClick = {
scope.launch { drawerState.close() }
openHostActionDialog("Sign-Off")
},
leadingIcon = {
Icon(
painterResource(R.drawable.ic_cancel_circle),
contentDescription = "icon",
tint = Color.LegacyRed
)
},
trailingIcon = {},
)
Item(
title = "Reversal", subTitle = "Enable / Disable Reversal",
onClick = {
reversalEnabled = !reversalEnabled
runCatching {
SystemParamsOperation.getInstance().setReversalFlag(reversalEnabled)
}
},
leadingIcon = {
Icon(
Icons.Default.Sync,
contentDescription = "icon",
tint = Color.LegacyRed
)
},
trailingIcon = {
Switch(
checked = reversalEnabled,
onCheckedChange = { isChecked ->
reversalEnabled = isChecked
runCatching {
SystemParamsOperation.getInstance().setReversalFlag(isChecked)
}
}
)
},
)
Text(
text = "System Management",
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(horizontal = 16.dp)
)
Item(
title = "Functions", subTitle = "System Function Settings",
onClick = {
scope.launch { drawerState.close() }
onNavigateFunctions()
},
leadingIcon = {
Icon(
painterResource(R.drawable.ic_four_boxes),
contentDescription = "icon",
tint = Color.LegacyRed,
modifier = Modifier.size(16.dp)
)
},
trailingIcon = {},
)
Item(
title = "Version", subTitle = "View App Version Info",
onClick = {
scope.launch { drawerState.close() }
onNavigateVersion()
},
leadingIcon = {
Icon(
painterResource(R.drawable.ic_version),
contentDescription = "icon",
tint = Color.LegacyRed
)
},
trailingIcon = {},
)
Spacer(modifier = Modifier.height(16.dp))
val adUrl = "https://www.mobmyanmar.com/"
val qrBitmap = remember {
runCatching {
val encoder = QRGEncoder(adUrl, null, QRGContents.Type.TEXT, 256)
encoder.colorBlack = android.graphics.Color.BLACK
encoder.colorWhite = android.graphics.Color.WHITE
encoder.bitmap.asImageBitmap()
}.getOrNull()
}
qrBitmap?.let { bitmap ->
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
Image(
bitmap = bitmap,
contentDescription = "Scan for User Guide",
modifier = Modifier.size(150.dp)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Scan for Offers",
style = MaterialTheme.typography.labelSmall,
color = Color.Gray
)
}
}
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.weight(1f))
Text(
text = "v${BuildConfig.VERSION_NAME}",
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp), .clickable {
style = MaterialTheme.typography.labelSmall, scope.launch { drawerState.close() }
color = Color.Black.copy(alpha = 0.5f) onTransactionClick()
}
.padding(horizontal = 24.dp, vertical = 14.dp),
fontSize = 16.sp
)
Text(
text = "History",
modifier = Modifier
.fillMaxWidth()
.clickable {
scope.launch { drawerState.close() }
onHistoryClick()
}
.padding(horizontal = 24.dp, vertical = 14.dp),
fontSize = 16.sp
)
Text(
text = "Settlement",
modifier = Modifier
.fillMaxWidth()
.clickable(enabled = settlementEnabled) {
scope.launch { drawerState.close() }
onSettlementClick()
}
.padding(horizontal = 24.dp, vertical = 14.dp),
color = if (settlementEnabled) Black else White,
fontSize = 16.sp
)
Text(
text = "WavePay",
modifier = Modifier
.fillMaxWidth()
.clickable(enabled = wavePayEnabled) {
scope.launch { drawerState.close() }
onWavePayClick()
}
.padding(horizontal = 24.dp, vertical = 14.dp),
color = if (wavePayEnabled) Black else White,
fontSize = 16.sp
) )
} }
} }
) { ) {
Scaffold( Scaffold(
containerColor = Color.IvoryBeige,
topBar = { topBar = {
AppBar( CenterAlignedTopAppBar(
title = "Dashboard", title = {
icon = Icons.Default.Menu, Text(text = "Dashboard", color = White, fontWeight = FontWeight.SemiBold)
onIconClick = { scope.launch { drawerState.open() } }, },
actions = { navigationIcon = {
IconButton(onClick = onNavigateNotifications) { IconButton(
Box { onClick = {
Icon( scope.launch {
imageVector = Icons.Default.Notifications, drawerState.open()
contentDescription = "Notifications", }
tint = Color.White
)
Box(
modifier = Modifier
.size(8.dp)
.background(Color.GoldenGlow, CircleShape)
.align(Alignment.TopEnd)
.offset(x = 2.dp, y = (-2).dp)
)
} }
) {
Icon(
painter = painterResource(R.drawable.ic_menu),
contentDescription = "Menu Icon",
tint = White
)
} }
} },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Primary
)
) )
} }
) { paddingValues -> ) { paddingValues ->
androidx.compose.foundation.lazy.LazyColumn( Column(
modifier = Modifier modifier = Modifier
.padding(paddingValues)
.fillMaxSize() .fillMaxSize()
.padding(paddingValues)
.background(White)
) { ) {
item { Box(
AdvertisingArea( modifier = Modifier
carouselUrls = dashboardUiState.carouselUrls, .fillMaxWidth()
.padding(20.dp),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(R.drawable.logo_mob),
contentDescription = "Mob logo"
)
}
Box(
modifier = Modifier
.fillMaxSize()
.background(
color = Primary,
shape = RoundedCornerShape(
topStart = 16.dp,
topEnd = 16.dp,
bottomStart = 0.dp,
bottomEnd = 0.dp
)
)
) {
Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxSize()
.height(200.dp) .padding(16.dp),
) verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
SquareButton(
title = "Sale",
icon = R.drawable.ic_sale,
modifier = Modifier.weight(1f),
containerColor = White,
contentColor = Primary,
iconTint = Primary,
border = null,
onClick = { onAmountClick("Sale") },
)
SquareButton(
title = "Sign On",
icon = R.drawable.ic_menu,
modifier = Modifier.weight(1f),
containerColor = White,
contentColor = Primary,
iconTint = Primary,
border = null,
onClick = { onAmountClick("Sign On") }
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
SquareButton(
title = "Settlement",
icon = R.drawable.ic_menu,
modifier = Modifier.weight(1f),
enabled = settlementEnabled,
containerColor = White,
contentColor = Primary,
iconTint = Primary,
border = null,
onClick = onSettlementClick
)
SquareButton(
title = "Others",
icon = R.drawable.ic_menu,
modifier = Modifier.weight(1f),
containerColor = White,
contentColor = Primary,
iconTint = Primary,
border = null,
onClick = onCardClick
)
}
}
} }
item {
Spacer(Modifier.height(12.dp))
SummaryCard(
onClickLastSync = onClickLastSync,
lastSyncTime = dashboardUiState.lastSyncTime,
totalSaleCount = dashboardUiState.totalSaleCount,
totalSaleAmount = dashboardUiState.totalSaleAmount,
settlementStatusText = dashboardUiState.settlementStatusText,
settlementDateTimeText = dashboardUiState.settlementDateTimeText
)
Spacer(Modifier.height(12.dp))
}
item {
MenuPager(
items = buildMenuItems(
onNavigateAmount = onNavigateAmount,
onNavigateSignOn = onNavigateSignOn,
onNavigateSettlement = onNavigateSettlement,
onNavigateHistory = onNavigateHistory,
onNavigateAction = onNavigateAction
),
modifier = Modifier.fillMaxWidth()
)
}
item { Spacer(Modifier.height(8.dp)) }
recentTransactionsItems(
transactions = dashboardUiState.recentTransactions,
onTransactionClick = onNavigateRecentTransaction
)
} }
} }
} }
} }
@Preview(showBackground = true, showSystemUi = true)
@Composable
private fun DashboardScreenPreview() {
MOBPOSTheme {
DashboardScreen(
settlementEnabled = true,
wavePayEnabled = true,
onAmountClick = {},
onTransactionClick = {},
onSettlementClick = {},
onHistoryClick = {},
onCardClick = {},
onWavePayClick = {}
)
}
}

View File

@ -1,23 +0,0 @@
package com.mob.utsmyanmar.ui.dashboard
data class TrnxRecord(
val pid: Long,
val typeLabel: String,
val amountDisplay: String,
val maskedCard: String,
val dateTime: String,
val isVoided: Boolean,
val isApproved: Boolean
)
data class DashboardUiState(
val recentTransactions: List<TrnxRecord> = emptyList(),
val lastSyncTime: Long? = null,
val carouselUrls: List<String> = emptyList(),
val totalSaleCount: Int = 0,
val totalSaleAmount: Long = 0L,
val settlementStatusText: String = "Pending",
val settlementDateTimeText: String = "--/-- --:--",
val isSyncing: Boolean = false,
val syncStatusText: String = "Connecting to TMS server..."
)

View File

@ -1,42 +0,0 @@
package com.mob.utsmyanmar.ui.dashboard
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
fun formatRelativeTime(timestamp: Long?, now: Long): String {
if (timestamp == null) return "Never"
val diff = now - timestamp
return when {
diff < 60_000L -> "Just now"
diff < 3_600_000L -> "${diff / 60_000L} mins ago"
diff < 86_400_000L -> "${diff / 3_600_000L} hours ago"
else -> "${diff / 86_400_000L} days ago"
}
}
@Composable
fun rememberIsOnline(): State<Boolean> = run {
val context = LocalContext.current
val cm = remember(context) {
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
}
produceState(
initialValue = cm.activeNetwork
?.let { cm.getNetworkCapabilities(it)?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) }
?: false
) {
val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { value = true }
override fun onLost(network: Network) { value = false }
}
cm.registerDefaultNetworkCallback(callback)
awaitDispose { cm.unregisterNetworkCallback(callback) }
}
}

View File

@ -1,193 +0,0 @@
package com.mob.utsmyanmar.ui.dashboard
import android.content.Context
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.SystemDateTime
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.text.DecimalFormat
import java.util.Calendar
import javax.inject.Inject
@HiltViewModel
class DashboardViewModel @Inject constructor(
private val repository: Repository,
@ApplicationContext private val context: Context
) : ViewModel() {
private val _uiState = MutableStateFlow(DashboardUiState())
val uiState: StateFlow<DashboardUiState> = _uiState.asStateFlow()
init {
viewModelScope.launch {
repository.getLastTenTransactions().asFlow().collect { payDetails ->
val latestSettlement = payDetails.lastOrNull {
it.transactionType == TransactionsType.SETTLEMENT.value
}
val isCurrentBusinessSettlement = latestSettlement?.isCurrentBusinessSettlement() == true
_uiState.update {
it.copy(
recentTransactions = payDetails
.filterNot { it.isSettlementTransaction() }
.map { pd -> pd.toRecord() },
totalSaleCount = payDetails.totalSaleCount(),
totalSaleAmount = payDetails.totalSaleAmount(),
settlementStatusText = if (isCurrentBusinessSettlement) {
"Completed"
} else {
"Pending"
},
settlementDateTimeText = latestSettlement
?.let { buildDateTime(it.TradeDate, it.TradeTime) }
?: "--/-- --:--"
)
}
}
}
refreshLastSyncTime()
loadCarouselUrls()
}
fun loadCarouselUrls() {
val raw = runCatching {
SystemParamsOperation.getInstance().carouselUrls
}.getOrDefault("")
val urls = raw.split(",").filter { it.isNotBlank() }
_uiState.update { it.copy(carouselUrls = urls) }
}
fun refreshLastSyncTime() {
val time = context.getSharedPreferences("mobpos_prefs", Context.MODE_PRIVATE)
.getLong("last_sync_time", 0L)
.takeIf { it > 0L }
_uiState.update { it.copy(lastSyncTime = time) }
}
fun startSync() {
_uiState.update { it.copy(isSyncing = true, syncStatusText = "Connecting to TMS server...") }
}
fun finishSync(message: String) {
_uiState.update { it.copy(syncStatusText = message) }
viewModelScope.launch {
kotlinx.coroutines.delay(1500)
_uiState.update { it.copy(isSyncing = false) }
}
}
}
private fun PayDetail.toRecord(): TrnxRecord {
val baseType = transType?.takeIf { it.isNotBlank() }?.let { formatTypeLabel(it) } ?: "Transaction"
val type = when {
transactionType == TransactionsType.REVERSAL.value && !baseType.equals("Reversal", ignoreCase = true) ->
"$baseType (Reversal)"
isCanceled -> "Void"
else -> baseType
}
val displayAmount = displayAmount()
val amountText = "MMK ${formatAmount(displayAmount)}"
val card = CardNo?.takeIf { it.length >= 4 }?.let { "**** ${it.takeLast(4)}" } ?: "----"
val dt = buildDateTime(TradeDate, TradeTime)
val approved = tradeAnswerCode == "00" || approvalCode?.isNotBlank() == true
return TrnxRecord(
pid = PID ?: 0L,
typeLabel = type,
amountDisplay = amountText,
maskedCard = card,
dateTime = dt,
isVoided = isCanceled,
isApproved = approved
)
}
fun formatAmount(amount: Long): String {
val scaled = BigDecimal(amount).movePointLeft(2)
return DecimalFormat("#,##0.00").format(scaled)
}
private fun PayDetail.displayAmount(): Long {
if (amount != 0L) return amount
if (transactionType != TransactionsType.SETTLEMENT.value) return 0L
settleDataObj?.let { data ->
return data.saleAmount +
data.preAuthCompAmount +
data.cashAdvanceAmount -
data.refundAmount
}
val parts = settleList.orEmpty().split("-")
if (parts.size >= 4) {
fun partAmount(raw: String): Long = raw.substringAfter(":", "0").toLongOrNull() ?: 0L
val sale = partAmount(parts[0])
val cashAdvance = partAmount(parts[1])
val refund = partAmount(parts[2])
val preAuth = partAmount(parts[3])
return sale + cashAdvance + preAuth - refund
}
return 0L
}
private fun List<PayDetail>.totalSaleCount(): Int = count { it.isSaleTransaction() }
private fun List<PayDetail>.totalSaleAmount(): Long = sumOf {
if (it.isSaleTransaction()) it.amount else 0L
}
private fun PayDetail.isSaleTransaction(): Boolean {
return transactionType == TransactionsType.SALE.value ||
(transactionType == TransactionsType.WAVEPAY.value && qrTransStatus == 1)
}
private fun PayDetail.isSettlementTransaction(): Boolean {
return transactionType == TransactionsType.SETTLEMENT.value ||
transType?.contains("SETTLEMENT", ignoreCase = true) == true
}
private fun formatTypeLabel(raw: String): String = when {
raw.equals("CASH_OUT", ignoreCase = true) -> "CASH_ADV"
raw.contains("SALE", ignoreCase = true) -> "Sale"
raw.contains("VOID", ignoreCase = true) -> "Void"
raw.contains("REFUND", ignoreCase = true) -> "Refund"
raw.contains("SETTLEMENT", ignoreCase = true) -> "Settlement"
raw.contains("PRE", ignoreCase = true) -> "Pre-Auth"
raw.contains("CASH", ignoreCase = true) -> "Cash Adv"
raw.contains("WAVE", ignoreCase = true) -> "Wave Pay"
else -> raw.lowercase().replaceFirstChar { it.uppercaseChar() }
}
// TradeDate = "MMDD", TradeTime = "HHmmss" (ISO 8583 fields 13 & 12)
private fun buildDateTime(date: String?, time: String?): String {
val d = date?.padStart(4, '0')?.takeIf { it.length >= 4 } ?: return "--/-- --:--"
val t = (time ?: "").padStart(6, '0')
return "${d.take(2)}/${d.drop(2).take(2)} ${t.take(2)}:${t.drop(2).take(2)}"
}
private fun PayDetail.isCurrentBusinessSettlement(): Boolean {
if (transactionType != TransactionsType.SETTLEMENT.value) return false
if (TradeDate != SystemDateTime.getMMDD()) return false
val now = Calendar.getInstance()
val cutoff = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 23)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}
return now.before(cutoff)
}

View File

@ -1,139 +0,0 @@
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.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.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextAlign
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.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 Adv", 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,
style = MaterialTheme.typography.titleSmall,
textAlign = TextAlign.Start
)
}
}
}
private data class SeeMoreItem(
val title: String,
val icon: ImageVector
)
@P2Preview
@Composable
private fun PreviewSeeMoreScreen() {
SeeMoreScreen()
}

View File

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

View File

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

View File

@ -1,246 +0,0 @@
package com.mob.utsmyanmar.ui.disable
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.offset
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.CircleShape
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.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.mob.utsmyanmar.R
import com.mob.utsmyanmar.ui.theme.Color
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DisableScreen(
message: String,
onRetry: () -> Unit
) {
Scaffold(
containerColor = Color.IvoryBeige,
topBar = {
CenterAlignedTopAppBar(
title = {
Text(
text = "Terminal Disabled",
color = Color.White,
style = MaterialTheme.typography.titleMedium
)
},
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.LegacyRed
)
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(Modifier.height(40.dp))
// Terminal icon with X badge
Box(
modifier = Modifier
.size(160.dp)
.clip(CircleShape)
.background(Color.LegacyRed.copy(alpha = 0.1f)),
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(R.drawable.ic_terminal),
contentDescription = null,
modifier = Modifier.size(90.dp),
tint = Color.LegacyRed
)
Box(
modifier = Modifier
.size(52.dp)
.align(Alignment.BottomEnd)
.offset(x = (-8).dp, y = (-8).dp)
.clip(CircleShape)
.background(Color.CrimsonRed),
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(R.drawable.ic_cancel_circle),
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = Color.White
)
}
}
Spacer(Modifier.height(24.dp))
Text(
text = "This terminal is disabled",
style = MaterialTheme.typography.titleLarge,
color = Color.LegacyRed,
textAlign = TextAlign.Center
)
Spacer(Modifier.height(8.dp))
Text(
text = "You won't be able to process any\ntransactions until the terminal is enabled.",
style = MaterialTheme.typography.bodyMedium,
color = Color.Gray,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 32.dp)
)
Spacer(Modifier.height(28.dp))
// Info cards
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
// Why card
Card(
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(0.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.Top
) {
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(Color.LegacyRed.copy(alpha = 0.1f)),
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(R.drawable.ic_protection_shield),
contentDescription = null,
modifier = Modifier.size(26.dp),
tint = Color.LegacyRed
)
}
Spacer(Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Why is this terminal disabled?",
style = MaterialTheme.typography.titleSmall
)
Spacer(Modifier.height(4.dp))
Text(
text = message.ifBlank {
"The terminal may be disabled by an administrator due to security, compliance or configuration changes."
},
style = MaterialTheme.typography.bodyMedium,
color = Color.Gray
)
}
}
}
// Help card
Card(
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = Color.GoldenGlow.copy(alpha = 0.35f)
),
elevation = CardDefaults.cardElevation(0.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.Top
) {
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(Color.GoldenGlow.copy(alpha = 0.5f)),
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(R.drawable.ic_alert_triangle),
contentDescription = null,
modifier = Modifier.size(26.dp),
tint = Color.LegacyRed
)
}
Spacer(Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Need help?",
style = MaterialTheme.typography.titleSmall
)
Spacer(Modifier.height(4.dp))
Text(
text = "Contact your system administrator or support team.",
style = MaterialTheme.typography.bodyMedium,
color = Color.Gray
)
}
}
}
}
Spacer(Modifier.height(32.dp))
Button(
onClick = onRetry,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.height(52.dp),
shape = RoundedCornerShape(28.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color.LegacyRed)
) {
Icon(
painter = painterResource(R.drawable.ic_refresh),
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(20.dp)
)
Spacer(Modifier.width(8.dp))
Text(
text = "Retry",
color = Color.White,
style = MaterialTheme.typography.titleMedium
)
}
Spacer(Modifier.height(24.dp))
}
}
}

View File

@ -1,2 +0,0 @@
package com.mob.utsmyanmar.ui.disable

View File

@ -1,2 +0,0 @@
package com.mob.utsmyanmar.ui.disable

View File

@ -1,139 +0,0 @@
package com.mob.utsmyanmar.ui.error
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.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.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Check
import androidx.compose.material.icons.rounded.Close
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.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.mob.utsmyanmar.ui.preview.P2Preview
import com.mob.utsmyanmar.ui.theme.Color
import kotlinx.coroutines.delay
private const val AUTO_DISMISS_DELAY_MS = 3000L
@Composable
fun TransactionErrorScreen(
isSuccess: Boolean,
message: String,
onDone: () -> Unit
) {
BackHandler(enabled = true) {
onDone()
}
LaunchedEffect(Unit) {
delay(AUTO_DISMISS_DELAY_MS)
onDone()
}
TransactionErrorContent(isSuccess= isSuccess, message = message)
}
@Composable
private fun TransactionErrorContent(isSuccess: Boolean,message: String) {
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(72.dp))
FailIcon(isSuccess)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Transaction Failed",
color = Color.LegacyRed,
style = MaterialTheme.typography.headlineLarge
)
Spacer(modifier = Modifier.height(10.dp))
Text(
text = message.ifBlank { "Something went wrong. Please try again." },
color = Color.Gray,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center
)
}
}
}
@Composable
private fun FailIcon(isSuccess: Boolean) {
Box(
modifier = Modifier.size(132.dp),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.size(132.dp)
.background(
color = if (isSuccess) Color.Success.copy(alpha = 0.12f) else Color.Error.copy(alpha = 0.12f),
shape = CircleShape
)
)
Box(
modifier = Modifier
.size(96.dp)
.background(
color = if (isSuccess) Color.Success else Color.Error,
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
if(isSuccess) {
Icon(
imageVector = Icons.Rounded.Check,
contentDescription = "Success",
tint = Color.White,
modifier = Modifier.size(64.dp)
)
} else {
Icon(
imageVector = Icons.Rounded.Close,
contentDescription = "Failed",
tint = Color.White,
modifier = Modifier.size(64.dp)
)
}
}
}
}
@P2Preview
@Composable
private fun PreviewTransactionErrorScreen() {
TransactionErrorContent(isSuccess = true, message = "Reversal Success")
}

View File

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

View File

@ -1,87 +0,0 @@
package com.mob.utsmyanmar.ui.history_functions
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.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.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.unit.dp
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.theme.Color
import com.utsmyanmar.paylibs.utils.POSUtil
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
@Composable
fun BatchTotalScreen(
viewModel: HistoryViewModel,
onBack: () -> Unit,
) {
val batchNo = SystemParamsOperation.getInstance().currentBatchNum
val records by viewModel.getReviewBatchTransaction(batchNo).observeAsState(emptyList())
val totalAmount = records.sumOf { detailReportSignedAmount(it) }
Scaffold(
containerColor = Color.IvoryBeige,
topBar = {
AppBar(
title = "Batch Total",
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(16.dp)) {
SummaryLine("Batch No", batchNo)
SummaryLine("Record Count", records.size.toString())
SummaryLine("Total Amount", POSUtil.getInstance().formatAmount(totalAmount))
}
}
Spacer(modifier = Modifier.height(12.dp))
if (records.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("No batch records found.", color = Color.Gray)
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(records, key = { "${it.voucherNo}_${it.PID}" }) { record ->
HistoryTransactionCard(record, onClick = null)
}
}
}
}
}
}

View File

@ -1,162 +0,0 @@
package com.mob.utsmyanmar.ui.history_functions
import android.widget.Toast
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.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.CircularProgressIndicator
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.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.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.theme.Color
import com.utsmyanmar.paylibs.print.PrintHelper
import com.utsmyanmar.paylibs.print.printx.PrintXReceipt
import com.utsmyanmar.paylibs.print.printx.PrintXStatus
import com.utsmyanmar.paylibs.utils.POSUtil
import com.utsmyanmar.paylibs.utils.enums.HostType
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
import com.utsmyanmar.paylibs.utils.params.Params
@Composable
fun DetailReportScreen(
viewModel: HistoryViewModel,
hostType: HostType,
onBack: () -> Unit,
) {
val context = LocalContext.current
val allTransactions by viewModel.getTransactionHistory().observeAsState(emptyList())
var isPrinting by remember { mutableStateOf(false) }
val records = remember(allTransactions, hostType) {
filterDetailReportTransactions(allTransactions, hostType)
}
val totalAmount = remember(records) {
records.sumOf(::detailReportSignedAmount)
}
fun printDetailReport() {
if (records.isEmpty()) {
Toast.makeText(context, "No transactions to print.", Toast.LENGTH_SHORT).show()
return
}
if (PrintHelper.getInstance().paperRollStatus() != 1) {
Toast.makeText(context, "Paper roll not ready.", Toast.LENGTH_SHORT).show()
return
}
val tradeData = Params.newTrade(false)
val payDetail = tradeData.payDetail
payDetail.transType = TransactionsType.DETAIL_REPORT.name
payDetail.transactionType = TransactionsType.DETAIL_REPORT.value
isPrinting = true
PrintXReceipt.getInstance().printDetailReport(
payDetail,
ArrayList(records),
hostType,
object : PrintXStatus {
override fun onSuccess() {
isPrinting = false
}
override fun onFailure() {
isPrinting = false
Toast.makeText(context, "Print failed.", Toast.LENGTH_SHORT).show()
}
}
)
}
Scaffold(
containerColor = Color.IvoryBeige,
topBar = {
AppBar(
title = "Print Detail Report",
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(16.dp)) {
SummaryLine("Host", hostTypeLabel(hostType))
SummaryLine("Card Type", hostTypeLabel(hostType))
SummaryLine("Transaction Count", records.size.toString())
SummaryLine("Total Amount", POSUtil.getInstance().formatAmount(totalAmount))
}
}
Spacer(modifier = Modifier.height(12.dp))
if (records.isEmpty()) {
Box(
modifier = Modifier.weight(1f),
contentAlignment = Alignment.Center
) {
Text("No transactions available.", color = Color.Gray)
}
} else {
LazyColumn(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(records, key = { "${it.voucherNo}_${it.PID}" }) { record ->
HistoryTransactionCard(record, onClick = null)
}
}
}
Spacer(modifier = Modifier.height(12.dp))
Button(
onClick = ::printDetailReport,
enabled = records.isNotEmpty() && !isPrinting,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.LegacyRed,
contentColor = Color.White
)
) {
if (isPrinting) {
CircularProgressIndicator(color = Color.White)
} else {
Text("Print")
}
}
}
}
}

View File

@ -1,186 +0,0 @@
package com.mob.utsmyanmar.ui.history_functions
import androidx.compose.foundation.clickable
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Backspace
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.mob.utsmyanmar.ui.theme.Color
import com.mob.utsmyanmar.utils.convertDateString
import com.utsmyanmar.paylibs.model.PayDetail
@Composable
internal fun HistoryTransactionCard(
payDetail: PayDetail,
onClick: (() -> Unit)?,
) {
Card(
modifier = Modifier
.fillMaxWidth()
.then(
if (onClick != null) {
Modifier.clickable(onClick = onClick)
} else {
Modifier
}
),
colors = CardDefaults.cardColors(containerColor = Color.White)
) {
Column(modifier = Modifier.padding(16.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = displayTransactionTitle(payDetail),
color = Color.LegacyRed,
style = MaterialTheme.typography.titleSmall
)
Text(
text = displayAmountWithSign(payDetail),
style = MaterialTheme.typography.titleSmall
)
}
Spacer(modifier = Modifier.height(4.dp))
Text("Trace ${payDetail.voucherNo.ifBlank { "-" }}", style = MaterialTheme.typography.bodySmall)
Text("RRN ${payDetail.referNo.ifBlank { "-" }}", style = MaterialTheme.typography.bodySmall, color = Color.Gray)
Text(
"${convertDateString(payDetail.transDate)} ${payDetail.transTime}",
style = MaterialTheme.typography.bodySmall,
color = Color.Gray
)
}
}
}
@Composable
internal fun HistoryTransactionDetailCard(payDetail: PayDetail) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color.White)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = displayTransactionTitle(payDetail),
color = Color.LegacyRed,
style = MaterialTheme.typography.titleMedium
)
SummaryLine("Date", payDetail.tradeDate)
SummaryLine("Time", payDetail.tradeTime)
SummaryLine("Amount", displayAmountWithSign(payDetail))
SummaryLine("Trace No", payDetail.voucherNo)
// SummaryLine("Card No", payDetail.cardNo)
SummaryLine("Reference No", payDetail.referNo)
SummaryLine("Approval Code", payDetail.approvalCode.orEmpty())
SummaryLine("Status", displayTransactionStatus(payDetail))
}
}
}
@Composable
internal fun SummaryLine(label: String, value: String) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(label, color = Color.Gray, style = MaterialTheme.typography.bodyMedium)
Text(
value.ifBlank { "-" },
style = MaterialTheme.typography.bodyMedium
)
}
}
@Composable
internal fun SummaryPickerRow(
label: String,
value: String,
onClick: () -> Unit,
) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick),
colors = CardDefaults.cardColors(containerColor = Color.White)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(label, style = MaterialTheme.typography.titleSmall)
Text(value, color = Color.Gray)
}
}
}
@Composable
internal fun HistoryNumericKeypad(
onDigit: (String) -> Unit,
onBackspace: () -> Unit,
onClear: () -> Unit,
) {
val rows = listOf(
listOf("1", "2", "3"),
listOf("4", "5", "6"),
listOf("7", "8", "9"),
listOf("C", "0", "⌫")
)
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
rows.forEach { row ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
row.forEach { key ->
Button(
onClick = {
when (key) {
"C" -> onClear()
"⌫" -> onBackspace()
else -> onDigit(key)
}
},
modifier = Modifier
.weight(1f)
.height(56.dp),
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.White,
contentColor = Color.Black
)
) {
if (key == "⌫") {
Icon(Icons.Rounded.Backspace, contentDescription = "Backspace")
} else {
Text(key, style = MaterialTheme.typography.headlineMedium)
}
}
}
}
}
}
}

View File

@ -1,161 +0,0 @@
package com.mob.utsmyanmar.ui.history_functions
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.utils.POSUtil
import com.utsmyanmar.paylibs.utils.enums.HostType
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
enum class HistoryHostPurpose {
REPRINT_ANY,
LAST_SETTLEMENT,
DETAIL_REPORT,
SUMMARY_REPORT,
}
internal fun prepareReprintPayDetail(payDetail: PayDetail): PayDetail {
val suffix = "(REPRINT)"
payDetail.transType = if (payDetail.transType.length >= 20) {
"${payDetail.transType}\n$suffix"
} else {
payDetail.transType + suffix
}
return payDetail
}
internal fun displayTransactionStatus(payDetail: PayDetail): String {
if (payDetail.transactionType == TransactionsType.REVERSAL.value) {
return if (payDetail.isNeedReversal) "REVERSAL FAILED" else "REVERSAL SUCCESS"
}
return POSUtil.getInstance().getResponse(payDetail.tradeAnswerCode.orEmpty().ifBlank { "-" })
}
internal fun displayTransactionTitle(payDetail: PayDetail): String {
val title = payDetail.transType
.ifBlank { "TRANSACTION" }
.replace("CASH_OUT", "CASH_ADV", ignoreCase = true)
return if (payDetail.transactionType == TransactionsType.REVERSAL.value) {
"$title (REVERSAL)"
} else {
title
}
}
internal fun isValidForReprint(payDetail: PayDetail): Boolean {
if (payDetail.transactionType == TransactionsType.SETTLEMENT.value) return false
if (payDetail.transactionType == TransactionsType.REVERSAL.value) return false
if (payDetail.transactionType == TransactionsType.WAVEPAY.value && payDetail.qrTransStatus != 1) {
return false
}
if (payDetail.transactionType == TransactionsType.WAVEPAY_REFUND.value && payDetail.qrTransStatus != 1) {
return false
}
return true
}
internal fun filterReprintAnyTransactions(
transactions: List<PayDetail>,
hostType: HostType
): List<PayDetail> {
return transactions.filter { pay ->
if (!isValidForReprint(pay)) return@filter false
when (hostType) {
HostType.MPU ->
pay.accountType == "MPU" &&
pay.transactionType != TransactionsType.WAVEPAY.value &&
pay.transactionType != TransactionsType.WAVEPAY_REFUND.value
HostType.QR ->
(pay.transactionType == TransactionsType.WAVEPAY.value ||
pay.transactionType == TransactionsType.WAVEPAY_REFUND.value) &&
pay.qrTransStatus == 1
HostType.VISA_MASTER ->
pay.accountType != "MPU" &&
pay.transactionType != TransactionsType.WAVEPAY.value &&
pay.transactionType != TransactionsType.WAVEPAY_REFUND.value
}
}
}
internal fun filterDetailReportTransactions(
transactions: List<PayDetail>,
hostType: HostType
): List<PayDetail> {
return transactions.filter { pay ->
if (pay.transactionType == TransactionsType.REVERSAL.value) return@filter false
if (pay.transactionType == TransactionsType.SETTLEMENT.value) return@filter false
when (hostType) {
HostType.MPU ->
pay.accountType == "MPU" &&
pay.transactionType != TransactionsType.WAVEPAY_REFUND.value &&
pay.transactionType != TransactionsType.WAVEPAY.value
HostType.QR ->
(pay.transactionType == TransactionsType.WAVEPAY.value ||
pay.transactionType == TransactionsType.WAVEPAY_REFUND.value) &&
pay.qrTransStatus == 1
HostType.VISA_MASTER ->
pay.accountType != "MPU" &&
pay.transactionType != TransactionsType.WAVEPAY_REFUND.value &&
pay.transactionType != TransactionsType.WAVEPAY.value
}
}
}
internal fun displayAmountWithSign(payDetail: PayDetail): String {
val prefix = if (payDetail.transactionType == TransactionsType.VOID.value ||
payDetail.transactionType == TransactionsType.REFUND.value ||
payDetail.transactionType == TransactionsType.WAVEPAY_REFUND.value ||
payDetail.transactionType == TransactionsType.CASH_OUT.value
) "-" else ""
return prefix + POSUtil.getInstance().formatAmount(payDetail.amount)
}
internal fun detailReportSignedAmount(payDetail: PayDetail): Long {
val isNegative = payDetail.transactionType == TransactionsType.VOID.value ||
payDetail.transactionType == TransactionsType.REFUND.value ||
payDetail.transactionType == TransactionsType.WAVEPAY_REFUND.value ||
payDetail.transactionType == TransactionsType.CASH_OUT.value
return if (isNegative) -payDetail.amount else payDetail.amount
}
internal fun hostTypeLabel(hostType: HostType): String = when (hostType) {
HostType.QR -> "QR PAY"
HostType.MPU -> "MPU"
HostType.VISA_MASTER -> "VISA/MASTER"
}
internal fun hostTypeAccountLabel(hostType: HostType): String = when (hostType) {
HostType.QR -> "WALLET"
HostType.MPU -> "MPU"
HostType.VISA_MASTER -> "Visa/Master"
}
internal fun searchReprintAnyTransactions(
transactions: List<PayDetail>,
query: String
): List<PayDetail> {
if (query.isBlank()) return transactions
return when (query.length) {
6 -> transactions.filter { it.voucherNo == query }
12 -> transactions.filter { it.referNo == query }
else -> transactions.filter {
it.voucherNo.contains(query) || it.referNo.contains(query)
}
}
}
private fun parseTransDate(value: String, formatter: SimpleDateFormat): Date? {
return try {
formatter.parse(value)
} catch (_: ParseException) {
null
}
}

View File

@ -1,78 +0,0 @@
package com.mob.utsmyanmar.ui.history_functions
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.functions.FunctionButton
import com.mob.utsmyanmar.ui.theme.Color
@Composable
fun HistoryMenuScreen(
onBack: () -> Unit,
onReprintLastTransaction: () -> Unit,
onReprintAnyTransaction: () -> Unit,
onReprintLastSettlement: () -> Unit,
onPrintDetailReport: () -> Unit,
onPrintSummaryReport: () -> Unit,
onReview: () -> Unit,
onBatchTotal: () -> Unit,
) {
Scaffold(
containerColor = Color.IvoryBeige,
topBar = {
AppBar(
title = "Select Function",
icon = Icons.AutoMirrored.Filled.ArrowBack,
onIconClick = onBack
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
HistoryMenuButton("Reprint Last Transaction", onReprintLastTransaction)
HistoryMenuButton("Reprint Any Transaction", onReprintAnyTransaction)
HistoryMenuButton("Reprint Last Settlement Report", onReprintLastSettlement)
HistoryMenuButton("Print Detail Report", onPrintDetailReport)
HistoryMenuButton("Print Summary Report", onPrintSummaryReport)
HistoryMenuButton("Review", onReview)
HistoryMenuButton("Batch Total", onBatchTotal)
}
}
}
@Composable
private fun HistoryMenuButton(
title: String,
onClick: () -> Unit,
) {
FunctionButton(
onClick = onClick,
title = title,
subTitle = "",
trailingIcon = {
Icon(
imageVector = Icons.Default.ChevronRight,
contentDescription = null,
tint = Color.LegacyRed
)
}
)
}

View File

@ -1,34 +0,0 @@
package com.mob.utsmyanmar.ui.history_functions
import android.content.Context
import android.widget.Toast
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.print.PrintHelper
import com.utsmyanmar.paylibs.print.printx.PrintXReceipt
import com.utsmyanmar.paylibs.print.printx.PrintXStatus
internal fun printSettlementReport(
context: Context,
payDetail: PayDetail,
onSuccess: () -> Unit,
onFailure: () -> Unit,
) {
if (PrintHelper.getInstance().paperRollStatus() != 1) {
Toast.makeText(context, "Paper roll not ready.", Toast.LENGTH_SHORT).show()
onFailure()
return
}
PrintXReceipt.getInstance().printSmileSettlementReport(
payDetail,
object : PrintXStatus {
override fun onSuccess() {
onSuccess()
}
override fun onFailure() {
onFailure()
}
}
)
}

View File

@ -1,180 +0,0 @@
package com.mob.utsmyanmar.ui.history_functions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.hilt.navigation.compose.hiltViewModel
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.utsmyanmar.paylibs.model.PayDetail
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.enums.TransMenu
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
private sealed interface HistoryDestination {
data object Menu : HistoryDestination
data class SelectHost(val purpose: HistoryHostPurpose) : HistoryDestination
data class ReprintAny(val hostType: HostType) : HistoryDestination
data class DetailReport(val hostType: HostType) : HistoryDestination
data class SummaryReport(val hostType: HostType) : HistoryDestination
data object ReviewTrace : HistoryDestination
data class ReviewDetail(val trace: String) : HistoryDestination
data object BatchTotal : HistoryDestination
data object ReprintSettlement : HistoryDestination
data object NotFound : HistoryDestination
}
@Composable
fun HistoryRoute(
sharedViewModel: SharedViewModel,
onBack: () -> Unit,
onNavigatePrintReceipt: () -> Unit,
viewModel: HistoryViewModel = hiltViewModel(),
) {
var destination by remember { mutableStateOf<HistoryDestination>(HistoryDestination.Menu) }
var hostPurpose by remember { mutableStateOf(HistoryHostPurpose.REPRINT_ANY) }
val voucherNo = SystemParamsOperation.getInstance().lastSuccessTrnx
val lastTransactions by viewModel.getLastTransaction(voucherNo).observeAsState(emptyList())
fun navigateToHostSelection(purpose: HistoryHostPurpose) {
hostPurpose = purpose
destination = HistoryDestination.SelectHost(purpose)
}
fun handleHostSelected(hostType: HostType) {
sharedViewModel.hostType.value = hostType
destination = when (hostPurpose) {
HistoryHostPurpose.REPRINT_ANY ->
HistoryDestination.ReprintAny(hostType)
HistoryHostPurpose.LAST_SETTLEMENT -> {
if (hostType == HostType.MPU) {
HistoryDestination.ReprintSettlement
} else {
HistoryDestination.NotFound
}
}
HistoryHostPurpose.DETAIL_REPORT ->
HistoryDestination.DetailReport(hostType)
HistoryHostPurpose.SUMMARY_REPORT ->
HistoryDestination.SummaryReport(hostType)
}
}
fun reprintTransaction(payDetail: PayDetail) {
sharedViewModel.setTransMenu(TransMenu.REPRINT)
sharedViewModel.isReprint.value = true
sharedViewModel.payDetail.value = payDetail
sharedViewModel.setPrintStatus(PrintStatus.FIRST_PRINT)
onNavigatePrintReceipt()
}
fun reprintLastTransaction() {
sharedViewModel.setTransMenu(TransMenu.REPRINT)
if (lastTransactions.isEmpty()) {
destination = HistoryDestination.NotFound
return
}
val payDetail = lastTransactions.last()
if (!isValidForReprint(payDetail)) {
destination = HistoryDestination.NotFound
return
}
reprintTransaction(prepareReprintPayDetail(payDetail))
}
when (val current = destination) {
HistoryDestination.Menu -> HistoryMenuScreen(
onBack = onBack,
onReprintLastTransaction = ::reprintLastTransaction,
onReprintAnyTransaction = {
sharedViewModel.setTransMenu(TransMenu.TRANSACTIONS)
sharedViewModel.transactionsType.value = null
navigateToHostSelection(HistoryHostPurpose.REPRINT_ANY)
},
onReprintLastSettlement = {
sharedViewModel.setTransMenu(TransMenu.LAST_SETTLEMENT)
navigateToHostSelection(HistoryHostPurpose.LAST_SETTLEMENT)
},
onPrintDetailReport = {
sharedViewModel.setTransMenu(TransMenu.DETAIL_REPORT)
navigateToHostSelection(HistoryHostPurpose.DETAIL_REPORT)
},
onPrintSummaryReport = {
sharedViewModel.setTransMenu(TransMenu.SUMMARY_REPORT)
navigateToHostSelection(HistoryHostPurpose.SUMMARY_REPORT)
},
onReview = {
sharedViewModel.setTransMenu(TransMenu.REVIEW)
sharedViewModel.transactionsType.value = TransactionsType.REVIEW_TRANS
destination = HistoryDestination.ReviewTrace
},
onBatchTotal = {
sharedViewModel.setTransMenu(TransMenu.REVIEW_BATCH)
destination = HistoryDestination.BatchTotal
}
)
is HistoryDestination.SelectHost -> SelectHostScreen(
onBack = { destination = HistoryDestination.Menu },
onSelectHost = ::handleHostSelected
)
is HistoryDestination.ReprintAny -> ReprintAnyTransactionScreen(
viewModel = viewModel,
hostType = current.hostType,
onBack = { destination = HistoryDestination.Menu },
onReprintTransaction = ::reprintTransaction
)
is HistoryDestination.DetailReport -> DetailReportScreen(
viewModel = viewModel,
hostType = current.hostType,
onBack = { destination = HistoryDestination.Menu }
)
is HistoryDestination.SummaryReport -> SummaryReportScreen(
viewModel = viewModel,
hostType = current.hostType,
onBack = { destination = HistoryDestination.Menu }
)
HistoryDestination.ReviewTrace -> ReviewTraceScreen(
viewModel = viewModel,
onBack = { destination = HistoryDestination.Menu },
onNavigateReviewDetail = { trace ->
destination = HistoryDestination.ReviewDetail(trace)
}
)
is HistoryDestination.ReviewDetail -> ReviewDetailScreen(
viewModel = viewModel,
trace = current.trace,
onBack = { destination = HistoryDestination.ReviewTrace }
)
HistoryDestination.BatchTotal -> BatchTotalScreen(
viewModel = viewModel,
onBack = { destination = HistoryDestination.Menu }
)
HistoryDestination.ReprintSettlement -> ReprintSettlementScreen(
viewModel = viewModel,
onBack = { destination = HistoryDestination.Menu },
onDone = onBack
)
HistoryDestination.NotFound -> TransactionNotFoundScreen(
onBack = { destination = HistoryDestination.Menu }
)
}
}

View File

@ -1,113 +0,0 @@
package com.mob.utsmyanmar.ui.history_functions
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.model.SettleData
import com.utsmyanmar.paylibs.utils.enums.HostType
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
internal fun formatTime(hour: String, minute: String): String {
if (hour.isBlank() || minute.isBlank()) return "Select"
return "$hour:$minute"
}
internal class SummaryCounter {
var count = 0
var amount = 0L
fun inc(value: Long) {
count++
amount += value
}
}
internal fun buildSummarySettleData(
transactions: List<PayDetail>,
hostType: HostType,
start: Date,
end: Date,
): SettleData {
val dateTimeFormat = SimpleDateFormat("dd/MM/yy HH:mm", Locale.getDefault())
val wave = SummaryCounter()
val waveRefund = SummaryCounter()
val voidTran = SummaryCounter()
val sale = SummaryCounter()
val ca = SummaryCounter()
val preAuth = SummaryCounter()
val preAuthVoid = SummaryCounter()
val preAuthComp = SummaryCounter()
val preAuthCompVoid = SummaryCounter()
val refund = SummaryCounter()
for (pay in transactions) {
val currentDate = try {
dateTimeFormat.parse("${pay.transDate} ${pay.transTime}")
} catch (_: ParseException) {
continue
} ?: continue
if (currentDate.before(start) || currentDate.after(end)) continue
when (hostType) {
HostType.QR -> when (pay.transactionType) {
TransactionsType.WAVEPAY.value ->
if (pay.qrTransStatus == 1) wave.inc(pay.amount)
TransactionsType.WAVEPAY_REFUND.value ->
if (pay.qrTransStatus == 1) waveRefund.inc(pay.amount)
}
HostType.MPU -> if (pay.accountType == "MPU") {
accumulateCardSummary(
pay, sale, voidTran, ca, preAuth, preAuthVoid, preAuthComp, preAuthCompVoid, refund
)
}
HostType.VISA_MASTER -> if (pay.accountType != "MPU") {
accumulateCardSummary(
pay, sale, voidTran, ca, preAuth, preAuthVoid, preAuthComp, preAuthCompVoid, refund
)
}
}
}
return SettleData(
sale.count, sale.amount,
wave.count, wave.amount,
voidTran.count, voidTran.amount,
ca.count, ca.amount,
preAuth.count, preAuth.amount,
preAuthVoid.count, preAuthVoid.amount,
preAuthComp.count, preAuthComp.amount,
preAuthCompVoid.count, preAuthCompVoid.amount,
refund.count, refund.amount,
waveRefund.count, waveRefund.amount
)
}
private fun accumulateCardSummary(
pay: PayDetail,
sale: SummaryCounter,
voidTran: SummaryCounter,
ca: SummaryCounter,
preAuth: SummaryCounter,
preAuthVoid: SummaryCounter,
preAuthComp: SummaryCounter,
preAuthCompVoid: SummaryCounter,
refund: SummaryCounter,
) {
when (pay.transactionType) {
TransactionsType.VOID.value -> voidTran.inc(pay.amount)
TransactionsType.SALE.value -> sale.inc(pay.amount)
TransactionsType.CASH_OUT.value -> ca.inc(pay.amount)
TransactionsType.PRE_AUTH_SALE.value -> preAuth.inc(pay.amount)
TransactionsType.PRE_AUTH_VOID.value -> preAuthVoid.inc(pay.amount)
TransactionsType.PRE_AUTH_COMPLETE.value -> preAuthComp.inc(pay.amount)
TransactionsType.PRE_AUTH_COMPLETE_VOID.value -> preAuthCompVoid.inc(pay.amount)
TransactionsType.REFUND.value -> refund.inc(pay.amount)
}
}

View File

@ -1,32 +0,0 @@
package com.mob.utsmyanmar.ui.history_functions
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import com.utsmyanmar.baselib.repo.Repository
import com.utsmyanmar.paylibs.model.PayDetail
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class HistoryViewModel @Inject constructor(
private val repository: Repository
) : ViewModel() {
fun getTransactionHistory(): LiveData<List<PayDetail>> =
repository.getTransactionHistory()
fun getLastTransaction(voucherNo: String): LiveData<List<PayDetail>> =
repository.getLastTransaction(voucherNo)
fun getLastSettlement(voucherNo: String): LiveData<List<PayDetail>> =
repository.getLastSettlement(voucherNo)
fun getReviewBatchTransaction(batchNo: String): LiveData<List<PayDetail>> =
repository.getReviewBatchTransaction(batchNo)
fun getSmileSummaryTrans(batchNo: String): LiveData<List<PayDetail>> =
repository.getSmileSummaryTrans(batchNo)
fun searchTransaction(voucherNo: String): LiveData<PayDetail> =
repository.searchTransaction(voucherNo)
}

View File

@ -1,263 +0,0 @@
package com.mob.utsmyanmar.ui.history_functions
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.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.automirrored.rounded.Backspace
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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.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.NumericKeypad
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.ui.theme.MOBPOSTheme
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.utils.enums.HostType
private val searchKeys = listOf(
listOf("1", "2", "3"),
listOf("4", "5", "6"),
listOf("7", "8", "9"),
listOf("C", "0", "\u232B")
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReprintAnyTransactionScreen(
viewModel: HistoryViewModel,
hostType: HostType,
onBack: () -> Unit,
onReprintTransaction: (PayDetail) -> Unit,
) {
val allTransactions by viewModel.getTransactionHistory().observeAsState(emptyList())
var searchQuery by remember { mutableStateOf("") }
var isFocused by remember { mutableStateOf(false) }
val filtered = remember(allTransactions, hostType, searchQuery) {
val base = filterReprintAnyTransactions(allTransactions, hostType)
searchReprintAnyTransactions(base, searchQuery.trim())
}
Scaffold(
containerColor = Color.IvoryBeige,
topBar = {
TopAppBar(
title = {
Column {
Text(
text = "Reprint Transaction",
style = MaterialTheme.typography.titleMedium
)
Text(
text = hostTypeLabel(hostType),
style = MaterialTheme.typography.labelSmall,
color = Color.Gray
)
}
},
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
tint = Color.LegacyRed
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.IvoryBeige
)
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(horizontal = 16.dp)
) {
SearchInputBox(
query = searchQuery,
isFocused = isFocused,
onFocus = { isFocused = true }
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "${filtered.size} transaction(s) found",
style = MaterialTheme.typography.labelSmall,
color = Color.Gray,
modifier = Modifier.padding(horizontal = 4.dp)
)
Spacer(modifier = Modifier.height(8.dp))
if (filtered.isEmpty()) {
Box(
modifier = Modifier.weight(1f),
contentAlignment = Alignment.Center
) {
Text(
text = if (allTransactions.isEmpty()) "No transactions available" else "No matching transactions",
color = Color.Gray,
style = MaterialTheme.typography.bodyMedium
)
}
} else {
LazyColumn(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(filtered, key = { "${it.voucherNo}_${it.referNo}_${it.PID}" }) { payDetail ->
HistoryTransactionCard(payDetail) {
isFocused = false
onReprintTransaction(prepareReprintPayDetail(payDetail))
}
}
}
}
AnimatedVisibility(
visible = isFocused,
enter = slideInVertically(initialOffsetY = { it }),
exit = slideOutVertically(targetOffsetY = { it })
) {
NumericKeypad(
keys = searchKeys,
modifier = Modifier
.fillMaxWidth()
.height(280.dp)
.padding(top = 8.dp),
onKeyClick = { key ->
when (key) {
"C" -> searchQuery = ""
"\u232B" -> searchQuery = searchQuery.dropLast(1)
else -> if (searchQuery.length < 12) searchQuery += key
}
}
)
}
}
}
}
@Composable
private fun SearchInputBox(
query: String,
isFocused: Boolean,
onFocus: () -> Unit,
) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onFocus),
colors = CardDefaults.cardColors(containerColor = Color.White),
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(defaultElevation = if (isFocused) 6.dp else 2.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = if (isFocused) "Trace or RRN" else "Tap to search by Trace or RRN",
style = MaterialTheme.typography.bodySmall,
color = if (isFocused) Color.LegacyRed else Color.Gray
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = query.ifEmpty { "_" },
color = Color.LegacyRed,
style = MaterialTheme.typography.headlineSmall.copy(
fontWeight = FontWeight.Bold,
letterSpacing = 2.sp
)
)
}
if (query.isNotEmpty()) {
Card(
shape = RoundedCornerShape(18.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
modifier = Modifier
.shadow(2.dp, RoundedCornerShape(18.dp))
.clickable { }
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.Backspace,
contentDescription = "Delete",
tint = Color.LegacyRed,
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
)
}
}
}
}
}
@P2Preview
@P3Preview
@Composable
private fun ReprintAnyTransactionScreenPreview() {
MOBPOSTheme {
Box(modifier = Modifier.height(600.dp)) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color.White)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "SALE",
color = Color.LegacyRed,
style = MaterialTheme.typography.titleSmall
)
Spacer(modifier = Modifier.height(4.dp))
Text("Trace 000001", style = MaterialTheme.typography.bodySmall)
Text("RRN 123456789012", style = MaterialTheme.typography.bodySmall, color = Color.Gray)
}
}
}
}
}

View File

@ -1,103 +0,0 @@
package com.mob.utsmyanmar.ui.history_functions
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
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.platform.LocalContext
import androidx.compose.ui.unit.dp
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.core_utils.SystemParamsOperation
@Composable
fun ReprintSettlementScreen(
viewModel: HistoryViewModel,
onBack: () -> Unit,
onDone: () -> Unit,
) {
val context = LocalContext.current
val voucherNo = SystemParamsOperation.getInstance().currentSerialNum
val settlements by viewModel.getLastSettlement(voucherNo).observeAsState(emptyList())
var payDetail by remember { mutableStateOf<PayDetail?>(null) }
var hasStarted by remember { mutableStateOf(false) }
var printFailed by remember { mutableStateOf(false) }
LaunchedEffect(settlements) {
if (hasStarted) return@LaunchedEffect
if (settlements.isEmpty()) {
Toast.makeText(context, "No transactions to print.", Toast.LENGTH_SHORT).show()
onBack()
return@LaunchedEffect
}
hasStarted = true
val detail = prepareReprintPayDetail(settlements.last())
payDetail = detail
printSettlementReport(context, detail, onSuccess = onDone) {
printFailed = true
}
}
Scaffold(
containerColor = Color.IvoryBeige,
topBar = {
AppBar(title = "Printing Settlement Report")
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
if (printFailed) {
Text("Printing settlement report failed.", color = Color.LegacyRed)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = {
payDetail?.let { detail ->
printSettlementReport(context, detail, onSuccess = onDone) {
printFailed = true
}
}
},
colors = ButtonDefaults.buttonColors(
containerColor = Color.LegacyRed,
contentColor = Color.White
)
) {
Text("Retry")
}
Spacer(modifier = Modifier.height(8.dp))
TextButton(onClick = onDone) {
Text("Done")
}
} else {
CircularProgressIndicator(color = Color.LegacyRed)
Spacer(modifier = Modifier.height(16.dp))
Text("Printing settlement report...")
}
}
}
}

View File

@ -1,54 +0,0 @@
package com.mob.utsmyanmar.ui.history_functions
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
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.unit.dp
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.theme.Color
@Composable
fun ReviewDetailScreen(
viewModel: HistoryViewModel,
trace: String,
onBack: () -> Unit,
) {
val transaction by viewModel.searchTransaction(trace).observeAsState()
Scaffold(
containerColor = Color.IvoryBeige,
topBar = {
AppBar(
title = "Transaction Info",
icon = Icons.AutoMirrored.Filled.ArrowBack,
onIconClick = onBack
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
) {
transaction?.let { payDetail ->
HistoryTransactionDetailCard(payDetail)
} ?: Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Transaction not found.", color = Color.Gray)
}
}
}
}

View File

@ -1,141 +0,0 @@
package com.mob.utsmyanmar.ui.history_functions
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.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.LaunchedEffect
import androidx.compose.runtime.getValue
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.material3.MaterialTheme
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.theme.Color
import com.utsmyanmar.paylibs.model.PayDetail
@Composable
fun ReviewTraceScreen(
viewModel: HistoryViewModel,
onBack: () -> Unit,
onNavigateReviewDetail: (String) -> Unit,
) {
var traceNumber by remember { mutableStateOf("") }
var searchedTrace by remember { mutableStateOf("") }
var lastNavigatedTrace by remember { mutableStateOf<String?>(null) }
val transactionSource: LiveData<PayDetail?> = remember(searchedTrace) {
if (searchedTrace.isBlank()) {
MutableLiveData(null)
} else {
@Suppress("UNCHECKED_CAST")
viewModel.searchTransaction(searchedTrace) as LiveData<PayDetail?>
}
}
val transaction by transactionSource.observeAsState()
LaunchedEffect(transaction?.voucherNo) {
val matchedTrace = transaction?.voucherNo ?: return@LaunchedEffect
if (lastNavigatedTrace == matchedTrace) return@LaunchedEffect
lastNavigatedTrace = matchedTrace
onNavigateReviewDetail(matchedTrace)
}
Scaffold(
containerColor = Color.IvoryBeige,
topBar = {
AppBar(
title = "Review",
icon = Icons.AutoMirrored.Filled.ArrowBack,
onIconClick = onBack
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
) {
Text(
text = if (searchedTrace.isNotBlank() && transaction == null) {
"No transaction found for trace $searchedTrace."
} else {
"Input trace number to review transaction."
},
color = if (searchedTrace.isNotBlank() && transaction == null) {
Color.LegacyRed
} else {
Color.Black
},
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(16.dp))
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color.White)
) {
Text(
text = traceNumber.padStart(6, '0').ifEmpty { "000000" },
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.displayMedium.copy(fontSize = 28.sp)
)
}
Spacer(modifier = Modifier.height(16.dp))
HistoryNumericKeypad(
onDigit = { digit ->
if (traceNumber.length < 6) traceNumber += digit
},
onBackspace = {
if (traceNumber.isNotEmpty()) {
traceNumber = traceNumber.dropLast(1)
}
},
onClear = { traceNumber = "" }
)
Spacer(modifier = Modifier.weight(1f))
Button(
onClick = { searchedTrace = traceNumber.padStart(6, '0') },
enabled = traceNumber.isNotBlank(),
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.LegacyRed,
contentColor = Color.White
)
) {
Text("Search")
}
}
}
}

View File

@ -1,98 +0,0 @@
package com.mob.utsmyanmar.ui.history_functions
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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.AlertDialog
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
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.Modifier
import androidx.compose.ui.unit.dp
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.theme.Color
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import com.utsmyanmar.paylibs.utils.enums.HostType
@Composable
fun SelectHostScreen(
onBack: () -> Unit,
onSelectHost: (HostType) -> Unit,
) {
var unavailableMessage by remember { mutableStateOf<String?>(null) }
Scaffold(
containerColor = Color.IvoryBeige,
topBar = {
AppBar(
title = "Select Host",
icon = Icons.AutoMirrored.Filled.ArrowBack,
onIconClick = onBack
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
HostButton("MPU") { onSelectHost(HostType.MPU) }
HostButton("Visa / Master") {
if (SystemParamsOperation.getInstance().isEmvEnabled) {
onSelectHost(HostType.VISA_MASTER)
} else {
unavailableMessage = "Currently this menu is unavailable!"
}
}
HostButton("QR Pay") { onSelectHost(HostType.QR) }
}
}
unavailableMessage?.let { message ->
AlertDialog(
onDismissRequest = { unavailableMessage = null },
confirmButton = {
TextButton(onClick = { unavailableMessage = null }) {
Text("OK")
}
},
text = { Text(message) }
)
}
}
@Composable
private fun HostButton(
label: String,
onClick: () -> Unit,
) {
ElevatedButton(
onClick = onClick,
shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.White,
contentColor = Color.Black
),
modifier = Modifier
.fillMaxWidth()
.height(64.dp)
) {
Text(label, style = MaterialTheme.typography.titleMedium)
}
}

View File

@ -1,220 +0,0 @@
package com.mob.utsmyanmar.ui.history_functions
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import androidx.compose.foundation.layout.Arrangement
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.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.theme.Color
import com.utsmyanmar.paylibs.print.PrintHelper
import com.utsmyanmar.paylibs.print.printx.PrintXReceipt
import com.utsmyanmar.paylibs.print.printx.PrintXStatus
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import com.utsmyanmar.paylibs.utils.enums.HostType
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
import com.utsmyanmar.paylibs.utils.params.Params
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
@Composable
fun SummaryReportScreen(
viewModel: HistoryViewModel,
hostType: HostType,
onBack: () -> Unit,
) {
val context = LocalContext.current
val calendarStart = remember { Calendar.getInstance() }
val calendarEnd = remember { Calendar.getInstance() }
var startDate by remember { mutableStateOf("") }
var endDate by remember { mutableStateOf("") }
var startHour by remember { mutableStateOf("") }
var startMinute by remember { mutableStateOf("") }
var endHour by remember { mutableStateOf("") }
var endMinute by remember { mutableStateOf("") }
var isPrinting by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
val allTransactions by viewModel.getSmileSummaryTrans(
SystemParamsOperation.getInstance().currentBatchNum
).observeAsState(emptyList())
fun showDatePicker(isStart: Boolean) {
val calendar = if (isStart) calendarStart else calendarEnd
DatePickerDialog(
context,
{ _, year, month, day ->
calendar.set(year, month, day)
val formatted = SimpleDateFormat("dd/MM/yy", Locale.getDefault())
.format(calendar.time)
if (isStart) startDate = formatted else endDate = formatted
},
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH),
calendar.get(Calendar.DAY_OF_MONTH)
).show()
}
fun showTimePicker(isStart: Boolean) {
val calendar = if (isStart) calendarStart else calendarEnd
TimePickerDialog(
context,
{ _, hour, minute ->
calendar.set(Calendar.HOUR_OF_DAY, hour)
calendar.set(Calendar.MINUTE, minute)
val hr = String.format(Locale.getDefault(), "%02d", hour)
val min = String.format(Locale.getDefault(), "%02d", minute)
if (isStart) {
startHour = hr
startMinute = min
} else {
endHour = hr
endMinute = min
}
},
calendar.get(Calendar.HOUR_OF_DAY),
calendar.get(Calendar.MINUTE),
false
).show()
}
fun printSummaryReport() {
if (startDate.isBlank() || endDate.isBlank() ||
startHour.isBlank() || startMinute.isBlank() ||
endHour.isBlank() || endMinute.isBlank()
) {
errorMessage = "Please select date and time!"
return
}
val pattern = "dd/MM/yy HH:mm"
val formatter = SimpleDateFormat(pattern, Locale.getDefault())
val start: Date
val end: Date
try {
start = formatter.parse("$startDate $startHour:$startMinute") ?: return
end = formatter.parse("$endDate $endHour:$endMinute") ?: return
} catch (_: ParseException) {
errorMessage = "Invalid date or time."
return
}
if (start.after(end)) {
errorMessage = "Invalid end date!"
return
}
val settleData = buildSummarySettleData(allTransactions, hostType, start, end)
if (settleData.saleCount + settleData.wavePayCount +
settleData.voidSaleCount + settleData.cashAdvanceCount +
settleData.preAuthCount + settleData.refundCount + settleData.waveRefundCount == 0
) {
errorMessage = "No transactions to print."
return
}
if (PrintHelper.getInstance().paperRollStatus() != 1) {
errorMessage = "Paper roll not ready."
return
}
val tradeData = Params.newTrade(false)
val payDetail = tradeData.payDetail
payDetail.settleDataObj = settleData
payDetail.transType = TransactionsType.SUMMARY_REPORT.name
payDetail.transactionType = TransactionsType.SUMMARY_REPORT.value
payDetail.accountType = hostTypeAccountLabel(hostType)
isPrinting = true
PrintXReceipt.getInstance().printSmileSummaryReport(
payDetail,
object : PrintXStatus {
override fun onSuccess() {
isPrinting = false
}
override fun onFailure() {
isPrinting = false
errorMessage = "Print failed."
}
}
)
}
Scaffold(
containerColor = Color.IvoryBeige,
topBar = {
AppBar(
title = "Transaction Summary",
icon = Icons.AutoMirrored.Filled.ArrowBack,
onIconClick = onBack
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
SummaryPickerRow("Start Date", startDate.ifBlank { "Select" }) { showDatePicker(true) }
SummaryPickerRow("Start Time", formatTime(startHour, startMinute)) { showTimePicker(true) }
SummaryPickerRow("End Date", endDate.ifBlank { "Select" }) { showDatePicker(false) }
SummaryPickerRow("End Time", formatTime(endHour, endMinute)) { showTimePicker(false) }
Spacer(modifier = Modifier.weight(1f))
Button(
onClick = ::printSummaryReport,
enabled = !isPrinting,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.LegacyRed,
contentColor = Color.White
)
) {
Text(if (isPrinting) "Printing..." else "Print")
}
}
}
errorMessage?.let { message ->
AlertDialog(
onDismissRequest = { errorMessage = null },
confirmButton = {
TextButton(onClick = { errorMessage = null }) {
Text("OK")
}
},
text = { Text(message) }
)
}
}

View File

@ -1,42 +0,0 @@
package com.mob.utsmyanmar.ui.history_functions
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
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 com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.theme.Color
@Composable
fun TransactionNotFoundScreen(onBack: () -> Unit) {
Scaffold(
containerColor = Color.IvoryBeige,
topBar = {
AppBar(
title = "Transaction Info",
icon = Icons.AutoMirrored.Filled.ArrowBack,
onIconClick = onBack
)
}
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = Alignment.Center
) {
Text(
text = "Transaction not found.",
color = Color.Gray,
style = MaterialTheme.typography.bodyLarge
)
}
}
}

View File

@ -1,89 +0,0 @@
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 adv" -> TransactionConfig(
transactionName = "CASH_OUT",
transactionType = TransactionsType.CASH_OUT,
processCode = ProcessCode.CASH_ADVANCE + ProcessCode.SMART + ProcessCode.TO_ACCOUNT
)
else -> TransactionConfig(
transactionName = "SALE",
transactionType = TransactionsType.SALE,
processCode = ProcessCode.SALE_PURCHASE + ProcessCode.SMART + ProcessCode.TO_ACCOUNT
)
}
}

View File

@ -1,254 +0,0 @@
package com.mob.utsmyanmar.ui.input_amount
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.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.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.mob.utsmyanmar.ui.components.NumericKeypad
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.preview.P2Preview
import com.mob.utsmyanmar.ui.preview.P3Preview
import com.mob.utsmyanmar.ui.theme.Color
private val amountKeys = listOf(
listOf("1", "2", "3"),
listOf("4", "5", "6"),
listOf("7", "8", "9"),
listOf(".", "0", "00")
)
@Composable
fun InputAmount(
title: String = "Amount",
onBackClick: () -> Unit = {},
onChargeClick: (String) -> Unit = {}
) {
var amount by remember { mutableStateOf("") }
val prefixLabel = "MMK"
val supportingText = "Enter the amount to continue"
Scaffold(
topBar = {
AppBar(
title = "Amount",
icon = Icons.AutoMirrored.Rounded.ArrowBack,
onIconClick = onBackClick
)
},
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,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = formatAmount(amount.ifEmpty { "0" }),
color = Color.LegacyRed,
style = MaterialTheme.typography.displayLarge
)
if (prefixLabel.isNotBlank()) {
Text(
text = prefixLabel,
color = Color.LegacyRed,
style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.Medium),
modifier = Modifier.padding(end = 10.dp, bottom = 6.dp)
)
}
Spacer(modifier = Modifier.height(14.dp))
Text(
text = supportingText,
color = Color.Gray,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.weight(3f),
verticalArrangement = Arrangement.Bottom
) {
NumericKeypad(
keys = amountKeys,
modifier = Modifier.fillMaxSize(),
onKeyClick = { value ->
amount = appendAmountValue(amount, value)
}
)
}
Spacer(Modifier.height(16.dp))
//third container
Box(
modifier = Modifier
.fillMaxWidth()
.weight(0.5f)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Button(
onClick = onBackClick,
modifier = Modifier
.weight(1f)
.height(56.dp),
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.White,
contentColor = Color.LegacyRed
)
) {
Text("Cancel")
}
val isAmountValid = amount.toDoubleOrNull()?.let { it > 0.0 } == true
Button(
onClick = {
onChargeClick(amount)
},
modifier = Modifier
.weight(1f)
.height(56.dp),
enabled = isAmountValid,
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",
style = MaterialTheme.typography.titleSmall
)
}
}
}
}
}
}
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.00"
}
val decimalPart = normalized.substringAfter(".", "")
return if (normalized.endsWith(".")) {
"$groupedWholePart.00"
} else {
val padded = decimalPart.padEnd(2, '0').take(2)
"$groupedWholePart.$padded"
}
}
@P2Preview
@P3Preview
@Composable
fun PreviewInputAmount() {
InputAmount();
}

View File

@ -1,426 +0,0 @@
package com.mob.utsmyanmar.ui.manual_entry
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable
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.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.automirrored.rounded.Backspace
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.mob.utsmyanmar.ui.components.NumericKeypad
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.pinpad.PinPadViewModel
import com.mob.utsmyanmar.ui.preview.P2Preview
import com.mob.utsmyanmar.ui.theme.Color
import com.mob.utsmyanmar.utils.TransactionUtil
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
import com.utsmyanmar.checkxread.model.CardDataX
import com.utsmyanmar.checkxread.util.CardTypeX
import com.utsmyanmar.paylibs.utils.POSUtil
private const val PAN_RAW_MAX = 19
private const val PAN_RAW_MIN = 13
private const val EXPIRY_RAW_MAX = 4
private enum class ManualEntryStep {
CARD_NUMBER,
EXPIRY
}
private val digitKeys = listOf(
listOf("1", "2", "3"),
listOf("4", "5", "6"),
listOf("7", "8", "9"),
listOf("", "0", "")
)
@Composable
fun InputManualCardRoute(
transProcessViewModel: TransProcessViewModel,
sharedViewModel: SharedViewModel,
pinPadViewModel: PinPadViewModel,
onBack: () -> Unit,
onNavigatePinPad: () -> Unit
) {
var step by rememberSaveable { mutableStateOf(ManualEntryStep.CARD_NUMBER) }
var pan by rememberSaveable { mutableStateOf("") }
var expiry by rememberSaveable { mutableStateOf("") }
var errorMessage by rememberSaveable { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
step = ManualEntryStep.CARD_NUMBER
pan = ""
expiry = ""
errorMessage = null
}
fun goBack() {
when (step) {
ManualEntryStep.EXPIRY -> {
step = ManualEntryStep.CARD_NUMBER
expiry = ""
errorMessage = null
}
ManualEntryStep.CARD_NUMBER -> onBack()
}
}
BackHandler { goBack() }
fun confirmCardNumber() {
errorMessage = null
if (pan.length < PAN_RAW_MIN || pan.length > PAN_RAW_MAX) {
errorMessage = "Invalid card number"
return
}
if (!luhnCheck(pan)) {
errorMessage = "Invalid card number"
return
}
step = ManualEntryStep.EXPIRY
errorMessage = null
}
fun confirmExpiry() {
errorMessage = null
if (expiry.length != EXPIRY_RAW_MAX) {
errorMessage = "Enter MM/YY"
return
}
val month = expiry.take(2).toIntOrNull() ?: 0
if (month < 1 || month > 12) {
errorMessage = "Invalid month"
return
}
val expDate = POSUtil.getInstance().getISOFormatExpDate(expiry)
val mpuCardData = CardDataX().apply {
setPan(pan)
setExp(expDate)
setCardHolderName("MANUAL ENTRY")
}
val tradeData = TransactionUtil.initMPUTransaction(mpuCardData, CardTypeX.MANUAL)
sharedViewModel.setCardDataExist(true)
sharedViewModel.payDetail.value = tradeData.payDetail
transProcessViewModel.setTransType(sharedViewModel.transactionsType.value)
transProcessViewModel.setTradeData(tradeData)
pinPadViewModel.setTradeData(tradeData)
onNavigatePinPad()
}
ManualCardEntryScreen(
step = step,
pan = pan,
expiry = expiry,
errorMessage = errorMessage,
onBackClick = ::goBack,
onCancelClick = ::goBack,
onConfirmClick = {
when (step) {
ManualEntryStep.CARD_NUMBER -> confirmCardNumber()
ManualEntryStep.EXPIRY -> confirmExpiry()
}
},
onKeyClick = { key ->
if (key.isBlank()) return@ManualCardEntryScreen
errorMessage = null
when (step) {
ManualEntryStep.CARD_NUMBER -> {
if (pan.length < PAN_RAW_MAX) {
pan += key.filter { it.isDigit() }
}
}
ManualEntryStep.EXPIRY -> {
if (expiry.length < EXPIRY_RAW_MAX) {
expiry += key.filter { it.isDigit() }
}
}
}
},
onDeleteClick = {
errorMessage = null
when (step) {
ManualEntryStep.CARD_NUMBER -> {
if (pan.isNotEmpty()) pan = pan.dropLast(1)
}
ManualEntryStep.EXPIRY -> {
if (expiry.isNotEmpty()) expiry = expiry.dropLast(1)
}
}
}
)
}
@Composable
private fun ManualCardEntryScreen(
step: ManualEntryStep,
pan: String,
expiry: String,
errorMessage: String?,
onBackClick: () -> Unit,
onCancelClick: () -> Unit,
onConfirmClick: () -> Unit,
onKeyClick: (String) -> Unit,
onDeleteClick: () -> Unit
) {
val isCardStep = step == ManualEntryStep.CARD_NUMBER
val displayValue = if (isCardStep) formatPanDisplay(pan) else formatExpiryDisplay(expiry)
val prompt = if (isCardStep) "CARD NUMBER" else "EXPIRY DATE"
val supportingText = when {
errorMessage != null -> errorMessage
isCardStep -> "Enter the card number to continue"
else -> "Enter expiry as MM/YY"
}
val confirmText = if (isCardStep) "Next" else "Confirm"
val confirmEnabled = if (isCardStep) {
pan.length >= PAN_RAW_MIN
} else {
expiry.length == EXPIRY_RAW_MAX
}
val canDelete = if (isCardStep) pan.isNotEmpty() else expiry.isNotEmpty()
val cardScheme = remember(pan) { detectCardScheme(pan) }
Scaffold(
topBar = {
AppBar(
title = "Manual Entry",
icon = Icons.AutoMirrored.Rounded.ArrowBack,
onIconClick = onBackClick
)
},
containerColor = Color.IvoryBeige
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.padding(horizontal = 20.dp)
) {
Spacer(modifier = Modifier.height(12.dp))
Text(
text = prompt,
color = Color.Gray,
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = displayValue.ifBlank {
if (isCardStep) "---- ---- ---- ----" else "--/--"
},
color = Color.LegacyRed,
style = MaterialTheme.typography.titleLarge.copy(
fontWeight = FontWeight.SemiBold,
letterSpacing = if (isCardStep) 1.sp else 2.sp,
textAlign = TextAlign.Center
),
textAlign = TextAlign.Center,
maxLines = 2,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
Card(
modifier = Modifier.clickable(enabled = canDelete, onClick = 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)
)
}
}
Spacer(modifier = Modifier.height(10.dp))
Text(
text = supportingText,
color = if (errorMessage == null) Color.Gray else Color.Error,
style = MaterialTheme.typography.labelSmall,
textAlign = TextAlign.Center,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
if (!isCardStep && pan.length >= 4) {
Spacer(modifier = Modifier.height(12.dp))
Text(
text = "**** **** **** ${pan.takeLast(4)}" +
(cardScheme?.let { " · $it" } ?: ""),
color = Color.Gray,
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
}
Spacer(modifier = Modifier.height(28.dp))
NumericKeypad(
modifier = Modifier
.fillMaxWidth()
.height(280.dp),
onKeyClick = onKeyClick,
keys = digitKeys
)
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(
text = if (isCardStep) "Cancel" else "Back",
style = MaterialTheme.typography.titleSmall
)
}
Button(
onClick = onConfirmClick,
enabled = confirmEnabled,
modifier = Modifier
.weight(1f)
.height(56.dp),
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,
style = MaterialTheme.typography.titleSmall
)
}
}
Spacer(modifier = Modifier.height(18.dp))
}
}
}
private fun formatPanDisplay(raw: String): String {
if (raw.isEmpty()) return ""
return raw.chunked(4).joinToString(" ")
}
private fun formatExpiryDisplay(raw: String): String {
return when {
raw.isEmpty() -> ""
raw.length <= 2 -> raw
else -> "${raw.take(2)}/${raw.drop(2)}"
}
}
private fun detectCardScheme(pan: String): String? = when {
pan.startsWith("4") -> "Visa"
pan.length >= 2 && (
pan.take(2).toIntOrNull() in 51..55 ||
pan.take(4).toIntOrNull() in 2221..2720
) -> "Mastercard"
pan.length >= 2 && pan.take(2) in listOf("34", "37") -> "Amex"
pan.startsWith("62") || pan.startsWith("6") -> "UnionPay / MPU"
else -> null
}
private fun luhnCheck(pan: String): Boolean {
var sum = 0
var alt = false
for (i in pan.length - 1 downTo 0) {
var n = pan[i].digitToIntOrNull() ?: return false
if (alt) {
n *= 2
if (n > 9) n -= 9
}
sum += n
alt = !alt
}
return sum % 10 == 0
}
@P2Preview
@Composable
private fun PreviewManualCardEntryPan() {
ManualCardEntryScreen(
step = ManualEntryStep.CARD_NUMBER,
pan = "6244310039574344",
expiry = "",
errorMessage = null,
onBackClick = {},
onCancelClick = {},
onConfirmClick = {},
onKeyClick = {},
onDeleteClick = {}
)
}
@P2Preview
@Composable
private fun PreviewManualCardEntryExpiry() {
ManualCardEntryScreen(
step = ManualEntryStep.EXPIRY,
pan = "6244310039574344",
expiry = "1228",
errorMessage = null,
onBackClick = {},
onCancelClick = {},
onConfirmClick = {},
onKeyClick = {},
onDeleteClick = {}
)
}

View File

@ -1,557 +1,34 @@
package com.mob.utsmyanmar.ui.navigation package com.mob.utsmyanmar.ui.navigation
import android.annotation.SuppressLint
import android.os.Build
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable 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.platform.LocalContext
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.NavType import androidx.navigation.NavType
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.mob.utsmyanmar.ui.cardwaiting.CardWaitingScreen import com.mob.utsmyanmar.ui.amount.AmountScreen
import com.mob.utsmyanmar.ui.cardwaiting.CardWaitingViewModel import com.mob.utsmyanmar.ui.dashboard.DashboardScreen
import com.mob.utsmyanmar.ui.dashboard.DashboardScreen2
import com.mob.utsmyanmar.ui.dashboard.DashboardViewModel
import com.mob.utsmyanmar.ui.dashboard.SeeMoreScreen
import com.mob.utsmyanmar.ui.device_info.DeviceInfoViewModel
import com.mob.utsmyanmar.ui.error.TransactionErrorScreen
import com.mob.utsmyanmar.ui.functions.FunctionsScreen
import com.mob.utsmyanmar.ui.history_functions.HistoryRoute
import com.mob.utsmyanmar.ui.history_functions.prepareReprintPayDetail
import com.mob.utsmyanmar.ui.input_amount.AmountRoute
import com.mob.utsmyanmar.ui.manual_entry.InputManualCardRoute
import com.mob.utsmyanmar.ui.password_input.InputPassword
import com.mob.utsmyanmar.ui.password_input.PasswordType
import com.mob.utsmyanmar.ui.pinpad.PinPadRoute
import com.mob.utsmyanmar.ui.pinpad.PinPadViewModel
import com.mob.utsmyanmar.ui.print_receipt.PrintReceiptScreen
import com.mob.utsmyanmar.ui.processing_card.ProcessingCardRoute
import com.mob.utsmyanmar.ui.processing_card.ProcessingCardViewModel
import com.mob.utsmyanmar.ui.refund_rrn.InputRrnRoute
import com.mob.utsmyanmar.ui.refund_rrn.RRNViewModel
import com.mob.utsmyanmar.ui.sale_void.TranDetailPage
import com.mob.utsmyanmar.ui.sale_void.VoidTraceScreen
import com.mob.utsmyanmar.ui.sale_void.VoidViewModel
import com.mob.utsmyanmar.ui.sending_to_host.ProcessingRoute
import com.mob.utsmyanmar.ui.settlement.SettlementScreen
import com.mob.utsmyanmar.ui.settlement.SettlementViewModel
import com.mob.utsmyanmar.ui.sign_on.SignOnResultScreen
import com.mob.utsmyanmar.ui.sign_on.SignOnRoute
import com.mob.utsmyanmar.ui.signature_pad.SignaturePad
import com.mob.utsmyanmar.ui.tms_setup.TmsSetupRoute
import com.mob.utsmyanmar.ui.tms_setup.TmsSetupViewModel
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultEvent
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultRoute
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultViewModel
import com.mob.utsmyanmar.ui.version.VersionScreen
import com.mob.utsmyanmar.viewmodel.CardReaderViewModel
import com.mob.utsmyanmar.viewmodel.EmvTransactionProcessViewModel
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
import com.utsmyanmar.paylibs.model.TradeData
import com.utsmyanmar.paylibs.utils.enums.TransMenu
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
@RequiresApi(Build.VERSION_CODES.N)
@SuppressLint("ContextCastToActivity")
@Composable @Composable
fun AppNavGraph( fun AppNavGraph(
navController: NavHostController, navController: NavHostController
autoSettlementRequested: Boolean = false,
onAutoSettlementHandled: () -> Unit = {}
) { ) {
val activity = LocalContext.current as ComponentActivity
val currentBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = currentBackStackEntry?.destination?.route
LaunchedEffect(autoSettlementRequested, currentRoute) {
if (!autoSettlementRequested ||
currentRoute == null ||
currentRoute == Routes.TmsSetup.route
) return@LaunchedEffect
if (currentRoute == Routes.Settlement.route) {
return@LaunchedEffect
}
if (currentRoute == Routes.Password.route) {
onAutoSettlementHandled()
return@LaunchedEffect
}
navController.navigate(Routes.Settlement.route) {
launchSingleTop = true
}
}
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = Routes.TmsSetup.route startDestination = Routes.Dashboard.route
) { ) {
composable(Routes.TmsSetup.route) {
val tmsSetupViewModel: TmsSetupViewModel = hiltViewModel()
TmsSetupRoute(
viewModel = tmsSetupViewModel,
onNavigateDashboard = {
navController.navigate(
if (autoSettlementRequested) Routes.Settlement.route else Routes.Dashboard.route
) {
popUpTo(Routes.TmsSetup.route) {
inclusive = true
}
launchSingleTop = true
}
},
onNavigateDisable = {}
)
}
composable(Routes.Signature.route) {
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
SignaturePad(
onBack = { navController.popBackStack() },
amount = sharedViewModel.payDetail.value?.amount,
cardNumber = sharedViewModel.payDetail.value?.cardNo ?: "",
onConfirm = { bitmap, signatureBlob ->
sharedViewModel.signBitmap = bitmap
sharedViewModel.updateSignatureBlob(signatureBlob)
navController.navigate(Routes.TransactionResult.route) {
popUpTo(Routes.Signature.route) {
inclusive = true
}
launchSingleTop = true
}
}
)
}
composable(Routes.Dashboard.route) { composable(Routes.Dashboard.route) {
val sharedViewModel: SharedViewModel = hiltViewModel(activity) DashboardScreen(
val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity) settlementEnabled = true,
val transResultViewModel: TransactionResultViewModel = hiltViewModel(activity) wavePayEnabled = true,
val dashboardViewModel: DashboardViewModel = hiltViewModel(activity) onAmountClick = { action ->
val dashboardUiState = dashboardViewModel.uiState.collectAsStateWithLifecycle().value navController.navigate(Routes.Amount.createRoute(action))
val recentPayDetails by sharedViewModel.getLastTenTransactions().observeAsState(emptyList())
val deviceInfoViewModel: DeviceInfoViewModel = hiltViewModel(activity)
val deviceInfoUiState = deviceInfoViewModel.uiState.collectAsStateWithLifecycle().value
val tmsSetupViewModel: TmsSetupViewModel = hiltViewModel(activity);
LaunchedEffect(deviceInfoViewModel) {
deviceInfoViewModel.loadDeviceInfo()
}
val tmsSetupUiState by tmsSetupViewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffect(tmsSetupViewModel) {
tmsSetupViewModel.onSyncCompleted.collect {
dashboardViewModel.refreshLastSyncTime()
dashboardViewModel.finishSync("Sync completed")
}
}
LaunchedEffect(tmsSetupUiState.isLoading, tmsSetupUiState.isError) {
if (dashboardUiState.isSyncing && !tmsSetupUiState.isLoading) {
if (tmsSetupUiState.isError) {
dashboardViewModel.finishSync(tmsSetupUiState.statusText.ifBlank { "Download failed!" })
}
}
}
LaunchedEffect(tmsSetupUiState.isTerminalDisabled) {
if (tmsSetupUiState.isTerminalDisabled) {
tmsSetupViewModel.clearTerminalDisabledState()
navController.navigate(Routes.TmsSetup.route) {
popUpTo(Routes.Dashboard.route) {
inclusive = true
}
launchSingleTop = true
}
}
}
transProcessViewModel.resetTransactionStatus()
DashboardScreen2(
dashboardUiState = dashboardUiState,
deviceInfo = deviceInfoUiState,
onClickLastSync = {
dashboardViewModel.startSync()
tmsSetupViewModel.downloadConfigs()
}, },
onNavigateRecentTransaction = { record -> onTransactionClick = {},
recentPayDetails.firstOrNull { it.PID == record.pid }?.let { payDetail -> onSettlementClick = {},
sharedViewModel.setTransMenu(TransMenu.REPRINT) onHistoryClick = {},
sharedViewModel.isReprint.value = true onCardClick = {},
sharedViewModel.payDetail.value = prepareReprintPayDetail(payDetail) onWavePayClick = {}
transResultViewModel.onEvent(
TransactionResultEvent.Start,
sharedViewModel
)
navController.navigate(Routes.PrintReceipt.route) {
launchSingleTop = true
}
}
},
onNavigateAmount = { action ->
when (action) {
"Pre-Auth",
"Cash Adv",
"Sale" -> {
navController.navigate(Routes.Amount.createRoute(action)) {
launchSingleTop = true
}
}
"Pre-Auth",
"Cash Adv" -> {
navController.navigate(Routes.Amount.createRoute(action)) {
launchSingleTop = true
}
}
else -> {
navController.navigate(
Routes.Password.createRoute(
destination = Routes.Amount.createRoute(action),
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
}
}
}
},
onNavigateAction = { action ->
when (action) {
"Void" -> {
sharedViewModel.transactionsType.value = TransactionsType.VOID
navController.navigate(
Routes.Password.createRoute(
destination = Routes.VoidTrace.route,
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
}
}
"Pre-Auth",
"Cash Adv" -> {
sharedViewModel.transactionsType.value = when (action) {
"Pre-Auth" -> TransactionsType.PRE_AUTH_SALE
else -> TransactionsType.CASH_OUT
}
navController.navigate(Routes.Amount.createRoute(action)) {
launchSingleTop = true
}
}
"Pre-Auth Complete Void" -> {
sharedViewModel.transactionsType.value = TransactionsType.PRE_AUTH_COMPLETE_VOID
navController.navigate(
Routes.Password.createRoute(
destination = Routes.VoidTrace.route,
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
}
}
else -> {
navController.navigate(
Routes.Password.createRoute(
destination = Routes.Amount.createRoute(action),
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
}
}
}
},
onNavigateSignOn = {
navController.navigate(Routes.SignOn.route) {
launchSingleTop = true
}
},
onNavigateSettlement = {
navController.navigate(
Routes.Password.createRoute(
destination = Routes.Settlement.route,
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
}
},
onNavigateVersion = {
navController.navigate(
Routes.Password.createRoute(
destination = Routes.Version.route,
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
}
},
onNavigateFunctions = {
navController.navigate(
Routes.Password.createRoute(
destination = Routes.Functions.route,
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
}
},
onNavigateHistory = {
navController.navigate(Routes.History.route)
}
)
}
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.Password.createRoute(
destination = Routes.VoidTrace.route,
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
}
}
"Pre-Auth",
"Cash Adv" -> {
sharedViewModel.transactionsType.value = when (action) {
"Pre-Auth" -> TransactionsType.PRE_AUTH_SALE
else -> TransactionsType.CASH_OUT
}
navController.navigate(Routes.Amount.createRoute(action)) {
launchSingleTop = true
}
}
"Pre-Auth Complete Void" -> {
sharedViewModel.transactionsType.value = TransactionsType.PRE_AUTH_COMPLETE_VOID
navController.navigate(
Routes.Password.createRoute(
destination = Routes.VoidTrace.route,
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
}
}
else -> {
navController.navigate(
Routes.Password.createRoute(
destination = Routes.Amount.createRoute(action),
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
}
}
}
}
)
}
composable(Routes.Version.route){
val deviceInfoViewModel: DeviceInfoViewModel = hiltViewModel();
VersionScreen(
onBack = {navController.popBackStack()},
deviceInfoViewModel = deviceInfoViewModel
)
}
composable(
route = Routes.Password.route,
arguments = listOf(
navArgument("destination") {
type = NavType.StringType
},
navArgument("passwordType") {
type = NavType.StringType
}
)
) { backStackEntry ->
val destination = backStackEntry.arguments?.getString("destination").orEmpty()
val passwordType = backStackEntry.arguments?.getString("passwordType").orEmpty()
InputPassword(
passwordType = passwordType,
onBack = { navController.popBackStack() },
onPasswordCorrect = {
navController.navigate(destination) {
popUpTo(Routes.Password.route) {
inclusive = true
}
launchSingleTop = true
}
}
)
}
composable(Routes.Functions.route) {
FunctionsScreen(
onBack = { navController.popBackStack() }
)
}
composable(Routes.VoidTrace.route) {
val voidViewModel: VoidViewModel = hiltViewModel()
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 = {
onAutoSettlementHandled()
if (!navController.popBackStack(Routes.Dashboard.route, false)) {
navController.navigate(Routes.Dashboard.route) {
launchSingleTop = true
}
}
},
autoStartSettlement = autoSettlementRequested,
onStartSettlement = {
sharedViewModel.transactionsType.value = TransactionsType.SETTLEMENT
navController.navigate(Routes.Processing.route) {
popUpTo(Routes.Settlement.route) {
inclusive = true
}
launchSingleTop = true
}
if (autoSettlementRequested) {
onAutoSettlementHandled()
}
}
)
}
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
}
}
) )
} }
@ -563,335 +40,10 @@ fun AppNavGraph(
} }
) )
) { backStackEntry -> ) { backStackEntry ->
val sharedViewModel: SharedViewModel = hiltViewModel(activity) AmountScreen(
AmountRoute(
action = backStackEntry.arguments?.getString("action").orEmpty(), action = backStackEntry.arguments?.getString("action").orEmpty(),
sharedViewModel = sharedViewModel, onBackClick = { navController.popBackStack() }
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 = {
navController.navigate(Routes.ManualEntry.route) {
launchSingleTop = true
}
},
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.ManualEntry.route) {
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity)
val pinPadViewModel: PinPadViewModel = hiltViewModel(activity)
InputManualCardRoute(
transProcessViewModel = transProcessViewModel,
sharedViewModel = sharedViewModel,
pinPadViewModel = pinPadViewModel,
onBack = { navController.popBackStack() },
onNavigatePinPad = {
pinPadViewModel.resetSessionState()
navController.navigate(Routes.PinPad.route) {
popUpTo(Routes.ManualEntry.route) {
inclusive = true
}
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.History.route) {
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
val transResultViewModel: TransactionResultViewModel = hiltViewModel(activity)
HistoryRoute(
sharedViewModel = sharedViewModel,
onBack = { navController.popBackStack() },
onNavigatePrintReceipt = {
transResultViewModel.onEvent(
TransactionResultEvent.Start,
sharedViewModel
)
navController.navigate(Routes.PrintReceipt.route) {
launchSingleTop = true
}
}
)
}
composable(Routes.Processing.route) {
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity)
val settlementViewModel: SettlementViewModel = hiltViewModel(activity)
val tmsSetupViewModel: TmsSetupViewModel = hiltViewModel(activity)
val dashboardViewModel: DashboardViewModel = hiltViewModel(activity)
ProcessingRoute(
sharedViewModel = sharedViewModel,
transProcessViewModel = transProcessViewModel,
settlementViewModel = settlementViewModel,
onTransactionFinished = {
tmsSetupViewModel.downloadConfigs(dashboardViewModel::refreshLastSyncTime)
},
onNavigateTransactionResult = {
navController.navigate(Routes.TransactionResult.route) {
popUpTo(Routes.Processing.route) {
inclusive = true
}
launchSingleTop = true
}
},
onNavigateSignature = {
navController.navigate(Routes.Signature.route) {
popUpTo(Routes.Processing.route) {
inclusive = true
}
launchSingleTop = true
}
},
onNavigateError = { isSuccess,message ->
navController.navigate(Routes.TransactionError.createRoute(isSuccess,message)) {
popUpTo(Routes.Processing.route) {
inclusive = true
}
launchSingleTop = true
}
}
)
}
composable(
route = Routes.TransactionError.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()
TransactionErrorScreen(
isSuccess = isSuccess,
message = message,
onDone = {
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 = { message ->
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
}
)
}
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
}
},
onPrintFinished = {
navController.navigate(Routes.Dashboard.route) {
popUpTo(Routes.Dashboard.route) {
inclusive = false
}
launchSingleTop = true
}
}
) )
} }
} }
} }
private fun formatAmountForDisplay(amount: String?): String {
val raw = amount.orEmpty().trim()
if (raw.isBlank()) return "0.00"
if (raw.contains(".")) {
val whole = raw.substringBefore(".").ifEmpty { "0" }
val decimal = raw.substringAfter(".", "")
val grouped = "%,d".format(whole.toLongOrNull() ?: 0L)
return "$grouped.${decimal.padEnd(2, '0').take(2)}"
}
val value = raw.toLongOrNull() ?: return raw
return "%,d.00".format(value)
}

View File

@ -1,51 +1,8 @@
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 Signature: Routes("signature")
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/${Uri.encode(action)}" fun createRoute(action: String): String = "amount/$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 ManualEntry : Routes("manual_entry")
data object TransactionResult : Routes("transaction_result")
data object PrintReceipt : Routes("print_receipt")
data object Version : Routes("version")
data object Functions : Routes("functions")
data object Password : Routes("password/{destination}/{passwordType}") {
fun createRoute(destination: String, passwordType: String): String =
"password/${Uri.encode(destination)}/$passwordType"
}
data object NotificationList : Routes("notification_list")
data object NotificationDetail : Routes("notification_detail/{notificationId}") {
fun createRoute(id: Int): String = "notification_detail/$id"
}
data object History : Routes("history")
data object Disable : Routes("disable/{message}") {
fun createRoute(message: String): String = "disable/${Uri.encode(message)}"
}
data object TransactionError : Routes("transaction_error/{isSuccess}/{message}") {
fun createRoute(isSuccess: Boolean, message: String): String = "transaction_error/$isSuccess/${Uri.encode(message)}"
} }
} }

View File

@ -1,115 +0,0 @@
package com.mob.utsmyanmar.ui.notification
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.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.unit.dp
import androidx.compose.ui.unit.sp
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.theme.Color
@Composable
fun NotificationDetailScreen(
viewModel: NotificationViewModel,
notificationId: Int,
onBack: () -> Unit
) {
val notification = viewModel.getById(notificationId)
Scaffold(
containerColor = Color.IvoryBeige,
topBar = {
AppBar(
title = "Notification Detail",
icon = Icons.AutoMirrored.Filled.ArrowBack,
onIconClick = onBack
)
}
) { paddingValues ->
if (notification == null) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = Alignment.Center
) {
Text(text = "Notification not found", color = Color.Gray)
}
} else {
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(modifier = Modifier.padding(20.dp)) {
Box(
modifier = Modifier
.size(56.dp)
.background(Color.LegacyRed.copy(alpha = 0.1f), CircleShape),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.Notifications,
contentDescription = null,
tint = Color.LegacyRed,
modifier = Modifier.size(28.dp)
)
}
Spacer(Modifier.height(16.dp))
Text(
text = notification.title,
style = MaterialTheme.typography.titleMedium,
color = Color.Black
)
Spacer(Modifier.height(6.dp))
Text(
text = notification.timestamp,
style = MaterialTheme.typography.bodySmall,
color = Color.Gray
)
Spacer(Modifier.height(16.dp))
Text(
text = notification.message,
color = Color.Black,
style = MaterialTheme.typography.bodyMedium,
lineHeight = 22.sp
)
}
}
}
}
}
}

View File

@ -1,174 +0,0 @@
package com.mob.utsmyanmar.ui.notification
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.theme.Color
@Composable
fun NotificationListScreen(
viewModel: NotificationViewModel,
onBack: () -> Unit,
onNavigateDetail: (Int) -> Unit
) {
val notifications by viewModel.notifications.collectAsState()
Scaffold(
containerColor = Color.IvoryBeige,
topBar = {
AppBar(
title = "Notifications",
icon = Icons.AutoMirrored.Filled.ArrowBack,
onIconClick = onBack,
actions = {
if (notifications.any { !it.isRead }) {
TextButton(onClick = { viewModel.markAllAsRead() }) {
Text(text = "Mark all read", color = Color.White, style = MaterialTheme.typography.bodySmall)
}
}
}
)
}
) { paddingValues ->
if (notifications.isEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
imageVector = Icons.Default.Notifications,
contentDescription = null,
tint = Color.Gray,
modifier = Modifier.size(64.dp)
)
Text(text = "No notifications", color = Color.Gray, style = MaterialTheme.typography.bodyMedium)
}
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(items = notifications, key = { it.id }) { notification ->
NotificationItem(
notification = notification,
onClick = {
viewModel.markAsRead(notification.id)
onNavigateDetail(notification.id)
}
)
}
}
}
}
}
@Composable
private fun NotificationItem(
notification: AppNotification,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(Color.White)
.clickable(onClick = onClick)
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(42.dp)
.background(
color = if (notification.isRead) Color.Gray.copy(alpha = 0.12f)
else Color.LegacyRed.copy(alpha = 0.12f),
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.Notifications,
contentDescription = null,
tint = if (notification.isRead) Color.Gray else Color.LegacyRed,
modifier = Modifier.size(22.dp)
)
}
Spacer(Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = notification.title,
style = if (notification.isRead) MaterialTheme.typography.bodyMedium
else MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
color = if (notification.isRead) Color.Gray else Color.Black,
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (!notification.isRead) {
Box(
modifier = Modifier
.size(8.dp)
.background(Color.LegacyRed, CircleShape)
)
}
}
Text(
text = notification.message,
style = MaterialTheme.typography.bodySmall,
color = Color.Gray,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = notification.timestamp,
style = MaterialTheme.typography.labelSmall,
color = Color.Gray.copy(alpha = 0.7f)
)
}
}
}

View File

@ -1,9 +0,0 @@
package com.mob.utsmyanmar.ui.notification
data class AppNotification(
val id: Int,
val title: String,
val message: String,
val timestamp: String,
val isRead: Boolean = false
)

View File

@ -1,37 +0,0 @@
package com.mob.utsmyanmar.ui.notification
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
@HiltViewModel
class NotificationViewModel @Inject constructor() : ViewModel() {
private val _notifications = MutableStateFlow(sampleNotifications)
val notifications: StateFlow<List<AppNotification>> = _notifications.asStateFlow()
val unreadCount: Int get() = _notifications.value.count { !it.isRead }
fun markAsRead(id: Int) {
_notifications.value = _notifications.value.map {
if (it.id == id) it.copy(isRead = true) else it
}
}
fun markAllAsRead() {
_notifications.value = _notifications.value.map { it.copy(isRead = true) }
}
fun getById(id: Int): AppNotification? = _notifications.value.find { it.id == id }
}
private val sampleNotifications = listOf(
AppNotification(1, "Settlement Reminder", "Your daily settlement is pending. Please settle before end of day.", "Today, 17:30", isRead = false),
AppNotification(2, "TMS Update Available", "A new terminal configuration update is ready. Please restart to apply.", "Today, 09:15", isRead = false),
AppNotification(3, "Transaction Approved", "Sale of MMK 50,000 was approved successfully. Trace #001234.", "Yesterday, 14:22", isRead = true),
AppNotification(4, "Network Warning", "Intermittent network issues detected. Contact your administrator if the problem persists.", "Yesterday, 11:05", isRead = true),
AppNotification(5, "Log-On Required", "Your terminal session has expired. Please log on again to continue.", "06/09, 08:00", isRead = true),
)

View File

@ -1,210 +0,0 @@
package com.mob.utsmyanmar.ui.password_input
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBackIosNew
import androidx.compose.material.icons.rounded.Backspace
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.mob.utsmyanmar.ui.components.NumericKeypad
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.preview.P2Preview
import com.mob.utsmyanmar.ui.preview.P3Preview
import com.mob.utsmyanmar.ui.theme.Color
object PasswordType {
const val SYSTEM = "system_password"
const val SETTLEMENT = "settlement_password"
const val SETTING = "setting_password"
}
private val passwords = mapOf(
PasswordType.SYSTEM to "111111",
PasswordType.SETTING to "222222",
PasswordType.SETTLEMENT to "123456"
)
private val passwordKeys = listOf(
listOf("1", "2", "3"),
listOf("4", "5", "6"),
listOf("7", "8", "9"),
listOf("", "0", "")
)
@Composable
fun InputPassword(
passwordType: String,
onBack: () -> Unit = {},
onPasswordCorrect: () -> Unit = {},
viewModel: PasswordInputViewModel = viewModel()
) {
val expectedPassword = passwords[passwordType] ?: passwords[PasswordType.SETTING]!!
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffect(Unit) { viewModel.reset() }
Scaffold(
topBar = {
AppBar(title = "Password", icon = Icons.Default.ArrowBackIosNew, onIconClick = onBack)
},
containerColor = Color.IvoryBeige
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.padding(16.dp)
) {
Spacer(Modifier.height(8.dp))
// Display area
Box(
modifier = Modifier
.fillMaxSize()
.weight(2f)
) {
Card(
modifier = Modifier
.align(Alignment.TopEnd)
.clickable(enabled = uiState.canDelete, onClick = viewModel::onDelete),
shape = RoundedCornerShape(18.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
) {
Icon(
imageVector = Icons.Rounded.Backspace,
contentDescription = "Delete",
tint = if (uiState.canDelete) Color.LegacyRed else Color.Gray,
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
)
}
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Enter Password", color = Color.LegacyRed, fontSize = 18.sp)
Spacer(Modifier.height(16.dp))
PasswordDots(filledCount = uiState.input.length, totalCount = PASSWORD_LENGTH)
Spacer(Modifier.height(8.dp))
Text(
text = uiState.errorMessage ?: "",
color = Color.LegacyRed,
fontSize = 14.sp
)
}
}
// Keypad — stable lambda: viewModel::onKey never changes between recompositions
Column(
modifier = Modifier
.fillMaxWidth()
.weight(3f),
verticalArrangement = Arrangement.Bottom
) {
NumericKeypad(
keys = passwordKeys,
modifier = Modifier.fillMaxSize(),
onKeyClick = viewModel::onKey
)
}
Spacer(Modifier.height(16.dp))
// Action buttons
Box(
modifier = Modifier
.fillMaxWidth()
.weight(0.5f)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Button(
onClick = onBack,
modifier = Modifier.weight(1f).height(56.dp),
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.White,
contentColor = Color.LegacyRed
)
) {
Text("Cancel")
}
Button(
onClick = { viewModel.validate(expectedPassword, onPasswordCorrect) },
modifier = Modifier.weight(1f).height(56.dp),
enabled = uiState.isConfirmEnabled,
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.LegacyRed,
contentColor = Color.White,
disabledContainerColor = Color.LegacyRed.copy(alpha = 0.5f),
disabledContentColor = Color.White
)
) {
Text(text = "Next", fontSize = 14.sp, fontWeight = FontWeight.Medium)
}
}
}
}
}
}
@Composable
private fun PasswordDots(filledCount: Int, totalCount: Int) {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
repeat(totalCount) { index ->
Box(
modifier = Modifier
.size(14.dp)
.background(
color = if (index < filledCount) Color.LegacyRed
else Color.Gray.copy(alpha = 0.3f),
shape = CircleShape
)
)
}
}
}
@P3Preview
@P2Preview
@Composable
fun PreviewInputPassword() {
InputPassword(passwordType = PasswordType.SETTING)
}

View File

@ -1,11 +0,0 @@
package com.mob.utsmyanmar.ui.password_input
internal const val PASSWORD_LENGTH = 6
data class PasswordInputUiState(
val input: String = "",
val errorMessage: String? = null
) {
val isConfirmEnabled: Boolean get() = input.length == PASSWORD_LENGTH
val canDelete: Boolean get() = input.isNotEmpty()
}

View File

@ -1,40 +0,0 @@
package com.mob.utsmyanmar.ui.password_input
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
class PasswordInputViewModel : ViewModel() {
private val _uiState = MutableStateFlow(PasswordInputUiState())
val uiState: StateFlow<PasswordInputUiState> = _uiState.asStateFlow()
fun onKey(digit: String) {
_uiState.update { state ->
if (state.input.length < PASSWORD_LENGTH)
state.copy(input = state.input + digit, errorMessage = null)
else
state
}
}
fun onDelete() {
_uiState.update { state ->
state.copy(input = state.input.dropLast(1), errorMessage = null)
}
}
fun validate(expectedPassword: String, onCorrect: () -> Unit) {
if (_uiState.value.input == expectedPassword) {
onCorrect()
} else {
_uiState.update { it.copy(errorMessage = "Incorrect password", input = "") }
}
}
fun reset() {
_uiState.value = PasswordInputUiState()
}
}

View File

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

View File

@ -1,178 +0,0 @@
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.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.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.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,
style = MaterialTheme.typography.titleMedium
)
},
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,
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(14.dp))
Text(
text = if (pinText.isBlank()) "------" else pinText,
color = White,
style = MaterialTheme.typography.displayMedium,
letterSpacing = 4.sp
)
if (!alertMessage.isNullOrBlank()) {
Spacer(modifier = Modifier.height(12.dp))
Text(
text = alertMessage,
color = White,
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center
)
}
}
}
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Enter your PIN on the secured keypad below.",
color = Black,
style = MaterialTheme.typography.bodyLarge,
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,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center
)
}
}
}
}

View File

@ -1,569 +0,0 @@
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.kizzy.cmhl.utils.ByteUtils
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 {
LogUtil.d(TAG,"pan : $pan")
if (pan.length < 13) {
return ByteArray(0)
}
LogUtil.d(TAG,"pan bytes : ${ByteUtils.byteArrayToHexString(pan
.substring(pan.length - 13, pan.length - 1)
.toByteArray(StandardCharsets.US_ASCII))}")
return pan
.substring(pan.length - 13, pan.length - 1)
.toByteArray(StandardCharsets.US_ASCII)
}
}

View File

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

View File

@ -1,385 +0,0 @@
package com.mob.utsmyanmar.ui.print_receipt
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.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.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.history_functions.displayTransactionStatus
import com.mob.utsmyanmar.ui.history_functions.displayTransactionTitle
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.ui.theme.MOBPOSTheme
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultState
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultViewModel
import com.mob.utsmyanmar.utils.convertDateString
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.utils.POSUtil
import com.utsmyanmar.paylibs.utils.PrintStatus
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
import kotlinx.coroutines.launch
private enum class ReceiptCopy(val label: String) {
MERCHANT("MERCHANT COPY"),
CUSTOMER("CUSTOMER COPY")
}
@Composable
fun PrintReceiptScreen(
transactionResultViewModel: TransactionResultViewModel,
sharedViewModel: SharedViewModel,
onPrint: () -> Unit,
onDone: () -> Unit,
onPrintFinished: () -> Unit,
) {
val state by transactionResultViewModel.state.collectAsStateWithLifecycle()
val isReprint = sharedViewModel.isReprint.value ?: false
PrintReceiptContent(
state = state,
isReprint = isReprint,
onPrint = onPrint,
onDone = onDone,
onPrintFinished = onPrintFinished,
onSetPrintStatus = { sharedViewModel.setPrintStatus(it) },
onResetReprint = { sharedViewModel.isReprint.value = false },
)
}
@Composable
fun PrintReceiptContent(
state: TransactionResultState,
isReprint: Boolean,
onPrint: () -> Unit,
onDone: () -> Unit,
onPrintFinished: () -> Unit,
onSetPrintStatus: (PrintStatus) -> Unit,
onResetReprint: () -> Unit
) {
val scope = rememberCoroutineScope()
val receiptOffsetY = remember { Animatable(0f) }
val merchant_copy_autoprint = true
val customer_copy_autoprint = false
var currentCopy by remember { mutableStateOf(ReceiptCopy.MERCHANT) }
var nextCopy by remember { mutableStateOf(ReceiptCopy.CUSTOMER) }
var isPrinting by remember { mutableStateOf(false) }
suspend fun animatePrint() {
receiptOffsetY.animateTo(
targetValue = -1400f,
animationSpec = tween(durationMillis = 4000, easing = FastOutSlowInEasing)
)
// delay(1000.milliseconds)
receiptOffsetY.animateTo(
targetValue = 0f,
animationSpec = tween(durationMillis = 0, easing = FastOutSlowInEasing)
)
}
fun doPrint(copy: ReceiptCopy, onFinished: (printFinished: ReceiptCopy) -> Unit) {
val actualCopy = if (isReprint && copy == ReceiptCopy.MERCHANT) ReceiptCopy.CUSTOMER else copy
currentCopy = actualCopy
onSetPrintStatus(
if (actualCopy == ReceiptCopy.MERCHANT)
PrintStatus.FIRST_PRINT
else
PrintStatus.SECOND_PRINT
)
onPrint()
scope.launch {
isPrinting = true
animatePrint()
isPrinting = false
val upcomingCopy = if (isReprint) ReceiptCopy.CUSTOMER
else if (actualCopy == ReceiptCopy.MERCHANT) ReceiptCopy.CUSTOMER
else ReceiptCopy.CUSTOMER
currentCopy = upcomingCopy
nextCopy = upcomingCopy
onFinished(actualCopy)
}
}
LaunchedEffect(Unit) {
if (isReprint) {
currentCopy = ReceiptCopy.CUSTOMER
nextCopy = ReceiptCopy.CUSTOMER
}
onResetReprint()
when {
merchant_copy_autoprint && !isReprint -> doPrint(ReceiptCopy.MERCHANT) { }
customer_copy_autoprint -> doPrint(ReceiptCopy.CUSTOMER) { }
}
}
Scaffold(
topBar = {
AppBar(title = "Receipt")
},
containerColor = Color.IvoryBeige
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
ReceiptPreview(
state = state,
copyLabel = currentCopy.label,
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.titleMedium
)
}
Button(
modifier = Modifier
.weight(1f)
.height(50.dp),
enabled = !isPrinting,
colors = ButtonDefaults.buttonColors(
containerColor = Color.LegacyRed,
contentColor = Color.White,
disabledContainerColor = Color.LegacyRed.copy(alpha = 0.5f),
disabledContentColor = Color.White.copy(alpha = 0.5f),
),
shape = RoundedCornerShape(12.dp),
onClick = {
doPrint(nextCopy) { printFinished ->
if (printFinished == ReceiptCopy.CUSTOMER) {
onPrintFinished()
}
}
}
) {
Text(
text = "Print",
style = MaterialTheme.typography.titleMedium
)
}
}
}
}
}
@Composable
fun ReceiptPreview(
state: TransactionResultState,
copyLabel: String,
modifier: Modifier = Modifier
) {
val isPreview = LocalInspectionMode.current
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",
style = MaterialTheme.typography.headlineLarge
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Transaction Receipt",
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = copyLabel,
style = MaterialTheme.typography.labelLarge,
color = Color.Gray
)
Spacer(modifier = Modifier.height(16.dp))
ReceiptDivider()
ReceiptRow("Date", convertDateString(state.payDetail?.transDate.toString()))
ReceiptRow("Time", state.payDetail?.transTime.toString())
ReceiptRow("Merchant Name", state.payDetail?.merchantName.toString())
ReceiptRow("TID", state.payDetail?.terminalNo.toString())
ReceiptRow("Trace No", state.payDetail?.voucherNo.toString())
ReceiptRow("Ref No", state.payDetail?.referNo.toString())
ReceiptRow("Approval Code", state.payDetail?.approvalCode.toString())
ReceiptRow("Transaction Type", state.payDetail?.let(::displayTransactionTitle).orEmpty())
ReceiptRow("Amount", POSUtil.getInstance().getDecimalAmountSeparatorFormat(state.payDetail?.amount?:0).let { amt ->
val prefix = POSUtil.getInstance().checkMinusSign(state.payDetail?.transactionType ?: 0)
if (prefix.isEmpty() && (state.payDetail?.transactionType ?: 0) == TransactionsType.CASH_OUT.value) "-$amt" else "$prefix$amt"
})
ReceiptRow("Card Type", state.payDetail?.accountType.toString())
ReceiptRow("Card No", state.payDetail?.cardNo.toString())
ReceiptRow("Status", if (isPreview) "SUCCESS" else state.payDetail?.let(::displayTransactionStatus).orEmpty())
ReceiptDivider()
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Thank You",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Please keep this receipt",
style = MaterialTheme.typography.bodySmall
)
}
}
@Composable
fun ReceiptRow(
label: String,
value: String
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 6.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = label,
style = MaterialTheme.typography.bodyMedium,
color = Color.Gray
)
Text(
text = value,
style = MaterialTheme.typography.titleSmall
)
}
}
@Composable
fun ReceiptDivider() {
Canvas(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 10.dp)
.height(2.dp)
) {
// Convert dp to pixels so it scales perfectly on all screen sizes
val dotRadius = 1.5.dp.toPx()
val dotSpacing = 6.dp.toPx()
val totalWidth = size.width
var currentX = 0f
// Loop across the width of the screen and draw individual circles
while (currentX < totalWidth) {
drawCircle(
color = Color.Gray,
radius = dotRadius,
center = Offset(x = currentX, y = size.height / 2)
)
// Move X forward by the size of the dot + the space we want
currentX += (dotRadius * 2) + dotSpacing
}
}
}
@P2Preview
@P3Preview
@Composable
private fun PrintReceiptScreenPreview() {
val samplePayDetail = PayDetail().apply {
merchantName = "Sample Merchant"
terminalNo = "12345678"
voucherNo = "000001"
amount = 100000
cardNo = "1234********5678"
transDate = "25-08-26"
transTime = "10:30:00"
referNo = "123456789012"
transType = "SALE"
tradeAnswerCode = "00"
approvalCode = "123456"
}
val sampleState = TransactionResultState(
payDetail = samplePayDetail
)
MOBPOSTheme {
PrintReceiptContent(
state = sampleState,
isReprint = false,
onPrint = {},
onDone = {},
onPrintFinished = {},
onSetPrintStatus = {},
onResetReprint = {}
)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,407 +0,0 @@
package com.mob.utsmyanmar.ui.refund_rrn
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.material3.Button
import androidx.compose.material3.ButtonDefaults
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.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
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.theme.MOBPOSTheme
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()
}
else -> Unit
}
}
InputRrnContent(
rrn = rrn,
errorMessage = errorMessage,
onRrnChange = { value ->
rrn = if (value == "Del") {
rrn.dropLast(1)
} else {
appendRrnValue(rrn, value)
}
errorMessage = null
},
onBack = onBack,
onNext = {
val trimmedRrn = rrn.trim()
if (POSUtil.getInstance().checkNumberField(trimmedRrn)) {
errorMessage = "Invalid RRN"
return@InputRrnContent
}
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 ?: ""
)
}
},
onDelete = {
if (rrn.isNotEmpty()) {
rrn = rrn.dropLast(1)
}
}
)
}
@Composable
private fun InputRrnContent(
rrn: String,
errorMessage: String?,
onRrnChange: (String) -> Unit,
onBack: () -> Unit,
onNext: () -> Unit,
onDelete: () -> Unit,
) {
Scaffold(
containerColor = Color.IvoryBeige,
topBar = {
AppBar(
title = "Input RRN",
icon = Icons.AutoMirrored.Filled.ArrowBack,
onIconClick = onBack
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "Enter RRN",
color = Color.Gray,
fontSize = 11.sp,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = rrn.ifEmpty { "0" },
color = Color.LegacyRed,
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = errorMessage ?: "Enter the retrieval reference number to continue",
color = if (errorMessage == null) Color.Gray else Color.Error,
fontSize = 11.sp,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
}
Spacer(modifier = Modifier.height(24.dp))
Column(
modifier = Modifier.fillMaxWidth()
) {
RrnKeypad(
onKeyClick = onRrnChange,
canDelete = rrn.isNotEmpty()
)
Spacer(modifier = Modifier.height(24.dp))
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 = onNext,
modifier = Modifier
.weight(1f)
.height(56.dp),
enabled = rrn.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("Next")
}
}
}
}
// Card(
// modifier = Modifier
// .align(Alignment.TopEnd)
// .padding(top = 110.dp, end = 16.dp)
// .clickable(enabled = rrn.isNotEmpty()) {
// onDelete()
// },
// 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 (rrn.isNotEmpty()) Color.LegacyRed else Color.Gray,
// modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
// )
// }
}
}
}
@Composable
private fun RrnKeypad(
onKeyClick: (String) -> Unit,
canDelete: Boolean
) {
val keys = listOf(
listOf("1", "2", "3"),
listOf("4", "5", "6"),
listOf("7", "8", "9"),
listOf("Del", "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 ->
RrnKeypadButton(
text = key,
modifier = Modifier.weight(1f),
enabled = key.isNotBlank() && (key != "Del" || canDelete),
onClick = { onKeyClick(key) }
)
}
}
}
}
}
@Composable
private fun RrnKeypadButton(
text: String,
modifier: Modifier = Modifier,
enabled: Boolean = true,
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(enabled = enabled) { onClick() },
contentAlignment = Alignment.Center
) {
Text(
text = text,
color = if (enabled) Color.LegacyRed else Color.Gray,
fontSize = 24.sp,
fontWeight = FontWeight.Normal,
textAlign = TextAlign.Center
)
}
}
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)
}
@Preview(showBackground = true)
@Composable
private fun InputRrnPreview() {
MOBPOSTheme {
InputRrnContent(
rrn = "123456789012",
errorMessage = null,
onRrnChange = {},
onBack = {},
onNext = {},
onDelete = {}
)
}
}
@Preview(showBackground = true)
@Composable
private fun InputRrnErrorPreview() {
MOBPOSTheme {
InputRrnContent(
rrn = "123",
errorMessage = "Invalid RRN",
onRrnChange = {},
onBack = {},
onNext = {},
onDelete = {}
)
}
}

View File

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

View File

@ -1,167 +0,0 @@
package com.mob.utsmyanmar.ui.sale_void
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.mob.utsmyanmar.utils.convertDateString
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 = "Void Sale",
color = Color.LegacyRed,
fontWeight = FontWeight.Bold,
fontSize = 16.sp
)
DetailRow("Date", convertDateString(transaction.transDate))
DetailRow("Time", transaction.transTime)
DetailRow("Amount", POSUtil.getInstance().formatAmount(transaction.amount))
DetailRow("Trace No", transaction.voucherNo)
DetailRow("Reference No", transaction.referNo)
DetailRow("Approval Code", transaction.approvalCode.orEmpty())
// DetailRow("Card No", transaction.cardNo)
}
}
}
@Composable
private fun DetailRow(
label: String,
value: String
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = label,
color = Color.Gray,
fontSize = 13.sp
)
Text(
text = value.ifBlank { "-" },
color = Color.Black,
fontSize = 13.sp,
fontWeight = FontWeight.Medium
)
}
}

View File

@ -1,267 +0,0 @@
package com.mob.utsmyanmar.ui.sale_void
import android.util.Log
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.MaterialTheme
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.unit.dp
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.mob.utsmyanmar.ui.components.NumericKeypad
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
private val traceKeys = listOf(
listOf("1", "2", "3"),
listOf("4", "5", "6"),
listOf("7", "8", "9"),
listOf("", "0", "00")
)
@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 ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.padding(16.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.weight(2f)
) {
Text(
text = headerMessage,
color = headerColor,
style = MaterialTheme.typography.bodyMedium
)
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Trace Number",
color = Color.Gray,
style = MaterialTheme.typography.labelSmall
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = displayTraceNumber,
color = Color.LegacyRed,
style = MaterialTheme.typography.displayLarge
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Enter 6-digit trace number",
color = Color.Gray,
style = MaterialTheme.typography.labelSmall
)
}
Card(
modifier = Modifier
.align(Alignment.CenterEnd)
.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)
)
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.weight(3f),
verticalArrangement = Arrangement.Bottom
) {
NumericKeypad(
keys = traceKeys,
modifier = Modifier.fillMaxSize(),
onKeyClick = { value ->
if (traceNumber.length < 6) {
traceNumber = (traceNumber + value).take(6)
}
}
)
}
Spacer(modifier = Modifier.height(16.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.weight(0.5f)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Button(
onClick = onBack,
modifier = Modifier
.weight(1f)
.height(56.dp),
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.White,
contentColor = Color.LegacyRed
)
) {
Text("Cancel")
}
Button(
onClick = {
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")
}
}
}
}
}
}

View File

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

View File

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

View File

@ -1,109 +0,0 @@
package com.mob.utsmyanmar.ui.sending_to_host
import android.R
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.mob.utsmyanmar.model.TransResultStatus
import com.mob.utsmyanmar.model.TransactionStatus
import com.mob.utsmyanmar.ui.settlement.SettlementViewModel
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
private const val MOCK_HOST_DELAY_MS = 2000L
@Composable
fun ProcessingRoute(
sharedViewModel: SharedViewModel,
transProcessViewModel: TransProcessViewModel,
settlementViewModel: SettlementViewModel,
onNavigateTransactionResult: () -> Unit,
onNavigateSignature: () -> Unit,
onNavigateError: (Boolean, String) -> Unit,
onTransactionFinished: () -> 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 -> {
onTransactionFinished()
sharedViewModel.payDetail.value = settlementViewModel.getPayDetail()
onNavigateTransactionResult()
}
else -> {}
}
}
} else {
LaunchedEffect(Unit) {
transProcessViewModel.startOnlineProcess()
}
LaunchedEffect(transStatus) {
when(transStatus) {
TransResultStatus.SUCCESS -> {
onTransactionFinished()
sharedViewModel.payDetail.value = transProcessViewModel.payDetailResult.value
if (requiresSignature(sharedViewModel.transactionsType.value, sharedViewModel.payDetail.value)) {
onNavigateSignature()
} else {
onNavigateTransactionResult()
}
}
TransResultStatus.FAIL -> {
onTransactionFinished()
sharedViewModel.payDetail.value = transProcessViewModel.payDetailResult.value
onNavigateTransactionResult()
}
TransResultStatus.REVERSAL_SUCCESS -> {
onTransactionFinished()
onNavigateError(true,"Reversal success")
}
TransResultStatus.REVERSAL_FAIL -> {
onTransactionFinished()
onNavigateError(false,"Reversal failed")
}
TransResultStatus.ERROR -> {
onTransactionFinished()
onNavigateError(false,
transProcessViewModel.errorMessage.value?.takeIf { it.isNotBlank() }
?: "Something went wrong. Please try again."
)
}
else -> {}
}
}
}
if(sharedViewModel.transactionsType.value == TransactionsType.SETTLEMENT) {
ProcessingScreen(settlementStatus.processingState)
} else {
ProcessingScreen(state)
}
}
private fun requiresSignature(transType: TransactionsType?, payDetail: com.utsmyanmar.paylibs.model.PayDetail?): Boolean {
if (transType == TransactionsType.VOID || transType == TransactionsType.PRE_AUTH_COMPLETE_VOID || transType == TransactionsType.PRE_AUTH_VOID) {
return false
}
return true
}

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