Ref(DeviceInfo-PaymentResult-WebView):
- Refactor `PaymentResultEntity` to include a nested `PaymentResultDataEntity` and companion factory methods for success and failure. - Update `PaymentResultStatus` enum members from `OK`/`ERROR` to `SUCCESS`/`FAILURE`. - Modify `PspService` and its implementations (`P3`, `PS4`, `Stage`) to return a non-nullable `PaymentResultEntity`. - Enhance `PspWebView` with improved instance lifecycle management, `rememberUpdatedState` for callbacks, and new `onWebViewReady` and `onPageFinished` hooks. - Optimize `PspWebView` to prevent redundant `loadUrl` calls during recomposition. - Add `README.md` and `AGENT.md` to provide project architecture overviews and hardware-specific development guidelines. - Update `TisWebViewScreen` and `StageWebViewScreen` to handle the refactored result models and utilize new WebView callbacks.
This commit is contained in:
Generated
+14
@@ -12,10 +12,24 @@
|
||||
</SelectionState>
|
||||
<SelectionState runConfigName="tis_app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2026-05-12T16:21:58.593804400Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=TP030090489" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
<SelectionState runConfigName="stage_app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2026-05-12T16:21:58.593804400Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=TP030090489" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# Agent Instructions: PSP Project
|
||||
|
||||
This document provides essential context and guidelines for AI agents working on the PSP (Payment Service Provider) project.
|
||||
|
||||
## Project Purpose
|
||||
A multi-module Android application for specialized POS (Point of Sale) hardware, providing a bridge between a Web-based POS interface and native hardware features (Thermal Printing & Payment Terminal).
|
||||
|
||||
## Architecture Overview
|
||||
The project follows **Dependency Inversion** principles. Features are defined as interfaces in `:core` and implemented in hardware-specific modules like `:p3` or `:ps4`.
|
||||
|
||||
### Key Modules
|
||||
- **`:core`**: Shared domain models (`PrintEntity`, `PaymentResultEntity`) and interfaces (`PspService`, `Printer`).
|
||||
- **`:design_system`**: Contains `PspWebView` and the `PspJavaScriptInterface` (Web-Native Bridge).
|
||||
- **`:p3` / `:ps4`**: Hardware SDK integrations.
|
||||
- **`:app` / `:tis_app` / `:stage_app`**: Application entry points and Hilt dependency configuration.
|
||||
|
||||
## Critical Classes & Interfaces
|
||||
|
||||
### 1. Web-Native Bridge
|
||||
- **`PspWebView.kt`**: The core component. Uses a `WebViewManager` for pooling and is highly optimized for weak hardware.
|
||||
- **`PspJavaScriptInterface.kt`**: Defines the methods exposed to JavaScript (`pay`, `print`, `reload`, `uuid`).
|
||||
- **Important**: Methods must be annotated with `@JavascriptInterface` and `@Keep`.
|
||||
- **Note**: Numbers from JS arrive as `Double`.
|
||||
|
||||
### 2. Printer Implementation (`:p3`)
|
||||
- **`P3Printer.kt`**: Implements the `Printer` interface.
|
||||
- **`POIPrinterManager`**: The vendor SDK class.
|
||||
- **Font Handling**: Use `getFontPath(resourceName: String)` to copy fonts from `res/font` to internal storage before passing the path to the SDK.
|
||||
- **Logo Rendering**: Use `drawableToBitmap()` to handle VectorDrawables correctly.
|
||||
|
||||
### 3. Payment Service (`:p3`)
|
||||
- **`Ps3Service.kt`**: Implementation of `PspService`.
|
||||
- **`IapPosService.kt`**: Handles communication with the underlying POS payment application.
|
||||
|
||||
## Common Agent Tasks
|
||||
|
||||
### Adding a new Bridge Method
|
||||
1. Add the method to `PspJavaScriptInterface.kt` with `@JavascriptInterface` and `@Keep`.
|
||||
2. Update the constructor of `PspJavaScriptInterface` to take a lambda.
|
||||
3. Update `PspWebView.kt` to pass the lambda from the UI layer.
|
||||
|
||||
### Optimizing for Weak Hardware
|
||||
When performance issues arise in the WebView:
|
||||
1. Check `WebViewManager.kt` settings.
|
||||
2. Ensure `setLayerType(View.LAYER_TYPE_HARDWARE, null)` is set.
|
||||
3. Keep `setOffscreenPreRaster(false)` for very weak hardware to save RAM.
|
||||
4. Ensure `loadUrl` is NOT called in the `AndroidView.update` block (use `LaunchedEffect` instead).
|
||||
|
||||
## Development Rules
|
||||
- **RTL Support**: The printer uses Persian/Arabic text. Always ensure numbers are converted to Persian digits using `toPersianDigits()` where appropriate.
|
||||
- **Dependency Injection**: Use Hilt for all service injections. Do not instantiate implementation classes (`P3Printer`, `Ps3Service`) directly in the app modules.
|
||||
- **WebView Lifecycle**: Always release WebViews back to the `WebViewManager` in `onDispose`.
|
||||
|
||||
## Mocking in `:stage_app`
|
||||
- Use `StageViewModel` to return success/failure mocks for payment results without requiring physical hardware.
|
||||
- Use the `PaymentResultEntity.success()` or `PaymentResultEntity.failure()` factory methods.
|
||||
@@ -0,0 +1,49 @@
|
||||
# PSP (Payment Service Provider) Project
|
||||
|
||||
This project is a multi-module Android application designed to provide payment and printing services on specialized POS hardware. It uses a modular architecture to separate core logic, hardware SDKs, and different application variants.
|
||||
|
||||
## Module Structure
|
||||
|
||||
### Core Modules
|
||||
* **`:core`**: The backbone of the project. Contains shared domain models (`PaymentResultEntity`, `PrintEntity`), interfaces for services (`PspService`, `Printer`), and common utilities. It has no dependencies on other modules.
|
||||
* **`:design_system`**: Contains shared UI components and utilities. The most critical component is `PspWebView`, a highly optimized WebView wrapper used across all app variants for rendering the web-based POS interface. It depends on `:core`.
|
||||
|
||||
### Hardware Implementation Modules
|
||||
These modules contain the specific SDK integrations for different POS hardware.
|
||||
* **`:p3`**: Implementation of `PspService` and `Printer` for P3 hardware. Includes SDK-specific logic for handling payments and thermal printing.
|
||||
* **`:ps4`**: Implementation for PS4 hardware variants.
|
||||
|
||||
### Application Variants
|
||||
* **`:app`**: The primary production application.
|
||||
* **`:tis_app`**: A specialized version of the application targeting the TIS environment (`https://tis.shift-am.ir`).
|
||||
* **`:stage_app`**: A development and staging variant used for testing. It often uses mock implementations (like `StageViewModel`) to simulate POS behavior without requiring physical hardware.
|
||||
|
||||
## Architecture & Relationships
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph Apps
|
||||
tis_app --> core
|
||||
tis_app --> design_system
|
||||
stage_app --> core
|
||||
stage_app --> design_system
|
||||
app --> core
|
||||
app --> design_system
|
||||
end
|
||||
|
||||
subgraph Hardware SDKs
|
||||
p3 --> core
|
||||
ps4 --> core
|
||||
end
|
||||
|
||||
design_system --> core
|
||||
```
|
||||
|
||||
1. **Dependency Inversion**: Apps depend on interfaces defined in `:core`. The actual hardware implementation (`:p3` or `:ps4`) is injected via Hilt, allowing the same app logic to run on different devices.
|
||||
2. **Web-Native Bridge**: The `design_system` module provides `PspJavaScriptInterface`, which allows the Web POS (running inside `PspWebView`) to communicate with native hardware features like the printer and payment terminal.
|
||||
3. **Hardware Performance**: The WebView is optimized for weak POS hardware with features like pooled instances, hardware acceleration, and lean rendering profiles.
|
||||
|
||||
## Key Features
|
||||
* **Thermal Printing**: Optimized for RTL (Persian) text and high-performance bitmap rendering for logos (including VectorDrawable support).
|
||||
* **Payment Integration**: Standardized bridge for processing payments and returning structured results to the web-front-end.
|
||||
* **Weak Hardware Optimization**: Custom WebView configurations to handle smooth animations (drawers/sheets) on low-spec devices.
|
||||
@@ -12,5 +12,5 @@ interface PspService {
|
||||
fun decodePosResponse(
|
||||
id: String?,
|
||||
activityResult: ActivityResult
|
||||
): PaymentResultEntity?
|
||||
): PaymentResultEntity
|
||||
}
|
||||
@@ -1,15 +1,48 @@
|
||||
package com.example.core.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class PaymentResultEntity(
|
||||
val id : String? = null,
|
||||
val status : PaymentResultStatus,
|
||||
val terminalId : String,
|
||||
val stan : String,
|
||||
val rrn : String,
|
||||
val responseCode : String,
|
||||
val customerCardNO : String,
|
||||
val transactionDateTime : String,
|
||||
val description : String?,
|
||||
val id: String? = null,
|
||||
val status: PaymentResultStatus,
|
||||
val message: String?,
|
||||
@SerializedName("data")
|
||||
val paymentResultDataEntity: PaymentResultDataEntity? = null,
|
||||
) {
|
||||
data class PaymentResultDataEntity(
|
||||
val terminalId: String,
|
||||
val stan: String,
|
||||
val rrn: String,
|
||||
val responseCode: String,
|
||||
val customerCardNO: String,
|
||||
val transactionDateTime: String,
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun success(
|
||||
id: String? = null,
|
||||
message: String? = "Success",
|
||||
data: PaymentResultDataEntity? = null
|
||||
): PaymentResultEntity {
|
||||
return PaymentResultEntity(
|
||||
id = id,
|
||||
status = PaymentResultStatus.SUCCESS,
|
||||
message = message,
|
||||
paymentResultDataEntity = data
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
fun failure(
|
||||
id: String? = null,
|
||||
message: String? = "Failure",
|
||||
data: PaymentResultDataEntity? = null
|
||||
): PaymentResultEntity {
|
||||
return PaymentResultEntity(
|
||||
id = id,
|
||||
status = PaymentResultStatus.FAILURE,
|
||||
message = message,
|
||||
paymentResultDataEntity = data
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.example.core.model
|
||||
|
||||
enum class PaymentResultStatus {
|
||||
OK,
|
||||
ERROR
|
||||
SUCCESS,
|
||||
FAILURE
|
||||
}
|
||||
|
||||
@@ -46,13 +46,13 @@ fun PspWebView(
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
var lastBackTime by remember { mutableLongStateOf(0L) }
|
||||
var webView by remember { mutableStateOf<WebView?>(null) }
|
||||
var webViewInstance by remember { mutableStateOf<WebView?>(null) }
|
||||
|
||||
|
||||
// Use rememberUpdatedState to ensure the bridge always calls the latest callbacks
|
||||
val currentOnPayClick by rememberUpdatedState(onPayClick)
|
||||
val currentOnPrintClick by rememberUpdatedState(onPrintClick)
|
||||
val currentOnDeviceInfo by rememberUpdatedState(onDeviceInfo)
|
||||
val currentOnWebViewReady by rememberUpdatedState(onWebViewReady)
|
||||
val currentOnPageFinished by rememberUpdatedState(onPageFinished)
|
||||
|
||||
val bridge = remember {
|
||||
PspJavaScriptInterface(
|
||||
@@ -63,8 +63,8 @@ fun PspWebView(
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
if (canGoBack.value) {
|
||||
webView?.goBack()
|
||||
if (webViewInstance?.canGoBack() == true) {
|
||||
webViewInstance?.goBack()
|
||||
} else {
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastBackTime < 2000) {
|
||||
@@ -72,38 +72,34 @@ fun PspWebView(
|
||||
} else {
|
||||
lastBackTime = now
|
||||
scope.launch {
|
||||
SnackbarController.sendEvent(
|
||||
SnackbarEvent(
|
||||
message = "برای خروج، دوباره بر روی بازگشت کلیک کنید."
|
||||
)
|
||||
)
|
||||
SnackbarController.sendEvent(SnackbarEvent("برای خروج، دوباره کلیک کنید."))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Manage pooling and lifecycle
|
||||
DisposableEffect(Unit) {
|
||||
webView = WebViewManager.getWebView(context)
|
||||
webView?.let(onWebViewReady)
|
||||
onDispose {
|
||||
webView?.let { WebViewManager.releaseWebView(it) }
|
||||
webViewInstance?.let { WebViewManager.releaseWebView(it) }
|
||||
}
|
||||
}
|
||||
|
||||
AndroidView(
|
||||
modifier = modifier,
|
||||
factory = { ctx ->
|
||||
(webView ?: WebViewManager.getWebView(ctx)).apply {
|
||||
val wv = WebViewManager.getWebView(ctx).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
overScrollMode = View.OVER_SCROLL_NEVER
|
||||
|
||||
webViewClient = object : WebViewClient() {
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
canGoBack.value = view?.canGoBack() ?: false
|
||||
view?.let(onPageFinished)
|
||||
view?.let(currentOnPageFinished)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,16 +108,21 @@ fun PspWebView(
|
||||
javaScriptEnabled = true
|
||||
domStorageEnabled = true
|
||||
cacheMode = WebSettings.LOAD_NO_CACHE
|
||||
allowFileAccess = true // Needed for Service Worker
|
||||
allowFileAccess = true
|
||||
}
|
||||
clearCache(true)
|
||||
|
||||
removeJavascriptInterface("NativeBridge")
|
||||
addJavascriptInterface(bridge, "NativeBridge")
|
||||
}
|
||||
|
||||
webViewInstance = wv
|
||||
currentOnWebViewReady(wv) // CRITICAL: Send the ACTUAL instance being shown
|
||||
wv
|
||||
},
|
||||
update = {
|
||||
if (it.url != url) {
|
||||
it.loadUrl(url)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@@ -159,24 +159,33 @@ class IapPosService @Inject constructor(
|
||||
|
||||
|
||||
if (result.isNullOrEmpty()) {
|
||||
throw Exception("پرداخت کنسل شد")
|
||||
return PaymentResultEntity.failure(
|
||||
id = id,
|
||||
message = "پرداخت لغو شد!"
|
||||
)
|
||||
}
|
||||
return try {
|
||||
val type = object : TypeToken<Map<String, Any>>() {}.type
|
||||
val data = gson.fromJson<Map<String, Any>>(result, type)
|
||||
PaymentResultEntity(
|
||||
id = id,
|
||||
status = if (data["Status"] == "OK") PaymentResultStatus.OK else PaymentResultStatus.ERROR,
|
||||
status = if (data["Status"] == "OK") PaymentResultStatus.SUCCESS else PaymentResultStatus.FAILURE,
|
||||
paymentResultDataEntity = PaymentResultEntity.PaymentResultDataEntity(
|
||||
terminalId = data["TerminalId"].toString(),
|
||||
stan = data["STAN"].toString(),
|
||||
rrn = data["RRN"].toString(),
|
||||
responseCode = data["ResponseCode"].toString(),
|
||||
customerCardNO = data["CustomerCardNO"].toString(),
|
||||
transactionDateTime = data["TransactionDateTime"].toString(),
|
||||
description = data["Description"].toString()
|
||||
),
|
||||
message = data["Description"].toString()
|
||||
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
throw Exception("POS response is not a JSON object.")
|
||||
PaymentResultEntity.failure(
|
||||
id = id,
|
||||
message = e.message ?: "در هنگام پرداخت خطایی رخ داده است!"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,14 +30,18 @@ class P3Service @Inject constructor(
|
||||
override fun decodePosResponse(
|
||||
id: String?,
|
||||
activityResult: ActivityResult
|
||||
): PaymentResultEntity? {
|
||||
): PaymentResultEntity {
|
||||
return try {
|
||||
iapPosService.decodeResponse(
|
||||
id = id,
|
||||
activityResult = activityResult
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
e.printStackTrace()
|
||||
PaymentResultEntity.failure(
|
||||
id = id,
|
||||
e.message
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,5 +18,5 @@ class Ps4Service @Inject constructor() : PspService {
|
||||
override fun decodePosResponse(
|
||||
id: String?,
|
||||
activityResult: ActivityResult
|
||||
): PaymentResultEntity? = null
|
||||
): PaymentResultEntity = PaymentResultEntity.success()
|
||||
}
|
||||
|
||||
@@ -54,13 +54,13 @@ fun StageWebViewScreen(
|
||||
activityResult = result
|
||||
)
|
||||
val jsResult = viewModel.postPaymentResult(paymentResultEntity)
|
||||
val currentWebView = webView
|
||||
if (currentWebView == null) {
|
||||
Log.w(stageWebViewTag, "Skipping onPaymentResult JS callback: WebView is not ready yet.")
|
||||
} else {
|
||||
currentWebView.post {
|
||||
currentWebView.evaluateJavascript(jsResult, null)
|
||||
webView?.let {
|
||||
it.post {
|
||||
it.evaluateJavascript(jsResult, null)
|
||||
}
|
||||
} ?: run {
|
||||
Log.w(stageWebViewTag, "Skipping onPaymentResult JS callback: WebView is not ready yet.")
|
||||
|
||||
}
|
||||
isPaymentProcessing = false
|
||||
}
|
||||
|
||||
@@ -32,17 +32,23 @@ class StageViewModel @Inject constructor(
|
||||
id: String?,
|
||||
activityResult: ActivityResult
|
||||
): PaymentResultEntity {
|
||||
return PaymentResultEntity(
|
||||
val mockPaymentResult = PaymentResultEntity.success(
|
||||
id = id,
|
||||
status = com.example.core.model.PaymentResultStatus.ERROR,
|
||||
message = "تراکنش با موفقیت انجام شد",
|
||||
data = PaymentResultEntity.PaymentResultDataEntity(
|
||||
terminalId = "123456",
|
||||
stan = "123456",
|
||||
rrn = "123456",
|
||||
responseCode = "01",
|
||||
customerCardNO = "1234567890123456",
|
||||
transactionDateTime = "14030101120000",
|
||||
description = "تراکنش ناموفق"
|
||||
rrn = "123456789012",
|
||||
responseCode = "00",
|
||||
customerCardNO = "603799******1234",
|
||||
transactionDateTime = "1403/05/20 12:30:45"
|
||||
)
|
||||
)
|
||||
return mockPaymentResult
|
||||
// return pspService.decodePosResponse(
|
||||
// id = id,
|
||||
// activityResult = activityResult
|
||||
// )
|
||||
}
|
||||
|
||||
private val mock = listOf<PrintEntity>(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.example.tis_app.screen
|
||||
|
||||
import android.util.Log
|
||||
import android.webkit.WebView
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@@ -17,10 +18,10 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.example.core.getAndroidId
|
||||
import com.example.core.getDeviceName
|
||||
import com.example.design_system.PspWebView
|
||||
import com.example.design_system.util.WebViewManager
|
||||
import com.example.tis_app.viewmodel.WebViewModel
|
||||
|
||||
private const val tisWebUrl = "https://tis.shift-am.ir"
|
||||
private const val tisWebViewTag = "TisWebViewScreen"
|
||||
|
||||
@Composable
|
||||
fun TisWebViewScreen(
|
||||
@@ -29,10 +30,11 @@ fun TisWebViewScreen(
|
||||
) {
|
||||
|
||||
val context = LocalContext.current
|
||||
var webView: WebView by remember { mutableStateOf(WebViewManager.getWebView(context)) }
|
||||
var webView: WebView? by remember { mutableStateOf(null) }
|
||||
var currentPaymentId by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
var isPaymentProcessing by rememberSaveable { mutableStateOf(false) }
|
||||
var isPrinting by rememberSaveable { mutableStateOf(false) }
|
||||
var initialCallbackSent by rememberSaveable { mutableStateOf(false) }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val deviceInfo by remember {
|
||||
mutableStateOf(
|
||||
@@ -50,17 +52,16 @@ fun TisWebViewScreen(
|
||||
val paymentResultEntity = viewModel.paymentResult(
|
||||
id = currentPaymentId,
|
||||
activityResult = result
|
||||
|
||||
)
|
||||
|
||||
webView.post {
|
||||
webView.evaluateJavascript(
|
||||
viewModel.postPaymentResult(paymentResultEntity = paymentResultEntity),
|
||||
null
|
||||
)
|
||||
val jsResult = viewModel.postPaymentResult(paymentResultEntity)
|
||||
webView?.let {
|
||||
it.post {
|
||||
it.evaluateJavascript(jsResult, null)
|
||||
}
|
||||
} ?: run {
|
||||
Log.w(tisWebViewTag, "Skipping onPaymentResult JS callback: WebView is not ready yet.")
|
||||
}
|
||||
isPaymentProcessing = false
|
||||
|
||||
}
|
||||
|
||||
PspWebView(
|
||||
@@ -86,6 +87,15 @@ fun TisWebViewScreen(
|
||||
},
|
||||
onDeviceInfo = {
|
||||
deviceInfo
|
||||
},
|
||||
onWebViewReady = { readyWebView ->
|
||||
webView = readyWebView
|
||||
},
|
||||
onPageFinished = { loadedWebView ->
|
||||
if (!initialCallbackSent) {
|
||||
loadedWebView.evaluateJavascript(viewModel.postPaymentResult(null), null)
|
||||
initialCallbackSent = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -32,7 +32,7 @@ class WebViewModel @Inject constructor(
|
||||
fun paymentResult(
|
||||
id: String?,
|
||||
activityResult: ActivityResult
|
||||
): PaymentResultEntity? {
|
||||
): PaymentResultEntity {
|
||||
return pspService.decodePosResponse(
|
||||
id = id,
|
||||
activityResult = activityResult
|
||||
|
||||
Reference in New Issue
Block a user