Compare commits

..

3 Commits

Author SHA1 Message Date
Amir Mousavi 5d60f62c20 Feat(P3): Modularize module and add Pardakht Novin support
- Split `p3` module into `p3_pardakht_novin` and `p3_sepehr` sub-modules
- Create `pardakht_novin_app` module with dedicated WebView and Hilt configuration
- Rename P3 services, printers, and models to be vendor-specific (e.g., `P3PardakhtNovinService`)
- Add `p3_pardakht_novin` and `p3_sepehr` product flavors to `tis_app` and `stage_app`
- Update project settings and build configurations to include the new modular structure
- Implement Sepehr-specific POS and Printer services in the new module
2026-06-16 00:18:45 +03:30
Amir Mousavi 1246a35771 Feat(Core): Pass payment result data to WebView
Update the `onPaymentResult` JavaScript callback to include the serialized `PaymentResultEntity`.
2026-06-08 10:43:11 +03:30
Amir Mousavi 08060001e1 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.
2026-05-17 13:40:32 +03:30
96 changed files with 2284 additions and 136 deletions
+3 -1
View File
@@ -1,6 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="CompilerConfiguration"> <component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" /> <bytecodeTargetLevel target="21">
<module name="PSP.p3_pardakht_novin" target="21" />
</bytecodeTargetLevel>
</component> </component>
</project> </project>
+52 -5
View File
@@ -6,18 +6,65 @@
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DialogSelection /> <DialogSelection />
</SelectionState> </SelectionState>
<SelectionState runConfigName="tis_sep">
<option name="selectionMode" value="DROPDOWN" />
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="tis_app"> <SelectionState runConfigName="tis_app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DialogSelection /> <DropdownSelection timestamp="2026-06-08T07:38:22.052645700Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\Amir Mousavi\.android\avd\Pixel_9_Pro.avd" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection>
<targets>
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=TP030078028" />
</handle>
</Target>
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\Amir Mousavi\.android\avd\Pixel_9_Pro.avd" />
</handle>
</Target>
</targets>
</DialogSelection>
</SelectionState> </SelectionState>
<SelectionState runConfigName="stage_app"> <SelectionState runConfigName="stage_app">
<option name="selectionMode" value="DROPDOWN" /> <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 /> <DialogSelection />
</SelectionState> </SelectionState>
<SelectionState runConfigName="pardakht_novin_app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-06-08T07:38:22.052645700Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\Amir Mousavi\.android\avd\Pixel_9_Pro.avd" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection>
<targets>
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=TP030078028" />
</handle>
</Target>
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\Amir Mousavi\.android\avd\Pixel_9_Pro.avd" />
</handle>
</Target>
</targets>
</DialogSelection>
</SelectionState>
</selectionStates> </selectionStates>
</component> </component>
</project> </project>
+7
View File
@@ -0,0 +1,7 @@
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>Novin</w>
</words>
</dictionary>
</component>
+3
View File
@@ -13,6 +13,9 @@
<option value="$PROJECT_DIR$/core" /> <option value="$PROJECT_DIR$/core" />
<option value="$PROJECT_DIR$/design_system" /> <option value="$PROJECT_DIR$/design_system" />
<option value="$PROJECT_DIR$/p3" /> <option value="$PROJECT_DIR$/p3" />
<option value="$PROJECT_DIR$/p3/p3_pardakht_novin" />
<option value="$PROJECT_DIR$/p3/p3_sepehr" />
<option value="$PROJECT_DIR$/pardakht_novin_app" />
<option value="$PROJECT_DIR$/ps4" /> <option value="$PROJECT_DIR$/ps4" />
<option value="$PROJECT_DIR$/stage_app" /> <option value="$PROJECT_DIR$/stage_app" />
<option value="$PROJECT_DIR$/tis_app" /> <option value="$PROJECT_DIR$/tis_app" />
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="externalSystemId" value="Gradle" />
<option name="version" value="2.3.10" />
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MarkdownSettings">
<option name="previewPanelProviderInfo">
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
</option>
</component>
</project>
+1
View File
@@ -4,6 +4,7 @@
<option name="approvalStates"> <option name="approvalStates">
<map> <map>
<entry key="20260504-162003-d0f92a5d-1a15-429a-82cc-cad6ed3c0e3e" value="true" /> <entry key="20260504-162003-d0f92a5d-1a15-429a-82cc-cad6ed3c0e3e" value="true" />
<entry key="20260608-113233-cbd224e1-fbde-45e9-8023-20976cbde20f" value="true" />
</map> </map>
</option> </option>
</component> </component>
+56
View File
@@ -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.
+49
View File
@@ -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.
+1
View File
@@ -5,4 +5,5 @@ plugins {
alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.kotlin.compose) apply false
id("com.google.dagger.hilt.android") version "2.59.2" apply false id("com.google.dagger.hilt.android") version "2.59.2" apply false
alias(libs.plugins.ksp) apply false alias(libs.plugins.ksp) apply false
alias(libs.plugins.jetbrains.kotlin.jvm) apply false
} }
@@ -10,8 +10,7 @@ class PaymentResultImpl @Inject constructor(
) : PaymentResult { ) : PaymentResult {
override fun post(paymentResultEntity: PaymentResultEntity?): String { override fun post(paymentResultEntity: PaymentResultEntity?): String {
return "window.WebV.onPaymentResult();" return "window.WebV.onPaymentResult(${gson.toJson(paymentResultEntity)});"
// return "window.WebV.onPaymentResult(${gson.toJson(paymentResultEntity)});"
} }
} }
@@ -12,5 +12,5 @@ interface PspService {
fun decodePosResponse( fun decodePosResponse(
id: String?, id: String?,
activityResult: ActivityResult activityResult: ActivityResult
): PaymentResultEntity? ): PaymentResultEntity
} }
@@ -1,15 +1,48 @@
package com.example.core.model package com.example.core.model
import com.google.gson.annotations.SerializedName
data class PaymentResultEntity( data class PaymentResultEntity(
val id : String? = null, val id: String? = null,
val status : PaymentResultStatus, val status: PaymentResultStatus,
val terminalId : String, val message: String?,
val stan : String, @SerializedName("data")
val rrn : String, val paymentResultDataEntity: PaymentResultDataEntity? = null,
val responseCode : String, ) {
val customerCardNO : String, data class PaymentResultDataEntity(
val transactionDateTime : String, val terminalId: String,
val description : 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 package com.example.core.model
enum class PaymentResultStatus { enum class PaymentResultStatus {
OK, SUCCESS,
ERROR FAILURE
} }
@@ -46,13 +46,13 @@ fun PspWebView(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val context = LocalContext.current val context = LocalContext.current
var lastBackTime by remember { mutableLongStateOf(0L) } 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 currentOnPayClick by rememberUpdatedState(onPayClick)
val currentOnPrintClick by rememberUpdatedState(onPrintClick) val currentOnPrintClick by rememberUpdatedState(onPrintClick)
val currentOnDeviceInfo by rememberUpdatedState(onDeviceInfo) val currentOnDeviceInfo by rememberUpdatedState(onDeviceInfo)
val currentOnWebViewReady by rememberUpdatedState(onWebViewReady)
val currentOnPageFinished by rememberUpdatedState(onPageFinished)
val bridge = remember { val bridge = remember {
PspJavaScriptInterface( PspJavaScriptInterface(
@@ -63,8 +63,8 @@ fun PspWebView(
} }
BackHandler { BackHandler {
if (canGoBack.value) { if (webViewInstance?.canGoBack() == true) {
webView?.goBack() webViewInstance?.goBack()
} else { } else {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
if (now - lastBackTime < 2000) { if (now - lastBackTime < 2000) {
@@ -72,38 +72,34 @@ fun PspWebView(
} else { } else {
lastBackTime = now lastBackTime = now
scope.launch { scope.launch {
SnackbarController.sendEvent( SnackbarController.sendEvent(SnackbarEvent("برای خروج، دوباره کلیک کنید."))
SnackbarEvent(
message = "برای خروج، دوباره بر روی بازگشت کلیک کنید."
)
)
} }
} }
} }
} }
// Manage pooling and lifecycle
DisposableEffect(Unit) { DisposableEffect(Unit) {
webView = WebViewManager.getWebView(context)
webView?.let(onWebViewReady)
onDispose { onDispose {
webView?.let { WebViewManager.releaseWebView(it) } webViewInstance?.let { WebViewManager.releaseWebView(it) }
} }
} }
AndroidView( AndroidView(
modifier = modifier, modifier = modifier,
factory = { ctx -> factory = { ctx ->
(webView ?: WebViewManager.getWebView(ctx)).apply { val wv = WebViewManager.getWebView(ctx).apply {
layoutParams = ViewGroup.LayoutParams( layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT ViewGroup.LayoutParams.MATCH_PARENT
) )
overScrollMode = View.OVER_SCROLL_NEVER overScrollMode = View.OVER_SCROLL_NEVER
webViewClient = object : WebViewClient() { webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) { override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url) super.onPageFinished(view, url)
canGoBack.value = view?.canGoBack() ?: false canGoBack.value = view?.canGoBack() ?: false
view?.let(onPageFinished) view?.let(currentOnPageFinished)
} }
} }
@@ -112,16 +108,21 @@ fun PspWebView(
javaScriptEnabled = true javaScriptEnabled = true
domStorageEnabled = true domStorageEnabled = true
cacheMode = WebSettings.LOAD_NO_CACHE cacheMode = WebSettings.LOAD_NO_CACHE
allowFileAccess = true // Needed for Service Worker allowFileAccess = true
} }
clearCache(true)
removeJavascriptInterface("NativeBridge")
addJavascriptInterface(bridge, "NativeBridge") addJavascriptInterface(bridge, "NativeBridge")
} }
webViewInstance = wv
currentOnWebViewReady(wv) // CRITICAL: Send the ACTUAL instance being shown
wv
}, },
update = { update = {
if (it.url != url) {
it.loadUrl(url) it.loadUrl(url)
} }
}
) )
} }
+2
View File
@@ -14,6 +14,7 @@ hilt = "2.59.2"
ksp = "2.3.6" ksp = "2.3.6"
retrofit = "2.11.0" retrofit = "2.11.0"
okhttp = "4.12.0" okhttp = "4.12.0"
jetbrainsKotlinJvm = "2.3.10"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -46,3 +47,4 @@ android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "jetbrainsKotlinJvm" }
@@ -5,7 +5,7 @@ plugins {
} }
android { android {
namespace = "com.example.p3" namespace = "com.example.p3_pardakht_novin"
compileSdk { compileSdk {
version = release(36) { version = release(36) {
minorApiLevel = 1 minorApiLevel = 1
@@ -1,4 +1,4 @@
package com.example.p3 package com.example.p3_pardakht_novin
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -1,4 +1,4 @@
package com.example.p3 package com.example.p3_pardakht_novin
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -6,15 +6,16 @@ import android.content.pm.PackageManager
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import com.example.core.model.PaymentResultEntity import com.example.core.model.PaymentResultEntity
import com.example.core.model.PaymentResultStatus import com.example.core.model.PaymentResultStatus
import com.example.p3.models.IapAccountSplit import com.example.p3_pardakht_novin.models.IapAccountSplit
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class IapPosService @Inject constructor( class P3PardakhtNovinIapPosService @Inject constructor(
private val context: Context @param:ApplicationContext private val context: Context
) { ) {
private val gson = Gson() private val gson = Gson()
@@ -159,24 +160,33 @@ class IapPosService @Inject constructor(
if (result.isNullOrEmpty()) { if (result.isNullOrEmpty()) {
throw Exception("پرداخت کنسل شد") return PaymentResultEntity.failure(
id = id,
message = "پرداخت لغو شد!"
)
} }
return try { return try {
val type = object : TypeToken<Map<String, Any>>() {}.type val type = object : TypeToken<Map<String, Any>>() {}.type
val data = gson.fromJson<Map<String, Any>>(result, type) val data = gson.fromJson<Map<String, Any>>(result, type)
PaymentResultEntity( PaymentResultEntity(
id = id, 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(), terminalId = data["TerminalId"].toString(),
stan = data["STAN"].toString(), stan = data["STAN"].toString(),
rrn = data["RRN"].toString(), rrn = data["RRN"].toString(),
responseCode = data["ResponseCode"].toString(), responseCode = data["ResponseCode"].toString(),
customerCardNO = data["CustomerCardNO"].toString(), customerCardNO = data["CustomerCardNO"].toString(),
transactionDateTime = data["TransactionDateTime"].toString(), transactionDateTime = data["TransactionDateTime"].toString(),
description = data["Description"].toString() ),
message = data["Description"].toString()
) )
} catch (e: Exception) { } catch (e: Exception) {
throw Exception("POS response is not a JSON object.") PaymentResultEntity.failure(
id = id,
message = e.message ?: "در هنگام پرداخت خطایی رخ داده است!"
)
} }
} }
} }
@@ -0,0 +1,335 @@
package com.example.p3_pardakht_novin
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.BitmapDrawable
import android.util.Log
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import androidx.core.graphics.createBitmap
import com.example.core.domain.Printer
import com.example.core.model.PrintEntity
import com.pos.sdk.printer.POIPrinterManager
import com.pos.sdk.printer.POIPrinterManager.IPrinterListener
import com.pos.sdk.printer.models.BitmapPrintLine
import com.pos.sdk.printer.models.PrintLine
import com.pos.sdk.printer.models.TextPrintLine
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File
import javax.inject.Inject
class P3PardakhtNovinPrinter @Inject constructor(
@param:ApplicationContext private val context: Context,
) : Printer {
companion object {
private const val TAG = "Printer"
// Printer state constants (should ideally come from the SDK if available)
private const val PRINTER_STATE_ERROR = 4
private const val DEFAULT_PRINT_GRAY = 2000
private const val DEFAULT_LINE_SPACE = 5
}
/**
* Prints a sample receipt for demonstration.
*/
fun printSampleReceipt(context: Context) {
// Use applicationContext to avoid potential memory leaks
val printerManager = initialP3Printer()
printerManager?.apply {
setPrintGray(DEFAULT_PRINT_GRAY)
setLineSpace(DEFAULT_LINE_SPACE)
// Header
addPrintLine(TextPrintLine("This is an example of a receipt", PrintLine.CENTER))
// Reusable logo bitmap
val logoBitmap = drawableToBitmap(context, android.R.drawable.btn_plus)
val logoLine = logoBitmap?.let { BitmapPrintLine(it, PrintLine.CENTER) }
logoLine?.let { addPrintLine(it) }
// Address
val addressFont = getFontPath("font")
if (addressFont.isNotEmpty()) {
setPrintFont(addressFont)
} else {
setPrintFont("/system/fonts/ComingSoon.ttf")
}
addPrintLine(
TextPrintLine(
"Floor ** , Building **, No.*** LONG DONG Avenue, Pudong New District, Shanghai, China",
PrintLine.LEFT,
20
)
)
// Transaction Details
val detailFont = getFontPath("font")
if (detailFont.isNotEmpty()) {
setPrintFont(detailFont)
} else {
setPrintFont("/system/fonts/DroidSansMono.ttf")
}
// addPrintLine(createPrintRow("24 June 2025", " Assistant 6", "815002", size = 18))
// addPrintLine(createPrintRow("Item", "Quantity", "Price", size = 24, isBold = true))
// addPrintLine(createPrintRow("Tomato", "1", "$2.08", size = 24))
// addPrintLine(createPrintRow("Orange", "1", "$1.06", size = 24))
//
// Footer
addPrintLine(TextPrintLine("Total $3.14", PrintLine.RIGHT))
addPrintLine(TextPrintLine(""))
logoLine?.let { addPrintLine(it) }
addPrintLine(TextPrintLine(""))
addPrintLine(
TextPrintLine(
"Did you know you could have earned Rewards points on this purchase?",
PrintLine.CENTER
)
)
addPrintLine(
TextPrintLine(
"Simply sign up today for a Membership Card!",
PrintLine.CENTER
)
)
logoLine?.let { addPrintLine(it) }
addPrintLine(TextPrintLine(" ", 0, 100))
val listener = object : IPrinterListener {
override fun onStart() {
Log.d(TAG, "Print started")
}
override fun onFinish() {
Log.d(TAG, "Print finished")
close()
}
override fun onError(code: Int, msg: String?) {
Log.e(TAG, "Print error - code: $code, message: $msg")
close()
}
}
beginPrint(listener)
}
}
private fun initialP3Printer(): POIPrinterManager? {
val printerManager = POIPrinterManager(context.applicationContext)
printerManager.open()
val state = printerManager.printerState
Log.d(TAG, "Printer state = $state")
if (state == PRINTER_STATE_ERROR) {
Log.e(TAG, "Printer is in an error state ($state). Aborting print.")
printerManager.close()
return null
}
return printerManager
}
/**
* Creates a row with left, center, and right aligned text.
*/
private fun createPrintRow(
label: String,
value: String,
size: Int,
isBold: Boolean = false
): List<TextPrintLine> {
return if (value.hasPersianLetters()) {
listOf(
TextPrintLine(label, PrintLine.LEFT, size, true),
TextPrintLine(value, PrintLine.RIGHT, size, isBold)
)
} else {
listOf(
TextPrintLine(label, PrintLine.LEFT, size, true),
TextPrintLine(value, PrintLine.LEFT, size, isBold)
)
}
}
private fun getFontPath(resourceName: String): String {
return try {
// Find resource ID by name (e.g., "font") in the "font" resource folder
val resId = context.resources.getIdentifier(resourceName, "font", context.packageName)
if (resId == 0) {
Log.e(TAG, "Font resource not found: $resourceName")
return ""
}
val fileName = "$resourceName.ttf"
val fontFile = File(context.filesDir, fileName)
if (!fontFile.exists()) {
context.resources.openRawResource(resId).use { input ->
fontFile.outputStream().use { output ->
input.copyTo(output)
}
}
}
fontFile.absolutePath
} catch (e: Exception) {
Log.e(TAG, "Error loading font from resources: $resourceName", e)
""
}
}
override fun print(
printEntities: List<PrintEntity>,
@DrawableRes icon: Int
) {
val printerManager = initialP3Printer()
printerManager?.apply {
setPrintGray(DEFAULT_PRINT_GRAY)
setLineSpace(DEFAULT_LINE_SPACE)
// Reusable logo bitmap
val logoBitmap = drawableToBitmap(context, icon, 200)
if (logoBitmap != null) {
val logoLine = BitmapPrintLine(logoBitmap, PrintLine.CENTER)
addPrintLine(logoLine)
addPrintLine(TextPrintLine(" ", 0, 10))
}
val customFontPath = getFontPath("font")
if (customFontPath.isNotEmpty()) {
setPrintFont(customFontPath)
} else {
setPrintFont("/system/fonts/DroidSansMono.ttf") // Fallback
}
val divider = drawableToBitmap(context, com.example.core.R.drawable.horizontal_line_svgrepo_com__1_)
printEntities.forEachIndexed { index, printEntity ->
// Header
printEntity.title?.let {title->
addPrintLine(
TextPrintLine(
title,
if (title.hasPersianLetters()) PrintLine.LEFT else PrintLine.RIGHT
)
)
}
printEntity.items?.let {items->
items.forEach { itemEntity ->
addPrintLine(
createPrintRow(
label = itemEntity.label,
value = itemEntity.value,
size = 16,
)
)
}
}
if (divider != null && printEntities.lastIndex > index) {
val dividerLine = BitmapPrintLine(divider, PrintLine.CENTER)
addPrintLine(TextPrintLine(" ", 0, 10))
addPrintLine(dividerLine)
addPrintLine(TextPrintLine(" ", 0, 10))
}
}
addPrintLine(TextPrintLine(" ", 0, 60))
val listener = object : IPrinterListener {
override fun onStart() {
Log.d(TAG, "Print started")
}
override fun onFinish() {
Log.d(TAG, "Print finished")
close()
}
override fun onError(code: Int, msg: String?) {
Log.e(TAG, "Print error - code: $code, message: $msg")
close()
}
}
beginPrint(listener)
}
}
// private fun fixForPrinter(input: String): String {
// val LRM = '\u200E' // کنترل جهت
//
// return input
// .toPersianDigits() // همونی که قبل دادم
// .replace(Regex("(\\d+)")) { matchResult ->
// "$LRM${matchResult.value}$LRM"
// }
// }
private fun String.hasPersianLetters(): Boolean {
return Regex("[\\u0621-\\u0628\\u062A-\\u063A\\u0641-\\u0642\\u0644-\\u0648\\u067E\\u0686\\u0698\\u06A9\\u06AF\\u06CC\\u0647]").containsMatchIn(
this
)
}
private fun drawableToBitmap(context: Context, @DrawableRes drawableId: Int): Bitmap? {
val drawable = ContextCompat.getDrawable(context, drawableId) ?: return null
if (drawable is BitmapDrawable) {
return drawable.bitmap
}
val bitmap = createBitmap(
drawable.intrinsicWidth.coerceAtLeast(1),
drawable.intrinsicHeight.coerceAtLeast(1)
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}
private fun drawableToBitmap(
context: Context,
@DrawableRes drawableId: Int,
maxWidth: Int = 300
): Bitmap? {
val drawable = ContextCompat.getDrawable(context, drawableId) ?: return null
val original = if (drawable is BitmapDrawable) {
drawable.bitmap
} else {
val bitmap = createBitmap(
drawable.intrinsicWidth.coerceAtLeast(1),
drawable.intrinsicHeight.coerceAtLeast(1)
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
bitmap
}
// 👇 scale با حفظ نسبت
val ratio = maxWidth.toFloat() / original.width
val width = maxWidth
val height = (original.height * ratio).toInt()
return Bitmap.createScaledBitmap(original, width, height, true)
}
}
@@ -0,0 +1,173 @@
package com.example.p3_pardakht_novin
import android.device.PrinterManager
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.Log
import com.google.gson.GsonBuilder
import com.google.gson.JsonElement
import com.google.gson.JsonParser
import javax.inject.Inject
import javax.inject.Singleton
/**
* Helper class for interacting with the POS Printer.
*/
@Singleton
class P3PardakhtNovinPrinterHelper @Inject constructor() {
private var printer: PrinterManager? = null
private val gson = GsonBuilder().setPrettyPrinting().create()
private val textPaint = Paint().apply {
textSize = 24f
color = Color.BLACK
isAntiAlias = true
}
init {
detectPrinter()
}
private fun detectPrinter() {
try {
// Attempt to instantiate the manager
val manager = PrinterManager()
try {
// Test the implementation.
// We use getStatus() to see if it's the system implementation or the stub.
val status = manager.getStatus()
printer = manager
Log.d("PrinterHelper", "PrinterManager initialized. Initial status: $status")
} catch (e: Exception) {
val errorMsg = e.cause?.message ?: e.message ?: ""
Log.w("PrinterHelper", "getStatus() failed with: $errorMsg")
if (errorMsg.contains("stub", ignoreCase = true)) {
Log.e("PrinterHelper", "CRITICAL: The SDK JAR is being bundled in your APK. You must Clean and Rebuild.")
} else {
// If it's not a 'stub' error, it might just be the hardware state
printer = manager
}
}
} catch (e: Throwable) {
Log.e("PrinterHelper", "PrinterManager not available on this device: ${e.message}")
}
}
fun getStatusMessage(code: Int): String {
return when (code) {
0 -> "Printer is healthy"
-1 -> "No paper in printer"
2 -> "Device is overheating"
-3 -> "Battery is too low to print"
else -> "Hardware error code: $code"
}
}
fun printJson(jsonData: String, onStatus: (String) -> Unit) {
try {
val jsonElement: JsonElement = JsonParser.parseString(jsonData)
val prettyJson = gson.toJson(jsonElement)
printLines(prettyJson.split("\n"), onStatus)
} catch (e: Exception) {
onStatus("Invalid JSON data provided.")
}
}
fun printLines(lines: List<String>, onStatus: (String) -> Unit) {
if (printer == null) {
detectPrinter()
}
val currentPrinter = printer
if (currentPrinter == null) {
onStatus("Printer hardware not supported or SDK issue.")
return
}
Log.d("PrinterHelper", "Starting print job...")
var bitmap: Bitmap? = null
try {
currentPrinter.open()
currentPrinter.setupPage(384, -1)
currentPrinter.clearPage()
val lineHeight = 32
val totalHeight = (lines.size * lineHeight) + 40
bitmap = Bitmap.createBitmap(384, totalHeight, Bitmap.Config.ARGB_8888)
bitmap.eraseColor(Color.WHITE)
val canvas = Canvas(bitmap)
var yPos = 30f
for (line in lines) {
canvas.drawText(line, 10f, yPos, textPaint)
yPos += lineHeight
}
currentPrinter.drawBitmap(bitmap, 0, 0)
val printResult = currentPrinter.printPage(0)
if (printResult == 0) {
currentPrinter.paperFeed(20)
onStatus("Print Job Completed Successfully")
} else {
onStatus(getStatusMessage(printResult))
}
} catch (e: Exception) {
onStatus("Printer Error: ${e.localizedMessage}")
Log.e("PrinterHelper", "Unexpected error during printing", e)
} finally {
bitmap?.recycle()
try {
currentPrinter.close()
} catch (e: Throwable) {}
}
}
/*
fun printText(): Int {
val manager = printer
val mCenterPaint = Paint().apply {
textSize = 30f
color = Color.BLACK
}
var mBitmap: Bitmap? = null
manager.setupPage(384, -1)
manager.clearPage()
val mconfig = Bitmap.Config.ARGB_8888
mBitmap = Bitmap.createBitmap(384, 2000, mconfig)
mBitmap.eraseColor(-0x1) // 0xffffffff
val mCanvas = Canvas(mBitmap)
mCanvas.drawText("سلام", 330f, 30f, mCenterPaint)
mCanvas.drawText("حال شما خوب هست", 165f, 60f, mCenterPaint)
mCanvas.drawText("سلام", 330f, 90f, mCenterPaint)
mCanvas.drawText("حال شما خوب هست", 165f, 120f, mCenterPaint)
var mirrored: Bitmap? = Bitmap.createBitmap(mBitmap, 0, 0, mBitmap.width, 130)
manager.drawBitmap(mirrored, 0, 0)
val ret = manager.printPage(0)
manager.paperFeed(15)
mBitmap?.let {
it.recycle()
mBitmap = null
}
mirrored?.let {
it.recycle()
mirrored = null
}
return ret
}
*/
}
@@ -0,0 +1,47 @@
package com.example.p3_pardakht_novin
import android.content.Intent
import androidx.activity.result.ActivityResult
import com.example.core.domain.PspService
import com.example.core.model.PaymentResultEntity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import javax.inject.Inject
class P3PardakhtNovinService @Inject constructor(
private val p3PardakhtNovinIapPosService: P3PardakhtNovinIapPosService,
private val p3PardakhtNovinPrinterHelper: P3PardakhtNovinPrinterHelper
) : PspService {
private val scope = CoroutineScope(Dispatchers.IO)
override fun pay(amount: Long): Intent {
return p3PardakhtNovinIapPosService.startPurchase(amount)
}
override fun print(lines: List<String>, onMessage: (String) -> Unit) {
p3PardakhtNovinPrinterHelper.printLines(lines) { status ->
onMessage("P3 Printer: $status")
}
}
override fun decodePosResponse(
id: String?,
activityResult: ActivityResult
): PaymentResultEntity {
return try {
p3PardakhtNovinIapPosService.decodeResponse(
id = id,
activityResult = activityResult
)
} catch (e: Exception) {
e.printStackTrace()
PaymentResultEntity.failure(
id = id,
e.message
)
}
}
}
@@ -0,0 +1,11 @@
package com.example.p3_pardakht_novin.di
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
object P3PardakhtNovinModule {
// P3PardakhtNovinIapPosService is provided via @Inject constructor
}
@@ -1,4 +1,4 @@
package com.example.p3.models package com.example.p3_pardakht_novin.models
data class IapAccountSplit( data class IapAccountSplit(
val index: Int, val index: Int,
@@ -1,4 +1,4 @@
package com.example.p3 package com.example.p3_pardakht_novin
import org.junit.Test import org.junit.Test
+1
View File
@@ -0,0 +1 @@
/build
+49
View File
@@ -0,0 +1,49 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.ksp)
alias(libs.plugins.hilt)
}
android {
namespace = "com.example.p3_sepehr"
compileSdk {
version = release(36) {
minorApiLevel = 1
}
}
defaultConfig {
minSdk = 24
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {
implementation(project(":core"))
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
implementation(libs.retrofit.converter.gson)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.core.ktx)
implementation(libs.material)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.androidx.junit)
}
View File
Binary file not shown.
Binary file not shown.
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,24 @@
package com.example.p3_pardakht_novin
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example.ps3.test", appContext.packageName)
}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.PRINTER" />
</manifest>
@@ -0,0 +1,78 @@
package com.example.p3_pardakht_novin
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import androidx.activity.result.ActivityResult
import com.example.core.model.PaymentResultEntity
import com.example.core.model.PaymentResultStatus
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class P3SepehrIapPosService @Inject constructor(
@param:ApplicationContext private val context: Context
) {
fun isPosAppInstalled(packageName: String = "com.dml.sima7.sepehr"): Boolean {
return try {
context.packageManager.getPackageInfo(packageName, 0)
true
} catch (e: PackageManager.NameNotFoundException) {
false
}
}
fun startPurchase(
amount: Long,
packageName: String = "com.dml.sima7.sepehr"
): Intent {
return Intent("com.dml.sima7.sepehr.activity.Intent_SwipeCardActivity").apply {
setPackage(packageName)
putExtra("amount", amount.toString())
setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
}
fun decodeResponse(
id: String?,
activityResult: ActivityResult
): PaymentResultEntity {
val data = activityResult.data
if (data == null) {
return PaymentResultEntity.failure(
id = id,
message = "پاسخی از دستگاه دریافت نشد!"
)
}
val resultCode = data.getStringExtra("resultCode")
val resultDescription = data.getStringExtra("resultDescription") ?: "خطای نامشخص"
// resultCode "00" usually means success in most Iranian PSPs
val isSuccess = resultCode == "00"
return if (isSuccess) {
PaymentResultEntity(
id = id,
status = PaymentResultStatus.SUCCESS,
paymentResultDataEntity = PaymentResultEntity.PaymentResultDataEntity(
terminalId = data.getStringExtra("terminalNo") ?: "",
stan = data.getStringExtra("traceNo") ?: "",
rrn = data.getStringExtra("refrenceCode") ?: "",
responseCode = resultCode,
customerCardNO = data.getStringExtra("maskedPan") ?: "",
transactionDateTime = data.getStringExtra("DateTime") ?: "",
),
message = resultDescription
)
} else {
PaymentResultEntity.failure(
id = id,
message = resultDescription
)
}
}
}
@@ -1,4 +1,4 @@
package com.example.p3 package com.example.p3_pardakht_novin
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
@@ -20,7 +20,7 @@ import java.io.File
import javax.inject.Inject import javax.inject.Inject
class P3Printer @Inject constructor( class P3SepehrPrinter @Inject constructor(
@param:ApplicationContext private val context: Context, @param:ApplicationContext private val context: Context,
) : Printer { ) : Printer {
companion object { companion object {
@@ -1,4 +1,4 @@
package com.example.p3 package com.example.p3_pardakht_novin
import android.device.PrinterManager import android.device.PrinterManager
import android.graphics.Bitmap import android.graphics.Bitmap
@@ -16,7 +16,7 @@ import javax.inject.Singleton
* Helper class for interacting with the POS Printer. * Helper class for interacting with the POS Printer.
*/ */
@Singleton @Singleton
class PrinterHelper @Inject constructor() { class P3SepehrPrinterHelper @Inject constructor() {
private var printer: PrinterManager? = null private var printer: PrinterManager? = null
private val gson = GsonBuilder().setPrettyPrinting().create() private val gson = GsonBuilder().setPrettyPrinting().create()
@@ -1,4 +1,4 @@
package com.example.p3 package com.example.p3_pardakht_novin
import android.content.Intent import android.content.Intent
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
@@ -8,20 +8,20 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import javax.inject.Inject import javax.inject.Inject
class P3Service @Inject constructor( class P3SepehrService @Inject constructor(
private val iapPosService: IapPosService, private val p3SepehrIapPosService: P3SepehrIapPosService,
private val printerHelper: PrinterHelper private val p3SepehrPrinterHelper: P3SepehrPrinterHelper
) : PspService { ) : PspService {
private val scope = CoroutineScope(Dispatchers.IO) private val scope = CoroutineScope(Dispatchers.IO)
override fun pay(amount: Long): Intent { override fun pay(amount: Long): Intent {
return iapPosService.startPurchase(amount) return p3SepehrIapPosService.startPurchase(amount)
} }
override fun print(lines: List<String>, onMessage: (String) -> Unit) { override fun print(lines: List<String>, onMessage: (String) -> Unit) {
printerHelper.printLines(lines) { status -> p3SepehrPrinterHelper.printLines(lines) { status ->
onMessage("P3 Printer: $status") onMessage("P3 Printer: $status")
} }
} }
@@ -30,14 +30,18 @@ class P3Service @Inject constructor(
override fun decodePosResponse( override fun decodePosResponse(
id: String?, id: String?,
activityResult: ActivityResult activityResult: ActivityResult
): PaymentResultEntity? { ): PaymentResultEntity {
return try { return try {
iapPosService.decodeResponse( p3SepehrIapPosService.decodeResponse(
id = id, id = id,
activityResult = activityResult activityResult = activityResult
) )
} catch (e: Exception) { } catch (e: Exception) {
null e.printStackTrace()
PaymentResultEntity.failure(
id = id,
e.message
)
} }
} }
} }
@@ -1,7 +1,7 @@
package com.example.p3.di package com.example.p3_pardakht_novin.di
import android.content.Context import android.content.Context
import com.example.p3.IapPosService import com.example.p3_pardakht_novin.P3SepehrIapPosService
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@@ -11,11 +11,11 @@ import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object P3Module { object P3SepehrModule {
@Provides @Provides
@Singleton @Singleton
fun provideIapPosService(@ApplicationContext context: Context): IapPosService { fun provideIapPosService(@ApplicationContext context: Context): P3SepehrIapPosService {
return IapPosService(context) return P3SepehrIapPosService(context)
} }
} }
@@ -0,0 +1,6 @@
package com.example.p3_pardakht_novin.models
data class IapAccountSplit(
val index: Int,
val percent: Int
)
Binary file not shown.
@@ -0,0 +1,17 @@
package com.example.p3_pardakht_novin
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
+1
View File
@@ -0,0 +1 @@
/build
+92
View File
@@ -0,0 +1,92 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.ksp)
alias(libs.plugins.hilt)
}
android {
namespace = "com.example.pardakht_novin_app"
compileSdk {
version = release(36) {
minorApiLevel = 1
}
}
defaultConfig {
applicationId = "com.example.pardakht_novin_app"
minSdk = 24
targetSdk = 36
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
flavorDimensions.add("module")
productFlavors {
create("p3_pardakht_novin") {
versionCode = 1
versionName = "1.0"
dimension = "module"
applicationId = "com.example.pardakht_novin_app_p3_pardakht_novin"
manifestPlaceholders["appName"] = "پرداخت نوین"
}
create("p3_sepehr") {
versionCode = 1
versionName = "1.0"
dimension = "module"
applicationId = "com.example.pardakht_novin_app_p3_sepehr"
manifestPlaceholders["appName"] = "تیس (سپهر)"
}
}
buildTypes {
debug {
applicationIdSuffix = ".debug"
signingConfig = signingConfigs.getByName("debug")
}
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("debug")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
buildFeatures {
compose = true
buildConfig = true
}
}
dependencies {
implementation(project(":core"))
implementation(project(":design_system"))
"p3_pardakht_novinImplementation"(project(":p3:p3_pardakht_novin"))
"p3_sepehrImplementation"(project(":p3:p3_sepehr"))
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.core.ktx)
implementation(libs.retrofit.converter.gson)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
testImplementation(libs.junit)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.androidx.junit)
debugImplementation(libs.androidx.compose.ui.test.manifest)
debugImplementation(libs.androidx.compose.ui.tooling)
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,24 @@
package com.example.pardakht_novin_app
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example.pardakht_novin_app", appContext.packageName)
}
}
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
<package android:name="ir.co.pna.pos" />
<intent>
<action android:name="ir.co.pna.pos.view.cart.IAPCActivity" />
</intent>
</queries>
<uses-permission android:name="android.permission.PRINTER" />
<uses-permission android:name="com.pos.permission.PRINTER" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".ui.PspApplication"
android:allowBackup="true"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.PSP"
android:usesCleartextTraffic="true"
>
<uses-library
android:name="android.device"
android:required="false" />
<activity
android:name=".ui.PardakhtNovinMainActivity"
android:exported="true"
android:label="${appName}"
android:theme="@style/Theme.PSP">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

@@ -0,0 +1,69 @@
package com.example.pardakht_novin_app.ui
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import com.example.design_system.theme.tis.TisTheme
import com.example.design_system.util.ObserveAsEvents
import com.example.design_system.util.snackbar.SnackbarController
import com.example.pardakht_novin_app.ui.screen.PardakhtNovinWebViewScreen
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
@AndroidEntryPoint
class PardakhtNovinMainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
TisTheme {
val scope = rememberCoroutineScope()
val snackbarHostState = remember {
SnackbarHostState()
}
ObserveAsEvents(
flow = SnackbarController.events,
snackbarHostState
) { event ->
scope.launch {
snackbarHostState.currentSnackbarData?.dismiss()
val result = snackbarHostState.showSnackbar(
message = event.message,
actionLabel = event.action?.name,
duration = event.duration
)
if (result == SnackbarResult.ActionPerformed) {
event.action?.action?.invoke()
}
}
}
Scaffold(
snackbarHost = {
SnackbarHost(
hostState = snackbarHostState
)
}
) { paddingValues ->
PardakhtNovinWebViewScreen(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
)
}
}
}
}
}
@@ -0,0 +1,13 @@
package com.example.pardakht_novin_app.ui
import android.app.Application
import com.example.design_system.util.WebViewManager
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class PspApplication : Application() {
override fun onCreate() {
super.onCreate()
WebViewManager.preload(this)
}
}
@@ -0,0 +1,101 @@
package com.example.pardakht_novin_app.ui.screen
import android.util.Log
import android.webkit.WebView
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
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.pardakht_novin_app.ui.viewmodel.WebViewModel
import kotlinx.coroutines.launch
private const val tisWebUrl = "https://tis.shift-am.ir"
private const val tisWebViewTag = "TisWebViewScreen"
@Composable
fun PardakhtNovinWebViewScreen(
modifier: Modifier = Modifier,
viewModel: WebViewModel = hiltViewModel()
) {
val context = LocalContext.current
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(
viewModel.deviceInfoJson(
androidId = getAndroidId(context),
getDeviceName()
)
)
}
// launcher to handle POS app result
val posLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
val paymentResultEntity = viewModel.paymentResult(
id = currentPaymentId,
activityResult = result
)
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(
url = tisWebUrl,
modifier = modifier,
onPayClick = { amount: Long, id: String? ->
if (isPaymentProcessing) return@PspWebView
isPaymentProcessing = true
currentPaymentId = id
viewModel.pay(amount)
.also(posLauncher::launch)
},
onPrintClick = { printEntities ->
if (isPrinting) return@PspWebView
isPrinting = true
coroutineScope.launch {
try {
viewModel.print(printEntities)
} finally {
isPrinting = false
}
}
},
onDeviceInfo = {
deviceInfo
},
onWebViewReady = { readyWebView ->
webView = readyWebView
},
onPageFinished = { loadedWebView ->
if (!initialCallbackSent) {
loadedWebView.evaluateJavascript(viewModel.postPaymentResult(null), null)
initialCallbackSent = true
}
}
)
}
@@ -0,0 +1,57 @@
package com.example.pardakht_novin_app.ui.viewmodel
import android.content.Intent
import androidx.activity.result.ActivityResult
import androidx.lifecycle.ViewModel
import com.example.core.domain.PaymentResult
import com.example.core.domain.Printer
import com.example.core.domain.PspService
import com.example.core.model.DeviceInfo
import com.example.core.model.PaymentResultEntity
import com.example.core.model.PrintEntity
import com.example.pardakht_novin_app.R
import com.google.gson.Gson
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class WebViewModel @Inject constructor(
private val pspService: PspService,
private val paymentResult: PaymentResult,
private val printer: Printer,
private val gson: Gson
) : ViewModel() {
fun postPaymentResult(paymentResultEntity: PaymentResultEntity?): String {
return paymentResult.post(paymentResultEntity)
}
fun pay(amount: Long): Intent = pspService.pay(amount)
fun paymentResult(
id: String?,
activityResult: ActivityResult
): PaymentResultEntity {
return pspService.decodePosResponse(
id = id,
activityResult = activityResult
)
}
fun print(printEntities: List<PrintEntity>) {
printer.print(
printEntities = printEntities,
icon = R.drawable.ic_pardakht_novin
)
}
fun deviceInfoJson(androidId: String, deviceName: String): String {
return gson.toJson(
DeviceInfo(
androidId = androidId,
deviceName = deviceName
)
)
}
}
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
@@ -0,0 +1,381 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="454dp"
android:height="454dp"
android:viewportWidth="454"
android:viewportHeight="454">
<path
android:pathData="M0,0C149.82,0 299.64,0 454,0C454,149.82 454,299.64 454,454C304.18,454 154.36,454 0,454C0,304.18 0,154.36 0,0Z"
android:fillColor="#FEFEFE"/>
<path
android:pathData="M346,64C350.79,65.65 354.79,68.25 359,71C359.75,71.49 360.5,71.98 361.27,72.48C368.13,76.99 374.6,81.86 381,87C382.08,87.85 383.16,88.7 384.25,89.55C390.02,94.16 390.02,94.16 392,96C392,96.66 392,97.32 392,98C392.54,98.21 393.09,98.43 393.65,98.64C396.85,100.49 399.15,102.99 401.63,105.69C402.15,106.26 402.68,106.83 403.22,107.42C408.91,113.79 413.55,120.73 418,128C418.55,128.89 418.55,128.89 419.11,129.8C426.12,141.3 431.3,153.21 435.25,166.06C435.5,166.85 435.74,167.64 436,168.46C439.56,180.53 440.41,192.61 440.38,205.13C440.37,205.85 440.37,206.57 440.37,207.32C440.33,218.96 439.46,229.84 436,241C435.76,241.81 435.52,242.61 435.27,243.44C419.51,295.08 377.17,334.7 330.63,359.54C329.14,360.32 329.14,360.32 327.63,361.13C326.7,361.62 325.78,362.11 324.84,362.61C311.99,369.3 298.62,374.02 284.75,378.06C284.02,378.28 283.29,378.49 282.53,378.71C252.7,387.39 223.01,390.69 192,390C191.14,389.99 190.29,389.97 189.41,389.96C153.63,389.27 117.06,379.94 86,362C86,361.01 86,360.02 86,359C87.12,358.42 88.23,357.83 89.38,357.23C110.46,346.2 131.29,334.71 152,323C153.25,322.29 153.25,322.29 154.53,321.57C168.91,313.41 182.98,304.74 197,296C198.07,295.33 198.07,295.33 199.17,294.65C220.79,281.11 241.36,266.28 261,250C261.89,249.26 261.89,249.26 262.81,248.5C272.92,240.08 282.52,231.12 292,222C292.59,221.44 293.17,220.87 293.78,220.29C299.88,214.41 305.59,208.34 310.87,201.69C312.26,199.93 313.68,198.2 315.13,196.5C332.26,176.36 347.38,151.63 351,125C351.16,123.8 351.16,123.8 351.33,122.57C353.38,105.03 351.23,87.31 346.8,70.31C346,67 346,67 346,64Z"
android:fillColor="#8677B7"/>
<path
android:pathData="M178.94,92.56C180.12,92.55 181.3,92.54 182.52,92.53C202.81,92.73 222.63,98.75 237.5,112.94C241.59,117.25 244.54,121.61 247,127C247.46,127.96 247.46,127.96 247.93,128.94C254.79,144 250.83,161.34 245.53,176.22C234.2,206.34 209.51,232.77 185,253C183.78,254.02 183.78,254.02 182.53,255.06C172.34,263.49 161.9,271.51 151,279C150.05,279.67 149.1,280.35 148.13,281.04C139.73,286.97 131.06,292.38 122.23,297.66C120.18,298.89 118.13,300.14 116.09,301.39C69.82,329.49 69.82,329.49 52,333C40.18,328.3 30.02,311.19 25.17,300.11C11.95,268.2 11.44,233.78 23.93,201.58C31.3,183.8 41.89,168.95 55,155C56.22,153.61 57.42,152.21 58.63,150.81C62.21,146.7 65.87,142.57 70,139C70.66,139 71.32,139 72,139C72,138.34 72,137.68 72,137C74.09,135.19 76.18,133.49 78.38,131.81C79.02,131.32 79.66,130.82 80.32,130.31C109.57,107.86 141.53,92.77 178.94,92.56Z"
android:fillColor="#897BB8"/>
<path
android:pathData="M408,115C412.37,118.99 415.34,123.31 418.38,128.38C418.86,129.17 419.34,129.97 419.83,130.79C427.61,143.91 433.02,157.3 437,172C437.34,173.26 437.69,174.53 438.04,175.83C442.54,196.27 442.2,221.02 436,241C435.76,241.81 435.52,242.61 435.27,243.44C422.99,283.68 393.77,317.45 359.94,341.56C359.19,342.1 358.44,342.64 357.66,343.19C356.96,343.69 356.25,344.18 355.53,344.69C354.9,345.14 354.26,345.58 353.61,346.04C352,347 352,347 350,347C352.09,335.73 354.76,324.66 357.58,313.55C360.91,300.48 364.2,287.42 367.13,274.25C369.42,263.95 372.25,253.82 375.16,243.68C378.08,233.5 380.64,223.28 382.9,212.93C385.13,202.79 387.97,192.79 390.7,182.77C393.89,171 396.96,159.22 399.74,147.34C401.18,141.17 402.71,135.02 404.25,128.88C404.51,127.84 404.77,126.8 405.03,125.73C405.28,124.76 405.52,123.8 405.77,122.8C405.98,121.95 406.19,121.09 406.42,120.21C406.88,118.46 407.43,116.72 408,115Z"
android:fillColor="#978DC2"/>
<path
android:pathData="M263,248C263.99,249.49 263.99,249.49 265,251C263.24,252.86 261.52,254.35 259.44,255.88C256.83,257.8 256.83,257.8 256.06,260.63C255.71,261.41 255.36,262.19 255,263C252,264 252,264 249,265C247.7,267.49 247.7,267.49 247,270C245.67,270.67 244.33,271.33 243,272C243,271.01 243,270.02 243,269C242.34,269.33 241.68,269.66 241,270C240.67,270.99 240.34,271.98 240,273C239.01,273 238.02,273 237,273C237,273.66 237,274.32 237,275C235,277 235,277 233,278C232.67,278.66 232.34,279.32 232,280C231.28,279.83 230.56,279.67 229.81,279.5C226.92,279.7 226.92,279.7 225.55,281.78C224.3,283.82 223.11,285.88 222,288C221.01,287.34 220.02,286.68 219,286C217.89,286.64 216.77,287.28 215.63,287.94C213.77,289.01 211.92,290.04 210,291C209.67,291.99 209.34,292.98 209,294C209.66,294.33 210.32,294.66 211,295C209.02,295.99 209.02,295.99 207,297C207,298.65 207,300.3 207,302C206.01,302.49 206.01,302.49 205,303C204.01,306.46 203.21,309.89 202.54,313.43C202.3,314.61 202.3,314.61 202.06,315.81C201.93,316.54 201.8,317.27 201.66,318.02C201,320 201,320 198,323C198.66,318.71 199.32,314.42 200,310C198.69,313.94 197.63,317.36 196.88,321.38C196.71,322.25 196.54,323.12 196.37,324.02C196.19,325 196.19,325 196,326C196.33,325.34 196.66,324.68 197,324C197.66,324.33 198.32,324.66 199,325C199.2,331.05 199.2,331.05 199,333C198.34,333.66 197.68,334.32 197,335C195.89,338.35 194.95,341.73 194,345.13C193,348 193,348 191,349C190.67,348.34 190.34,347.68 190,347C189.51,348.48 189.51,348.48 189,350C189.99,350 190.98,350 192,350C191,353 191,353 189,355C189.66,355 190.32,355 191,355C190.79,355.62 190.59,356.24 190.38,356.88C190.19,357.93 190.19,357.93 190,359C190.66,359.66 191.32,360.32 192,361C191.63,363.63 191.63,363.63 191,366C190.34,366 189.68,366 189,366C187.12,369.27 186.51,372.28 186,376C185.34,375.01 184.68,374.02 184,373C183.67,374.65 183.34,376.3 183,378C183.66,378 184.32,378 185,378C184.67,380.31 184.34,382.62 184,385C190.22,386.99 195.79,387.11 202.3,387C207.9,387 213.43,387.48 219,388C219.33,387.34 219.66,386.68 220,386C222.66,384.13 223.78,383.97 227.06,384.44C228.03,384.62 229,384.81 230,385C230.99,384.67 231.98,384.34 233,384C233.66,384.33 234.32,384.66 235,385C242.32,385.55 248.93,383.81 256,382C256,381.34 256,380.68 256,380C257.18,379.57 257.18,379.57 258.38,379.13C261.1,378.15 261.1,378.15 263,376C263.99,376 264.98,376 266,376C266.33,376.99 266.66,377.98 267,379C269,380.21 269,380.21 271,381C249.86,392.77 215.56,390.52 192,390C191.14,389.99 190.29,389.97 189.41,389.96C153.63,389.27 117.06,379.94 86,362C86,361.01 86,360.02 86,359C87.12,358.42 88.23,357.83 89.38,357.23C110.46,346.2 131.29,334.71 152,323C152.84,322.53 153.67,322.05 154.53,321.57C168.91,313.41 182.98,304.74 197,296C197.72,295.55 198.43,295.11 199.17,294.65C232.85,273.65 232.85,273.65 263,248ZM187,358C186.17,360.89 186,362.89 186,366C186.99,365.67 187.98,365.34 189,365C188.67,362.69 188.34,360.38 188,358C187.67,358 187.34,358 187,358Z"
android:fillColor="#9287C0"/>
<path
android:pathData="M148,325C148.99,326.48 148.99,326.48 150,328C147.35,329.46 146.11,330 143,330C143,331.65 143,333.3 143,335C138.25,336 138.25,336 136,336C134.41,337.45 132.99,339.01 131.56,340.63C129.11,342.78 126.6,342.91 123.44,343.38C120.69,344.08 119.71,344.78 118,347C117.5,348.48 117.5,348.48 117,350C112.88,352.25 108.6,352.53 104,353C104,353.99 104,354.98 104,356C103.34,356.33 102.68,356.66 102,357C102.33,357.99 102.66,358.98 103,360C102.34,360.66 101.68,361.32 101,362C100.34,360.68 99.68,359.36 99,358C98.01,358.49 98.01,358.49 97,359C97.7,361.51 97.7,361.51 99,364C101.04,364.89 101.04,364.89 103,365C103,365.66 103,366.32 103,367C106.51,368.69 106.51,368.69 110.31,368.63C110.87,368.42 111.43,368.21 112,368C112.33,368.99 112.66,369.98 113,371C115.97,370.51 115.97,370.51 119,370C119.33,370.66 119.66,371.32 120,372C119.34,372 118.68,372 118,372C118.33,372.66 118.66,373.32 119,374C119.99,374.33 120.98,374.66 122,375C122,375.66 122,376.32 122,377C116.11,376.41 111.12,374.15 105.75,371.81C104.82,371.41 103.88,371.01 102.92,370.59C96.63,367.83 90.67,364.9 85,361C87.78,357.66 91.52,356 95.31,354.06C96.86,353.26 98.4,352.45 99.94,351.65C100.76,351.22 101.58,350.79 102.43,350.35C117.82,342.26 132.94,333.69 148,325ZM114,347C115,349 115,349 115,349Z"
android:fillColor="#ACA6D2"/>
<path
android:pathData="M239,183C239.99,183.49 239.99,183.49 241,184C240.46,188.83 238.19,192.42 235.63,196.44C235.18,197.14 234.73,197.84 234.27,198.57C218.72,222.67 197.41,243.32 175,261C174.34,260.67 173.68,260.34 173,260C174.31,257.5 174.31,257.5 176,255C176.99,255 177.98,255 179,255C179.33,254.01 179.66,253.02 180,252C181.32,252 182.64,252 184,252C184.08,251.11 184.17,250.23 184.25,249.31C185.25,244.9 187.42,242.52 190.63,239.44C192.22,237.77 193.06,236.09 194,234C194.66,234.33 195.32,234.66 196,235C197.22,235.07 197.22,235.07 199,234C198.67,233.01 198.34,232.02 198,231C199.1,230.21 200.21,229.42 201.31,228.63C201.93,228.18 202.54,227.74 203.18,227.29C204.77,226.17 206.38,225.08 208,224C208.08,223.36 208.17,222.72 208.25,222.06C208.5,221.38 208.74,220.7 209,220C210.15,219.53 211.31,219.05 212.5,218.56C214.23,217.79 214.23,217.79 216,217C216.83,214.53 216.83,214.53 217,212C218.56,210.19 218.56,210.19 220,209C220.99,209.66 221.98,210.32 223,211C223,210.01 223,209.02 223,208C223.66,208 224.32,208 225,208C224.75,207.42 224.51,206.85 224.25,206.25C224.17,205.51 224.09,204.76 224,204C225.75,201.94 225.75,201.94 228,200C229.76,198.31 230.91,197.19 232,195C232.66,195 233.32,195 234,195C233.47,193.64 233.47,193.64 232.94,192.25C232.04,189.12 231.87,187.95 233,185C233.99,185 234.98,185 236,185C236.49,185.99 236.49,185.99 237,187C237,186.01 237,185.02 237,184C237.66,183.67 238.32,183.34 239,183Z"
android:fillColor="#786AA5"/>
<path
android:pathData="M263,248C263.66,248.99 264.32,249.98 265,251C263.24,252.86 261.52,254.35 259.44,255.88C256.83,257.8 256.83,257.8 256.06,260.63C255.71,261.41 255.36,262.19 255,263C252,264 252,264 249,265C247.7,267.49 247.7,267.49 247,270C245.67,270.67 244.33,271.33 243,272C243,271.01 243,270.02 243,269C242.01,269.49 242.01,269.49 241,270C240.51,271.48 240.51,271.48 240,273C239.01,273 238.02,273 237,273C237,273.66 237,274.32 237,275C235,277 235,277 233,278C232.67,278.66 232.34,279.32 232,280C231.28,279.83 230.56,279.67 229.81,279.5C226.92,279.7 226.92,279.7 225.55,281.78C224.3,283.82 223.11,285.88 222,288C221.01,287.34 220.02,286.68 219,286C217.89,286.64 216.77,287.28 215.63,287.94C213.77,289.01 211.92,290.04 210,291C209.67,291.99 209.34,292.98 209,294C209.66,294.33 210.32,294.66 211,295C209.68,295.66 208.36,296.32 207,297C207,298.65 207,300.3 207,302C206.01,302.49 206.01,302.49 205,303C204.27,305.31 203.6,307.65 203,310C201.73,306.36 202.27,303.61 203,299.88C204.02,295.81 204.02,295.81 203,292C203.57,291.65 204.15,291.29 204.74,290.93C224.79,278.36 246.2,264.8 263,248Z"
android:fillColor="#A098BF"/>
<path
android:pathData="M51,164C51.99,164.33 52.98,164.66 54,165C53.67,165.99 53.34,166.98 53,168C52.2,168.27 51.39,168.54 50.56,168.81C49.72,169.2 48.87,169.6 48,170C47.31,171.99 46.64,173.99 46,176C44.06,176.63 44.06,176.63 42,177C39.62,179.55 39,180.44 39,184C38.02,185.02 37.02,186.02 36,187C35.3,188.32 34.63,189.65 34,191C33.23,192.24 33.23,192.24 32.44,193.5C30.95,195.81 30.95,195.81 30.56,197.88C29.67,201.26 27.79,204 26,207C26.66,207.33 27.32,207.66 28,208C26.51,208.49 26.51,208.49 25,209C24.88,210.61 24.88,210.61 24.75,212.25C23.95,216.27 23.12,217.48 20,220C20.66,220.33 21.32,220.66 22,221C21.86,221.56 21.71,222.11 21.56,222.69C21.17,224.3 20.84,225.93 20.56,227.56C20,230 20,230 18,233C17.62,234.91 17.62,234.91 17.55,236.99C17.51,237.77 17.46,238.56 17.42,239.36C17.38,240.19 17.35,241.02 17.31,241.88C17.27,242.72 17.23,243.57 17.19,244.44C16.93,250.6 17.18,256.51 17.94,262.63C17.96,263.41 17.98,264.19 18,265C17.01,265.99 17.01,265.99 16,267C11.2,232.75 20.98,200.02 40.33,171.71C42.37,169.62 44.25,168.93 47,168C48.34,167.35 49.68,166.69 51,166C51,165.34 51,164.68 51,164Z"
android:fillColor="#978EBB"/>
<path
android:pathData="M262,376C263.32,376 264.64,376 266,376C266.49,377.48 266.49,377.48 267,379C269,380.21 269,380.21 271,381C247.62,394.02 206.89,391.73 181,389C181.33,387.68 181.66,386.36 182,385C182.56,385.14 183.12,385.29 183.7,385.44C190.01,386.9 195.9,387.11 202.37,387C207.94,387 213.46,387.48 219,388C219.33,387.34 219.66,386.68 220,386C222.66,384.13 223.78,383.97 227.06,384.44C228.03,384.62 229,384.81 230,385C230.99,384.67 231.98,384.34 233,384C233.66,384.33 234.32,384.66 235,385C242.32,385.55 248.93,383.81 256,382C256,381.34 256,380.68 256,380C256.8,379.71 257.61,379.42 258.44,379.13C261.13,378.27 261.13,378.27 262,376Z"
android:fillColor="#968EBD"/>
<path
android:pathData="M218,101C228.37,103.66 237.67,111.56 243.57,120.33C244.82,122.52 245.94,124.72 247,127C247.46,127.96 247.46,127.96 247.93,128.95C253.39,140.97 251.97,155.74 248,168C247.67,168 247.34,168 247,168C247.05,167.32 247.09,166.63 247.14,165.93C249,147.12 249,147.12 245,129C243.36,127.64 241.69,126.3 240,125C239.69,121.75 239.69,121.75 240,119C239.22,119.19 238.43,119.37 237.63,119.56C235,120 235,120 233,119C232.41,116.35 232.26,113.71 232,111C229.69,111.33 227.38,111.66 225,112C224.79,110.89 224.59,109.77 224.38,108.63C223.38,104.73 223.38,104.73 219.88,103.56C218.93,103.38 217.98,103.19 217,103C217.33,102.34 217.66,101.68 218,101Z"
android:fillColor="#786AA3"/>
<path
android:pathData="M304,214C304,215.65 304,217.3 304,219C303.34,219 302.68,219 302,219C301.72,219.96 301.72,219.96 301.44,220.94C300,223 300,223 296.38,223.75C294.7,223.87 294.7,223.87 293,224C293.33,225.65 293.66,227.3 294,229C292.35,229.33 290.7,229.66 289,230C289,230.66 289,231.32 289,232C288.24,232.43 287.47,232.87 286.69,233.31C283.64,235.23 282.65,236.85 281,240C280.34,240.99 279.68,241.98 279,243C279.31,245.15 279.31,245.15 280,247C278.68,246.67 277.36,246.34 276,246C276,246.66 276,247.32 276,248C275.34,248 274.68,248 274,248C273.67,246.68 273.34,245.36 273,244C272.67,245.32 272.34,246.64 272,248C271.38,248.14 270.76,248.29 270.13,248.44C267.97,248.93 267.97,248.93 266,250C267.24,245.33 268.61,242.96 272.31,239.88C273.2,239.11 274.09,238.35 275.01,237.56C276,236.71 276.98,235.87 278,235C280.23,233 282.43,230.97 284.63,228.94C285.18,228.43 285.73,227.92 286.3,227.4C290.13,223.86 293.82,220.21 297.41,216.43C299.47,214.57 301.24,214 304,214Z"
android:fillColor="#8D82B6"/>
<path
android:pathData="M355,70C366.58,74.34 375.9,82.74 385.26,90.57C386.81,91.84 388.39,93.06 389.98,94.27C390.64,94.84 391.31,95.41 392,96C392,96.66 392,97.32 392,98C392.66,98 393.32,98 394,98C394.66,99.98 395.32,101.96 396,104C393.63,104.63 393.63,104.63 391,105C389.81,103.81 388.64,102.61 387.5,101.38C386.09,99.77 386.09,99.77 384,100C383.34,98.35 382.68,96.7 382,95C380.35,95.33 378.7,95.66 377,96C377,93.03 377,90.06 377,87C376.01,86.67 375.02,86.34 374,86C373.67,86.33 373.34,86.66 373,87C371.67,87.36 370.34,87.7 369,88C369.66,87.01 370.32,86.02 371,85C369.99,82.43 369.99,82.43 368,80C364.4,79.1 364.4,79.1 361,79C360.67,77.35 360.34,75.7 360,74C358.68,74 357.36,74 356,74C355.67,72.68 355.34,71.36 355,70Z"
android:fillColor="#77689F"/>
<path
android:pathData="M204,227C205.49,227.49 205.49,227.49 207,228C207,228.66 207,229.32 207,230C206.4,230.19 205.8,230.37 205.19,230.56C202.48,232.34 202.09,234.14 200.96,237.07C199.12,240.76 196.03,243.28 193,246C192.38,246.57 191.76,247.13 191.12,247.71C185.92,252.36 180.47,256.68 175,261C174.34,260.67 173.68,260.34 173,260C174.31,257.5 174.31,257.5 176,255C176.99,255 177.98,255 179,255C179.33,254.01 179.66,253.02 180,252C181.32,252 182.64,252 184,252C184.08,251.11 184.17,250.23 184.25,249.31C185.25,244.9 187.42,242.52 190.63,239.44C192.22,237.77 193.06,236.09 194,234C194.66,234.33 195.32,234.66 196,235C197.22,235.07 197.22,235.07 199,234C198.67,233.01 198.34,232.02 198,231C200.81,228.94 200.81,228.94 204,227Z"
android:fillColor="#8071AE"/>
<path
android:pathData="M148,325C148.66,325.99 149.32,326.98 150,328C147.35,329.46 146.11,330 143,330C143,331.65 143,333.3 143,335C138.25,336 138.25,336 136,336C134.41,337.45 132.99,339.01 131.56,340.63C129.11,342.78 126.6,342.91 123.44,343.38C120.69,344.08 119.71,344.78 118,347C117.67,347.99 117.34,348.98 117,350C114.94,351.19 114.94,351.19 113,352C112.87,348.76 112.82,346.95 114,344C117.1,341.34 120.65,339.7 124.31,337.94C126.32,336.91 128.33,335.88 130.34,334.86C131.31,334.37 132.29,333.88 133.29,333.38C136.73,331.63 140.05,329.72 143.38,327.75C144.29,327.21 145.21,326.67 146.15,326.11C147.06,325.56 147.06,325.56 148,325Z"
android:fillColor="#A49BC5"/>
<path
android:pathData="M72,137C72.99,137.33 73.98,137.66 75,138C75,138.66 75,139.32 75,140C74.34,140 73.68,140 73,140C73,140.66 73,141.32 73,142C71.35,142.33 69.7,142.66 68,143C67.92,143.58 67.83,144.15 67.75,144.75C66.79,147.62 65.13,148.87 63,151C61.75,153.63 61.75,153.63 61,156C60.34,156 59.68,156 59,156C58.73,156.58 58.46,157.15 58.19,157.75C56.96,160.07 55.75,162.04 54,164C53.01,164 52.02,164 51,164C51,164.66 51,165.32 51,166C49.47,167.29 49.47,167.29 47.5,168.63C46.85,169.07 46.2,169.52 45.53,169.98C45.03,170.31 44.52,170.65 44,171C43.67,170.34 43.34,169.68 43,169C47.87,162.87 52.87,156.93 58.19,151.19C58.67,150.66 59.16,150.14 59.66,149.59C61.1,148.05 62.55,146.52 64,145C64.54,144.43 65.07,143.86 65.63,143.28C67.04,141.81 68.52,140.4 70,139C70.66,139 71.32,139 72,139C72,138.34 72,137.68 72,137Z"
android:fillColor="#9085B4"/>
<path
android:pathData="M263,248C263.66,248.99 264.32,249.98 265,251C263.24,252.86 261.52,254.35 259.44,255.88C256.83,257.8 256.83,257.8 256.06,260.63C255.71,261.41 255.36,262.19 255,263C252,264 252,264 249,265C247.7,267.49 247.7,267.49 247,270C245.67,270.67 244.33,271.33 243,272C243,271.01 243,270.02 243,269C241.02,269.99 239.04,270.98 237,272C237,271.01 237,270.02 237,269C238.9,267.33 240.77,265.85 242.81,264.38C244,263.49 245.19,262.6 246.38,261.71C246.99,261.26 247.59,260.81 248.21,260.35C253.32,256.5 258.48,252.52 263,248Z"
android:fillColor="#9A90C0"/>
<path
android:pathData="M377,232C378.42,235.83 377.57,238.56 376.52,242.43C376.36,243.06 376.19,243.69 376.02,244.33C375.48,246.37 374.93,248.4 374.38,250.44C373.63,253.24 372.88,256.05 372.14,258.85C371.96,259.55 371.77,260.24 371.58,260.96C369.74,267.91 368.1,274.91 366.55,281.93C365.21,287.79 365.21,287.79 363,290C362.67,286.37 362.34,282.74 362,279C363.32,278.34 364.64,277.68 366,277C366.65,274.31 366.65,274.31 366.79,272.26C367.05,269.48 367.49,266.75 367.94,264C368.09,263.03 368.24,262.06 368.4,261.06C369.07,257.63 370.01,254.35 371,251C371.42,249.47 371.84,247.94 372.25,246.41C372.47,245.63 372.68,244.84 372.9,244.04C373.35,242.41 373.79,240.78 374.23,239.14C375.89,233.11 375.89,233.11 377,232Z"
android:fillColor="#7263A3"/>
<path
android:pathData="M281,232C282.32,232.66 283.64,233.32 285,234C284.55,234.39 284.09,234.78 283.63,235.19C281.31,237.76 280,240.51 280,244C280,244.99 280,245.98 280,247C278.68,246.67 277.36,246.34 276,246C276,246.66 276,247.32 276,248C275.34,248 274.68,248 274,248C273.67,246.68 273.34,245.36 273,244C272.67,245.32 272.34,246.64 272,248C271.38,248.14 270.76,248.29 270.13,248.44C267.97,248.93 267.97,248.93 266,250C267.28,245 269.01,242.62 272.94,239.31C274.25,238.19 274.25,238.19 275.59,237.05C276.39,236.37 277.18,235.7 278,235C279.01,234.01 280.02,233.02 281,232Z"
android:fillColor="#9388BD"/>
<path
android:pathData="M132,103C131.44,103.25 130.89,103.5 130.31,103.75C127.6,105.22 125.36,107.03 123,109C122.34,108.67 121.68,108.34 121,108C121,108.66 121,109.32 121,110C120.01,110.5 120.01,110.5 119,111C117.87,113.02 117.87,113.02 117,115C116.67,114.34 116.34,113.68 116,113C115.35,114.56 114.7,116.12 114.06,117.69C113.7,118.56 113.34,119.43 112.97,120.32C112.04,122.89 111.5,125.33 111,128C110.37,129.68 109.71,131.35 109,133C108.67,133 108.34,133 108,133C108.24,131.36 108.47,129.73 108.71,128.09C108.9,126.71 109.07,125.33 109.22,123.94C110.61,111.22 110.61,111.22 114.59,107.52C119.4,104.39 126.32,100.59 132,103Z"
android:fillColor="#9D91C8"/>
<path
android:pathData="M332,174C332.66,174 333.32,174 334,174C333.67,175.65 333.34,177.3 333,179C332.34,179 331.68,179 331,179C330.73,179.58 330.46,180.15 330.19,180.75C328.98,183.03 327.59,184.97 326,187C326.33,187.66 326.66,188.32 327,189C326.28,189.16 325.56,189.33 324.81,189.5C321.23,191.41 320.18,193.61 318.31,197.13C316.77,199.32 315.41,199.91 313,201C311.67,203.14 310.91,205.25 310.06,207.63C309,210 309,210 306,212C305.01,211.67 304.02,211.34 303,211C303.7,210.21 304.41,209.42 305.13,208.61C313.17,199.46 320.45,189.85 327.56,179.98C328.04,179.32 328.51,178.67 329,178C329.51,177.26 330.03,176.51 330.56,175.75C331.03,175.17 331.51,174.59 332,174Z"
android:fillColor="#857CA9"/>
<path
android:pathData="M113,110C112.45,114.57 111.58,118.67 110,123C108.35,123.33 106.7,123.66 105,124C104.34,122.68 103.68,121.36 103,120C101.02,120.33 99.04,120.66 97,121C98.51,116.75 101.17,115.41 105,113.25C106.07,112.64 107.14,112.02 108.25,111.39C111,110 111,110 113,110Z"
android:fillColor="#806EB4"/>
<path
android:pathData="M100,159C100.33,159 100.66,159 101,159C100.98,159.78 100.96,160.57 100.94,161.38C100.81,163.98 100.81,163.98 102,166C101.53,166.58 101.05,167.15 100.56,167.75C99.05,169.93 98.46,171.43 98,174C98.66,174 99.32,174 100,174C99,176 99,176 97,177C96.42,178.78 96.42,178.78 96.06,180.94C95.19,185.27 94.01,189.43 92.68,193.64C91.64,197.23 91.03,200.73 90.63,204.44C89.97,208.16 88.74,210.66 87,214C86.23,216.32 85.61,218.64 85,221C83.36,216.95 84.73,213.46 85.91,209.47C86.12,208.74 86.33,208 86.55,207.24C87,205.66 87.46,204.08 87.91,202.51C89.1,198.43 90.25,194.35 91.41,190.27C91.64,189.46 91.87,188.66 92.1,187.84C94.16,180.52 95.98,173.15 97.64,165.73C98.91,160.09 98.91,160.09 100,159Z"
android:fillColor="#A094CA"/>
<path
android:pathData="M75,250C75.33,250.66 75.66,251.32 76,252C76.66,252 77.32,252 78,252C78,253.65 78,255.3 78,257C77.01,257.49 77.01,257.49 76,258C76.33,258.99 76.66,259.98 77,261C76.34,261.66 75.68,262.32 75,263C74.67,262.67 74.34,262.34 74,262C73.88,262.65 73.76,263.3 73.63,263.98C72.71,268.8 71.62,273.36 70,278C69.66,279.66 69.33,281.33 69,283C68.49,285.27 67.96,287.54 67.44,289.81C67.17,290.97 66.9,292.14 66.62,293.33C66.42,294.21 66.21,295.09 66,296C65.34,296 64.68,296 64,296C64.58,290.74 65.44,285.76 66.75,280.63C66.92,279.95 67.09,279.28 67.27,278.59C69.06,271.62 71,264.69 72.95,257.77C73.67,255.18 74.36,252.6 75,250Z"
android:fillColor="#A39ACB"/>
<path
android:pathData="M245,124C252.04,134.56 252.5,147.56 250.33,159.78C249.63,162.55 248.88,165.28 248,168C247.67,168 247.34,168 247,168C247.05,167.32 247.09,166.63 247.14,165.93C248.37,147.26 248.37,147.26 245,129C244.34,126.71 244.34,126.71 244,125C244.33,124.67 244.66,124.34 245,124Z"
android:fillColor="#8274A7"/>
<path
android:pathData="M154,95C152.25,98.88 152.25,98.88 150,100C149.28,101.64 148.61,103.31 148,105C147.67,104.34 147.34,103.68 147,103C145.58,103.28 145.58,103.28 144.13,103.56C141,104 141,104 139,103C137.82,102.94 137.82,102.94 136.13,103.69C135.42,103.79 134.72,103.89 134,104C132.37,102.52 132.37,102.52 131,101C133.75,100.19 136.5,99.37 139.25,98.56C140.03,98.33 140.8,98.1 141.61,97.87C142.74,97.53 142.74,97.53 143.89,97.19C144.93,96.89 144.93,96.89 145.99,96.57C151.52,95 151.52,95 154,95Z"
android:fillColor="#9487BF"/>
<path
android:pathData="M398,105C402.13,106.59 405.65,109.24 408,113C408.78,119.1 406.12,125.37 404,131C403.34,130.34 402.68,129.68 402,129C402.24,126.84 402.24,126.84 402.88,124.38C403.08,123.56 403.28,122.74 403.49,121.9C403.74,120.96 403.74,120.96 404,120C403.34,120 402.68,120 402,120C401.67,117.36 401.34,114.72 401,112C400.01,111.67 399.02,111.34 398,111C398.33,110.01 398.66,109.02 399,108C398.67,107.01 398.34,106.02 398,105Z"
android:fillColor="#72639D"/>
<path
android:pathData="M62,321C65.34,320.93 67.8,320.93 71,322C73.34,321.72 75.68,321.39 78,321C73.81,324.78 69.48,326.71 64.31,328.75C63.16,329.22 63.16,329.22 61.97,329.71C56.29,332 56.29,332 54,332C54,330.68 54,329.36 54,328C54.99,327.67 55.98,327.34 57,327C57.66,327.33 58.32,327.66 59,328C59.33,327.34 59.66,326.68 60,326C60.33,326 60.66,326 61,326C61.33,324.35 61.66,322.7 62,321Z"
android:fillColor="#A69CCC"/>
<path
android:pathData="M94,176C94.33,176 94.66,176 95,176C94.41,181.44 93.47,186.61 92.04,191.89C91.85,192.61 91.66,193.32 91.46,194.06C91.06,195.55 90.65,197.05 90.24,198.54C89.62,200.82 89.02,203.11 88.41,205.4C88.02,206.85 87.62,208.31 87.23,209.77C87.05,210.45 86.87,211.13 86.69,211.83C85.72,215.3 84.9,217.76 82,220C81.5,218.51 81.5,218.51 81,217C81.66,216.34 82.32,215.68 83,215C82.97,213.92 82.97,213.92 82.94,212.81C83,209.79 83.88,207.87 85.14,205.16C86.59,201.51 87.38,197.69 88.25,193.87C88.97,191.12 89.89,188.6 91,186C90.34,185.34 89.68,184.68 89,184C89.99,184 90.98,184 92,184C92.14,182.87 92.29,181.73 92.44,180.56C93,177 93,177 94,176Z"
android:fillColor="#796BA8"/>
<path
android:pathData="M18,220C20.83,222.83 20.8,225.06 21,229C20.52,229.55 20.05,230.1 19.56,230.67C17.18,234.22 17.39,237.74 17.25,241.94C17.21,242.78 17.18,243.62 17.14,244.49C16.91,250.62 17.19,256.54 17.94,262.63C17.96,263.41 17.98,264.19 18,265C17.34,265.66 16.68,266.32 16,267C13.76,251.05 15.2,235.79 18,220Z"
android:fillColor="#A29BBF"/>
<path
android:pathData="M205,294C204.64,300.4 204.64,300.4 201.81,303.06C198.17,304.28 196.47,303.47 193,302C192.34,302 191.68,302 191,302C192.25,298.24 193.53,297.75 196.88,295.75C197.76,295.21 198.64,294.67 199.55,294.11C202,293 202,293 205,294Z"
android:fillColor="#8877BD"/>
<path
android:pathData="M353,334C353.84,334.49 353.84,334.49 354.69,335C357.3,336.13 359.18,336.16 362,336C361.69,339.19 361.43,340.61 359,342.8C358.2,343.3 357.39,343.8 356.56,344.31C355.76,344.82 354.96,345.34 354.13,345.86C352,347 352,347 350,347C350.31,345.21 350.62,343.42 350.94,341.63C351.11,340.63 351.29,339.63 351.46,338.6C352,336 352,336 353,334Z"
android:fillColor="#A09BC4"/>
<path
android:pathData="M182,385C182.56,385.14 183.12,385.29 183.7,385.44C188.89,386.64 193.66,387.09 199,387C204.93,386.9 210.26,387.52 216,389C216,389.33 216,389.66 216,390C212.23,390.03 208.46,390.05 204.69,390.06C203.63,390.07 202.58,390.08 201.5,390.09C194.61,390.11 187.85,389.76 181,389C181.33,387.68 181.66,386.36 182,385Z"
android:fillColor="#AAA4C6"/>
<path
android:pathData="M349,137C350.56,141.85 349.48,146.22 348,151C346.67,152.67 345.33,154.33 344,156C343.65,157.68 343.37,159.37 343.13,161.06C342.4,165.58 342.4,165.58 340.55,167.54C338.88,168.63 338.88,168.63 336,170C336.64,165.14 338.68,161.21 340.81,156.88C343.95,150.37 346.74,143.87 349,137Z"
android:fillColor="#8378A7"/>
<path
android:pathData="M100,368C109.38,368 109.38,368 113,369C113,369.66 113,370.32 113,371C114.98,370.67 116.96,370.34 119,370C119.33,370.66 119.66,371.32 120,372C119.34,372 118.68,372 118,372C118.33,372.66 118.66,373.32 119,374C119.99,374.33 120.98,374.66 122,375C122,375.66 122,376.32 122,377C117.03,376.37 112.79,374.93 108.19,373.06C107.17,372.67 107.17,372.67 106.13,372.26C101.13,370.25 101.13,370.25 100,368Z"
android:fillColor="#A49EC5"/>
<path
android:pathData="M230,385C230.99,384.67 231.98,384.34 233,384C235.37,384.92 237.7,385.92 240,387C240,387.33 240,387.66 240,388C232.77,388.88 225.66,389.13 218.38,389.06C217.37,389.06 216.37,389.05 215.33,389.05C212.89,389.04 210.44,389.02 208,389C208,388.67 208,388.34 208,388C211.63,388 215.26,388 219,388C219.33,387.34 219.66,386.68 220,386C223.53,383.52 225.92,384.22 230,385Z"
android:fillColor="#9C97B9"/>
<path
android:pathData="M17,269C17.99,269.49 17.99,269.49 19,270C18.67,270.66 18.34,271.32 18,272C19.98,272.49 19.98,272.49 22,273C22.93,275.79 23.02,278.22 22.94,281.13C22.55,285.01 22.55,285.01 25,287C25,290 25,290 23,293C19.45,285.56 16.68,277.32 17,269Z"
android:fillColor="#A098C4"/>
<path
android:pathData="M31,310C34.88,311.75 34.88,311.75 36,314C36.66,313.34 37.32,312.68 38,312C38.19,313.09 38.37,314.19 38.56,315.31C39.58,318.95 39.58,318.95 41.69,320.13C43.84,321.06 43.84,321.06 47,322C46.67,323.32 46.34,324.64 46,326C46.66,326 47.32,326 48,326C47.67,326.99 47.34,327.98 47,329C40.4,323.94 35.68,317.82 31,311C31,310.67 31,310.34 31,310Z"
android:fillColor="#9389B6"/>
<path
android:pathData="M239,183C239.66,183.33 240.32,183.66 241,184C240.42,188.06 239.11,190.85 236.94,194.31C236.39,195.2 235.84,196.08 235.28,196.99C234.86,197.65 234.43,198.32 234,199C233.34,197.68 232.68,196.36 232,195C232.66,195 233.32,195 234,195C233.47,193.64 233.47,193.64 232.94,192.25C232.04,189.12 231.87,187.95 233,185C233.99,185 234.98,185 236,185C236.33,185.66 236.66,186.32 237,187C237,186.01 237,185.02 237,184C237.66,183.67 238.32,183.34 239,183Z"
android:fillColor="#7968AD"/>
<path
android:pathData="M383,94C387.75,94.75 387.75,94.75 390,97C390.66,97.08 391.32,97.17 392,97.25C392.66,97.5 393.32,97.75 394,98C395.25,101.06 395.25,101.06 396,104C393.63,104.63 393.63,104.63 391,105C389.81,103.81 388.64,102.61 387.5,101.38C386.09,99.77 386.09,99.77 384,100C383.67,98.02 383.34,96.04 383,94Z"
android:fillColor="#7764AD"/>
<path
android:pathData="M95,309C95.66,309.33 96.32,309.66 97,310C96,313 96,313 94.33,314.07C93.62,314.39 92.91,314.71 92.18,315.04C91.04,315.57 91.04,315.57 89.87,316.11C89.07,316.46 88.26,316.82 87.44,317.19C86.63,317.56 85.82,317.92 84.99,318.3C83,319.21 81,320.11 79,321C79.33,319.02 79.66,317.04 80,315C81.65,315 83.3,315 85,315C85,314.01 85,313.02 85,312C86.09,311.88 87.19,311.75 88.31,311.63C92.07,311.25 92.07,311.25 95,309Z"
android:fillColor="#9E97C1"/>
<path
android:pathData="M122,288C123.14,288.25 123.14,288.25 124.31,288.5C127.04,289.09 127.04,289.09 130,289C130.66,289.33 131.32,289.66 132,290C132,290.66 132,291.32 132,292C129.88,293.51 129.88,293.51 127.13,295.13C126.22,295.66 125.32,296.2 124.38,296.76C122,298 122,298 120,298C120.46,294.53 120.89,291.33 122,288Z"
android:fillColor="#958CBE"/>
<path
android:pathData="M114,345C113.25,349.75 113.25,349.75 111,352C108.68,352.4 106.34,352.74 104,353C104,353.99 104,354.98 104,356C102.35,356 100.7,356 99,356C99,354.68 99,353.36 99,352C101.12,350.83 103.25,349.66 105.38,348.5C106.28,348 106.28,348 107.2,347.49C111.77,345 111.77,345 114,345Z"
android:fillColor="#A9A3C7"/>
<path
android:pathData="M148,325C148.66,325.99 149.32,326.98 150,328C147.35,329.46 146.11,330 143,330C143,331.65 143,333.3 143,335C139.16,335.81 135.94,336 132,336C132,335.34 132,334.68 132,334C137.28,331.03 142.56,328.06 148,325Z"
android:fillColor="#9E97C3"/>
<path
android:pathData="M131,378C131.33,377.34 131.66,376.68 132,376C132.99,376 133.98,376 135,376C135.33,377.32 135.66,378.64 136,380C138.64,380.33 141.28,380.66 144,381C143.67,381.99 143.34,382.98 143,384C136.07,382.02 129.14,380.04 122,378C122,377.67 122,377.34 122,377C125.78,375.95 127.65,375.89 131,378Z"
android:fillColor="#A29AC1"/>
<path
android:pathData="M187,244C187.99,244.99 188.98,245.98 190,247C185.55,252.58 180.76,256.79 175,261C174.34,260.67 173.68,260.34 173,260C174.31,257.5 174.31,257.5 176,255C176.99,255 177.98,255 179,255C179.33,254.01 179.66,253.02 180,252C181.32,252 182.64,252 184,252C184.12,251.05 184.25,250.1 184.38,249.13C185,246 185,246 187,244Z"
android:fillColor="#8076A5"/>
<path
android:pathData="M360,304C360.66,304.33 361.32,304.66 362,305C361.43,308.02 360.74,310.97 359.94,313.94C359.25,316.6 359,318.51 359,321.31C359,324 359,324 357,327C356.3,328.99 355.63,330.99 355,333C354.67,333 354.34,333 354,333C354.6,326.26 355.85,319.97 357.56,313.44C357.8,312.52 358.03,311.61 358.27,310.67C358.84,308.45 359.42,306.22 360,304Z"
android:fillColor="#A99FCF"/>
<path
android:pathData="M388,198C388.66,198.99 389.32,199.98 390,201C389.31,204.16 388.51,207.01 387.02,209.89C385.86,212.3 385.12,214.68 384.38,217.25C383.98,218.59 383.98,218.59 383.59,219.95C383.39,220.63 383.2,221.3 383,222C382.67,222 382.34,222 382,222C382.31,215.08 382.9,203.1 388,198Z"
android:fillColor="#A094C9"/>
<path
android:pathData="M290,371C290.99,371.49 290.99,371.49 292,372C291.01,372.49 291.01,372.49 290,373C290,372.34 290,371.68 290,371ZM297,372C297,372.66 297,373.32 297,374C294.4,374.87 291.79,375.72 289.19,376.56C288.45,376.81 287.71,377.06 286.95,377.31C281.34,379.11 281.34,379.11 278,378C278,377.34 278,376.68 278,376C278.89,375.82 278.89,375.82 279.79,375.63C283.24,374.9 286.48,374.21 289.75,372.88C292.3,371.88 294.28,371.84 297,372Z"
android:fillColor="#9088B4"/>
<path
android:pathData="M245,167C245.66,167.66 246.32,168.32 247,169C246.65,174.14 244.98,178.8 242,183C241.34,182.67 240.68,182.34 240,182C240,180.02 240,178.04 240,176C240.66,175.67 241.32,175.34 242,175C242.66,175.33 243.32,175.66 244,176C244,174.68 244,173.36 244,172C243.34,172 242.68,172 242,172C243.88,168.13 243.88,168.13 245,167Z"
android:fillColor="#7A6AA6"/>
<path
android:pathData="M23,294C23.99,294.33 24.98,294.66 26,295C26,295.66 26,296.32 26,297C26.99,297.33 27.98,297.66 29,298C32,301 32,301 32.2,302.95C32.13,304.96 32.07,306.98 32,309C31.34,309 30.68,309 30,309C28.83,306.69 27.66,304.38 26.5,302.06C26.17,301.4 25.83,300.74 25.49,300.06C23,295.11 23,295.11 23,294Z"
android:fillColor="#998FBE"/>
<path
android:pathData="M199,96C204.99,95.68 208.64,96.32 214,99C214,99.99 214,100.98 214,102C213.01,102.33 212.02,102.66 211,103C210.67,103.33 210.34,103.66 210,104C208.32,103.71 206.66,103.38 205,103C205.66,102.01 206.32,101.02 207,100C206.34,99.34 205.68,98.68 205,98C204.01,98.33 203.02,98.66 202,99C202,98.34 202,97.68 202,97C201.01,96.67 200.02,96.34 199,96Z"
android:fillColor="#8072AD"/>
<path
android:pathData="M377,320C378.98,320.66 380.96,321.32 383,322C379.67,325.33 376.33,328.67 373,332C371.68,331.34 370.36,330.68 369,330C370.32,330 371.64,330 373,330C373.1,329.36 373.21,328.72 373.31,328.06C373.54,327.38 373.77,326.7 374,326C374.99,325.67 375.98,325.34 377,325C377,323.35 377,321.7 377,320Z"
android:fillColor="#9F99C1"/>
<path
android:pathData="M208,291C208.33,291.99 208.66,292.98 209,294C209.66,294.33 210.32,294.66 211,295C209.02,295.99 209.02,295.99 207,297C207,298.65 207,300.3 207,302C206.01,302.49 206.01,302.49 205,303C204.27,305.31 203.6,307.65 203,310C201.61,306.19 202.36,303.26 203.25,299.38C203.5,298.23 203.76,297.08 204.02,295.9C205,293 205,293 208,291Z"
android:fillColor="#A59BCD"/>
<path
android:pathData="M402,112C403.98,112.99 403.98,112.99 406,114C406.37,125.52 406.37,125.52 404,131C403.34,130.34 402.68,129.68 402,129C402.24,126.84 402.24,126.84 402.88,124.38C403.18,123.15 403.18,123.15 403.49,121.9C403.66,121.27 403.83,120.65 404,120C403.34,120 402.68,120 402,120C402,117.36 402,114.72 402,112Z"
android:fillColor="#6D5BA2"/>
<path
android:pathData="M197,324C197.66,324.33 198.32,324.66 199,325C199.2,331.05 199.2,331.05 199,333C198.34,333.66 197.68,334.32 197,335C196.4,336.52 195.86,338.07 195.38,339.63C195.11,340.44 194.85,341.26 194.59,342.1C194.39,342.73 194.2,343.35 194,344C193.67,344 193.34,344 193,344C192.76,339.16 193.6,335.26 195,330.63C195.37,329.38 195.74,328.13 196.13,326.85C196.41,325.91 196.7,324.97 197,324Z"
android:fillColor="#A59BCD"/>
<path
android:pathData="M132,103C131.16,103.37 131.16,103.37 130.31,103.75C127.6,105.22 125.36,107.03 123,109C122.34,108.67 121.68,108.34 121,108C121,108.66 121,109.32 121,110C119.35,109.67 117.7,109.34 116,109C118.24,106.5 120.39,105.13 123.44,103.75C124.2,103.39 124.96,103.04 125.75,102.67C128.28,101.92 129.54,102.13 132,103Z"
android:fillColor="#9D93BC"/>
<path
android:pathData="M360,298C360.33,298 360.66,298 361,298C360.61,306.38 358.47,314.01 356,322C355.67,322 355.34,322 355,322C355.08,321.37 355.17,320.75 355.25,320.1C355.36,319.28 355.46,318.47 355.56,317.63C355.67,316.81 355.77,316 355.88,315.16C356.18,312.95 356.18,312.95 355,311C355.66,311 356.32,311 357,311C357.12,309.78 357.25,308.57 357.38,307.31C357.79,303.94 358.55,301.1 360,298Z"
android:fillColor="#796BA7"/>
<path
android:pathData="M112,112C113,114 113,114 112.44,116.16C112.15,116.98 111.86,117.79 111.56,118.63C111.28,119.44 110.99,120.26 110.69,121.1C110.35,122.04 110.35,122.04 110,123C108.35,123.33 106.7,123.66 105,124C105.51,121.83 106,120 107,118C107.1,117.34 107.21,116.68 107.31,116C108,114 108,114 110.06,112.75C110.7,112.5 111.34,112.25 112,112Z"
android:fillColor="#7460A7"/>
<path
android:pathData="M346,64C349.99,65.37 352.8,67.26 356,70C354.82,70.12 354.82,70.12 353.63,70.25C350.85,70.73 350.85,70.73 349.69,73.06C349.46,73.7 349.23,74.34 349,75C346.88,71.47 346.47,68.06 346,64Z"
android:fillColor="#8B7EAE"/>
<path
android:pathData="M418,130C421.38,133.04 423.27,136.05 425.25,140.13C426.03,141.71 426.03,141.71 426.83,143.32C428,146 428,146 428,148C427.34,148 426.68,148 426,148C424.98,146.34 423.98,144.68 423,143C422.34,142.67 421.68,142.34 421,142C420.66,139.67 420.33,137.33 420,135C419.34,134.34 418.68,133.68 418,133C418,132.01 418,131.02 418,130Z"
android:fillColor="#948DB4"/>
<path
android:pathData="M178.25,309.88C178.83,310.25 179.4,310.62 180,311C179.22,311.25 178.43,311.49 177.63,311.75C174.68,313.15 174.1,314.01 173,317C172.44,316.77 171.89,316.55 171.31,316.31C168.09,315.88 166.68,317.31 164,319C163.34,318.67 162.68,318.34 162,318C163.58,316.87 165.16,315.75 166.75,314.63C167.63,314 168.51,313.37 169.42,312.73C175.36,308.75 175.36,308.75 178.25,309.88Z"
android:fillColor="#8E86AF"/>
<path
android:pathData="M112,304C105.07,307.46 105.07,307.46 98,311C98.66,308.69 99.32,306.38 100,304C101.44,303.8 102.87,303.62 104.31,303.44C105.11,303.33 105.91,303.23 106.74,303.12C109,303 109,303 112,304Z"
android:fillColor="#9890BF"/>
<path
android:pathData="M403,134C403.66,134.33 404.32,134.66 405,135C404.58,141.19 403.35,146.29 401,152C400.32,154 399.65,156 399,158C397.91,154.69 398.12,152.58 399,149.23C399.34,147.9 399.34,147.9 399.69,146.54C400.06,145.16 400.06,145.16 400.44,143.75C400.68,142.81 400.92,141.88 401.17,140.91C401.77,138.61 402.38,136.3 403,134Z"
android:fillColor="#9E91CB"/>
<path
android:pathData="M230,385C230.99,384.67 231.98,384.34 233,384C233.66,384.33 234.32,384.66 235,385C235,385.66 235,386.32 235,387C233.27,387.17 231.54,387.34 229.81,387.5C228.85,387.59 227.89,387.69 226.89,387.78C224.25,387.98 221.65,388.04 219,388C221.44,383.11 225.31,384.1 230,385Z"
android:fillColor="#9C94C9"/>
<path
android:pathData="M59,326C61.64,326 64.28,326 67,326C64.58,328.87 62.4,329.71 58.81,330.69C57.91,330.94 57.01,331.19 56.08,331.45C55.39,331.63 54.71,331.81 54,332C54,330.68 54,329.36 54,328C54.99,327.67 55.98,327.34 57,327C57.66,327.33 58.32,327.66 59,328C59,327.34 59,326.68 59,326Z"
android:fillColor="#A9A3C3"/>
<path
android:pathData="M144,281C143.88,282.81 143.88,282.81 143,285C139.94,286.75 139.94,286.75 137,288C137,285.69 137,283.38 137,281C139.67,279.67 141.17,280.33 144,281Z"
android:fillColor="#938ABF"/>
<path
android:pathData="M408,115C408.99,115.99 409.98,116.98 411,118C410.55,119.03 410.09,120.06 409.63,121.13C408.59,123.52 407.74,125.82 406.94,128.31C406,131 406,131 404,133C405.04,126.89 406.16,120.92 408,115Z"
android:fillColor="#A498C6"/>
<path
android:pathData="M200,309C201.08,312.24 200.96,312.9 200,316.01C199.66,317.15 199.66,317.15 199.31,318.31C199.06,319.09 198.82,319.88 198.56,320.69C198.32,321.48 198.08,322.28 197.83,323.1C197.23,325.07 196.61,327.04 196,329C195.67,329 195.34,329 195,329C195.5,324.51 196.12,320.07 196.94,315.63C197.09,314.79 197.24,313.96 197.4,313.1C198,311 198,311 200,309Z"
android:fillColor="#7F74AD"/>
<path
android:pathData="M385,202C386.1,205.31 385.9,206.19 385,209.45C384.66,210.72 384.66,210.72 384.31,212.02C384.06,212.9 383.82,213.78 383.56,214.69C383.32,215.58 383.08,216.47 382.83,217.39C382.22,219.6 381.62,221.8 381,224C380.67,224 380.34,224 380,224C380.25,215.85 381.01,209.19 385,202Z"
android:fillColor="#6F5F9E"/>
<path
android:pathData="M412,283C412.66,284.32 413.32,285.64 414,287C409.55,292.94 409.55,292.94 405,299C404,297 404,297 404.94,293.88C405.29,292.93 405.64,291.98 406,291C406.66,291 407.32,291 408,291C408.66,289.68 409.32,288.36 410,287C410.66,287 411.32,287 412,287C412,285.68 412,284.36 412,283Z"
android:fillColor="#928CAF"/>
<path
android:pathData="M205,225C205.66,225 206.32,225 207,225C207,225.66 207,226.32 207,227C207.99,227 208.98,227 210,227C208.35,231.29 205.26,233.88 202,237C201.34,236.67 200.68,236.34 200,236C201.81,233 201.81,233 204,230C204.99,230 205.98,230 207,230C206.34,228.35 205.68,226.7 205,225Z"
android:fillColor="#766AA1"/>
<path
android:pathData="M432,159C433.84,161.76 434.68,163.75 435.63,166.88C435.89,167.72 436.15,168.56 436.41,169.43C437.01,172.04 437.11,174.33 437,177C435,176 435,176 434.15,173.71C431.59,164.11 431.59,164.11 432,159Z"
android:fillColor="#938DB0"/>
<path
android:pathData="M295,368C297.64,368 300.28,368 303,368C303,368.66 303,369.32 303,370C303.66,370.33 304.32,370.66 305,371C303.02,371.66 301.04,372.32 299,373C299,372.34 299,371.68 299,371C297.35,371 295.7,371 294,371C294.33,370.01 294.66,369.02 295,368Z"
android:fillColor="#8C81BD"/>
<path
android:pathData="M61,306C62.97,309.93 61.05,314.9 60,319C58.84,321.83 57.51,324.33 56,327C54.72,324.43 55.19,323.62 55.96,320.9C56.3,319.7 56.3,319.7 56.64,318.47C56.88,317.64 57.13,316.8 57.38,315.94C57.61,315.1 57.84,314.26 58.09,313.39C59.86,307.14 59.86,307.14 61,306Z"
android:fillColor="#A9A0CF"/>
<path
android:pathData="M198,295C198.66,295.99 199.32,296.98 200,298C199.34,298 198.68,298 198,298C198,299.65 198,301.3 198,303C195.69,302.67 193.38,302.34 191,302C192.33,298 194.5,297.16 198,295Z"
android:fillColor="#8D82B3"/>
<path
android:pathData="M106,131C107.34,134.71 106.66,137.36 105.69,141.13C105.42,142.18 105.16,143.23 104.89,144.32C104,147 104,147 102,149C102.23,146.84 102.47,144.69 102.72,142.53C102.9,140.91 103.07,139.28 103.22,137.66C103.41,135.76 103.7,133.88 104,132C104.66,131.67 105.32,131.34 106,131Z"
android:fillColor="#7464A7"/>
<path
android:pathData="M353,334C353.99,334.66 354.98,335.32 356,336C356,336.66 356,337.32 356,338C355.34,338 354.68,338 354,338C354,338.66 354,339.32 354,340C354.66,340.33 355.32,340.66 356,341C355.53,341.43 355.05,341.87 354.56,342.31C352.76,344.02 352.76,344.02 352,347C351.34,347 350.68,347 350,347C350.31,345.21 350.62,343.42 350.94,341.63C351.11,340.63 351.29,339.63 351.46,338.6C352,336 352,336 353,334Z"
android:fillColor="#A9A4D2"/>
<path
android:pathData="M432,243C432.66,243.33 433.32,243.66 434,244C433.01,248.84 432.04,253.48 430,258C429.34,258 428.68,258 428,258C428.65,254.48 429.51,251.11 430.56,247.69C430.83,246.8 431.1,245.92 431.38,245.01C431.58,244.35 431.79,243.68 432,243Z"
android:fillColor="#928BAF"/>
<path
android:pathData="M190,303C189.22,303.41 188.43,303.83 187.63,304.25C184.94,306.04 184.12,307.05 183,310C181.35,309.67 179.7,309.34 178,309C185.11,301.37 185.11,301.37 190,303Z"
android:fillColor="#9086B1"/>
<path
android:pathData="M118,296C118.66,296.99 119.32,297.98 120,299C117.69,300.32 115.38,301.64 113,303C112.67,301.35 112.34,299.7 112,298C113.98,297.34 115.96,296.68 118,296Z"
android:fillColor="#9990BF"/>
<path
android:pathData="M204,227C204.99,227.33 205.98,227.66 207,228C207,228.66 207,229.32 207,230C206.36,230.29 205.72,230.58 205.06,230.88C202.85,231.83 202.85,231.83 202,234C201.01,234 200.02,234 199,234C198.67,233.01 198.34,232.02 198,231C200.81,228.94 200.81,228.94 204,227Z"
android:fillColor="#7C6AB7"/>
<path
android:pathData="M355,70C358.46,71.25 361.15,72.68 364,75C363.34,76.32 362.68,77.64 362,79C361.34,78.34 360.68,77.68 360,77C360,76.01 360,75.02 360,74C358.68,74 357.36,74 356,74C355.67,72.68 355.34,71.36 355,70Z"
android:fillColor="#7E6FA6"/>
<path
android:pathData="M201,310C201.66,310.33 202.32,310.66 203,311C202.74,312.44 202.47,313.88 202.19,315.31C202.04,316.11 201.89,316.91 201.73,317.74C200.92,320.23 199.98,321.32 198,323C198.31,321.21 198.62,319.42 198.94,317.63C199.11,316.63 199.29,315.63 199.46,314.6C200,312 200,312 201,310Z"
android:fillColor="#A49BCB"/>
<path
android:pathData="M105,141C105.33,141 105.66,141 106,141C106.49,147.7 105.08,152.07 102,158C100.44,154.03 101.64,151.19 102.94,147.25C103.32,146.08 103.7,144.91 104.09,143.7C104.39,142.81 104.69,141.92 105,141Z"
android:fillColor="#9C90C8"/>
<path
android:pathData="M422,266C422.66,266.33 423.32,266.66 424,267C423.44,270.95 422.26,273.71 420,277C419.67,277 419.34,277 419,277C419,275.35 419,273.7 419,272C419.66,272 420.32,272 421,272C420.97,270.79 420.97,270.79 420.94,269.56C421,267 421,267 422,266Z"
android:fillColor="#9088B0"/>
<path
android:pathData="M65,146C64.46,149.8 62.8,151.47 60,154C59.01,153.34 58.02,152.68 57,152C57.95,150.99 58.91,150 59.88,149C60.41,148.44 60.94,147.89 61.49,147.31C63,146 63,146 65,146Z"
android:fillColor="#8A80AB"/>
<path
android:pathData="M98,354C98,354.66 98,355.32 98,356C96.4,356.87 94.8,357.72 93.19,358.56C92.29,359.04 91.4,359.51 90.48,360C88,361 88,361 85,360C86.79,358.99 88.58,358 90.38,357C91.37,356.44 92.37,355.89 93.4,355.31C96,354 96,354 98,354Z"
android:fillColor="#ACA8C4"/>
<path
android:pathData="M93,121C93.99,121 94.98,121 96,121C95.79,121.62 95.59,122.24 95.38,122.88C95.25,123.58 95.13,124.28 95,125C95.66,125.66 96.32,126.32 97,127C94.33,128.33 92.83,127.67 90,127C91.88,122.13 91.88,122.13 93,121Z"
android:fillColor="#7F6FAC"/>
<path
android:pathData="M232,111C232.99,111.33 233.98,111.66 235,112C236.19,115.06 236.19,115.06 237,118C236.34,118.66 235.68,119.32 235,120C234.34,119.67 233.68,119.34 233,119C232.41,116.35 232.26,113.71 232,111Z"
android:fillColor="#7B66B3"/>
<path
android:pathData="M140,98C140,98.99 140,99.98 140,101C137.5,102.69 137.5,102.69 135,104C133.68,103.01 132.36,102.02 131,101C133.97,100.01 136.94,99.02 140,98Z"
android:fillColor="#9A92BC"/>
<path
android:pathData="M224,280C224.66,280 225.32,280 226,280C224.38,285.75 224.38,285.75 221,288C220.67,286.35 220.34,284.7 220,283C221.32,282.67 222.64,282.34 224,282C224,281.34 224,280.68 224,280Z"
android:fillColor="#A396CE"/>
<path
android:pathData="M93.31,358.25C94.15,358.62 94.15,358.62 95,359C94.01,359 93.02,359 92,359C92,360.98 92,362.96 92,365C90.02,364.01 88.04,363.02 86,362C90.43,357.94 90.43,357.94 93.31,358.25Z"
android:fillColor="#B5AFD3"/>
<path
android:pathData="M329,355C330.32,355 331.64,355 333,355C332.88,356.88 332.88,356.88 332,359C328.94,360.25 328.94,360.25 326,361C327,359 328,357 329,355Z"
android:fillColor="#8175AA"/>
<path
android:pathData="M70,264C70.33,264 70.66,264 71,264C71.41,269.42 70.04,273.07 68,278C67.67,278 67.34,278 67,278C66.65,272.35 67.57,268.97 70,264Z"
android:fillColor="#8073AD"/>
<path
android:pathData="M378,235C378.66,235.99 379.32,236.98 380,238C379.25,241.13 379.25,241.13 378,244C377.01,244.49 377.01,244.49 376,245C376.14,243.52 376.29,242.04 376.44,240.56C376.52,239.74 376.6,238.92 376.68,238.07C377,236 377,236 378,235Z"
android:fillColor="#A094C5"/>
<path
android:pathData="M110,120C112,123 112,123 111.75,125.38C111,128.01 110.08,130.48 109,133C108.67,133 108.34,133 108,133C108.61,128.66 109.26,124.32 110,120Z"
android:fillColor="#9588C6"/>
<path
android:pathData="M402,112C403.98,112.99 403.98,112.99 406,114C405.34,115.98 404.68,117.96 404,120C403.34,120 402.68,120 402,120C402,117.36 402,114.72 402,112Z"
android:fillColor="#715EB0"/>
<path
android:pathData="M113,110C112.19,111.94 112.19,111.94 111,114C110.01,114.33 109.02,114.66 108,115C107.34,115.66 106.68,116.32 106,117C106,115.68 106,114.36 106,113C110.07,110 110.07,110 113,110Z"
android:fillColor="#796D9B"/>
<path
android:pathData="M400,299C400.99,299 401.98,299 403,299C402.65,302.26 401.98,303.02 399.44,305.25C398.63,305.83 397.83,306.4 397,307C397,306.01 397,305.02 397,304C397.99,303.67 398.98,303.34 400,303C400,301.68 400,300.36 400,299Z"
android:fillColor="#9993B9"/>
<path
android:pathData="M21,284C23,285.31 23,285.31 25,287C25,290 25,290 23,293C21.14,289.87 20.8,287.63 21,284Z"
android:fillColor="#9B8FC1"/>
<path
android:pathData="M249,260C249.66,260.33 250.32,260.66 251,261C250,264 250,264 248.13,265.19C246,266 246,266 243,266C245,264 247,262 249,260Z"
android:fillColor="#958DB3"/>
<path
android:pathData="M239,183C239.66,183.33 240.32,183.66 241,184C240.5,187.69 239.9,189.6 237,192C237,189.33 237,186.67 237,184C237.66,183.67 238.32,183.34 239,183Z"
android:fillColor="#8174A5"/>
<path
android:pathData="M391,310C391.99,310.33 392.98,310.66 394,311C392.72,312.71 391.38,314.37 390,316C389.34,316 388.68,316 388,316C388.57,313.13 388.86,312.14 391,310Z"
android:fillColor="#9E97C2"/>
<path
android:pathData="M202,293C202.99,293.33 203.98,293.66 205,294C204.67,295.65 204.34,297.3 204,299C203.67,298.34 203.34,297.68 203,297C201.68,297 200.36,297 199,297C199.33,296.01 199.66,295.02 200,294C200.66,293.67 201.32,293.34 202,293Z"
android:fillColor="#887AAB"/>
<path
android:pathData="M281,232C281.33,232.66 281.66,233.32 282,234C281.67,234.99 281.34,235.98 281,237C280.01,237.33 279.02,237.66 278,238C277.67,238.66 277.34,239.32 277,240C276.01,239.67 275.02,239.34 274,239C276.31,236.69 278.62,234.38 281,232Z"
android:fillColor="#9790B2"/>
<path
android:pathData="M299,215C299.66,215.99 300.32,216.98 301,218C300.67,218.66 300.34,219.32 300,220C298.67,220.35 297.34,220.69 296,221C295.01,221.66 294.02,222.32 293,223C294.44,219.63 296.33,217.49 299,215Z"
android:fillColor="#857CA7"/>
<path
android:pathData="M316,196C316.33,196.99 316.66,197.98 317,199C316.38,199.25 315.76,199.49 315.13,199.75C312.65,201.21 312.05,202.38 311,205C310.34,204.67 309.68,204.34 309,204C311.31,201.36 313.62,198.72 316,196Z"
android:fillColor="#8179A1"/>
<path
android:pathData="M426,149C426.99,149 427.98,149 429,149C430.06,150.81 430.06,150.81 431,153C430.67,153.99 430.34,154.98 430,156C429.01,155.67 428.02,155.34 427,155C426.67,153.02 426.34,151.04 426,149Z"
android:fillColor="#948CB4"/>
<path
android:pathData="M77,133C77.99,133.33 78.98,133.66 80,134C79.34,134 78.68,134 78,134C77.88,134.62 77.75,135.24 77.63,135.88C77,138 77,138 75,140C74.34,139.67 73.68,139.34 73,139C74.15,136.53 75.05,134.95 77,133Z"
android:fillColor="#8277A8"/>
<path
android:pathData="M112,112C112.33,112.66 112.66,113.32 113,114C112.67,114.99 112.34,115.98 112,117C111.34,117 110.68,117 110,117C109.67,117.66 109.34,118.32 109,119C109,118.34 109,117.68 109,117C108.34,117 107.68,117 107,117C108,114 108,114 110.06,112.81C110.7,112.54 111.34,112.28 112,112Z"
android:fillColor="#7B69AC"/>
<path
android:pathData="M148,325C148.66,325.99 149.32,326.98 150,328C147.65,329.43 146.52,330.09 143.75,329.63C143.17,329.42 142.6,329.21 142,329C143.98,327.68 145.96,326.36 148,325Z"
android:fillColor="#9A94B7"/>
<path
android:pathData="M243,266C243.33,266.99 243.66,267.98 244,269C241.09,270.26 239.2,271 236,271C240.75,266 240.75,266 243,266Z"
android:fillColor="#9992B8"/>
<path
android:pathData="M101,116C101.99,116.33 102.98,116.66 104,117C102.19,119 102.19,119 100,121C99.01,121 98.02,121 97,121C98.75,117.13 98.75,117.13 101,116Z"
android:fillColor="#7C6E9E"/>
<path
android:pathData="M156,321C156,322.65 156,324.3 156,326C154.35,326 152.7,326 151,326C153.26,321 153.26,321 156,321Z"
android:fillColor="#9B95B9"/>
<path
android:pathData="M435,231C435.66,231 436.32,231 437,231C436.67,233.97 436.34,236.94 436,240C435.67,240 435.34,240 435,240C434.12,233.85 434.12,233.85 434,232C434.33,231.67 434.66,231.34 435,231Z"
android:fillColor="#9087B2"/>
<path
android:pathData="M438,220C438.33,220 438.66,220 439,220C439.2,226.05 439.2,226.05 439,228C438.34,228.66 437.68,229.32 437,230C435.84,225.95 436.16,223.78 438,220Z"
android:fillColor="#9087B3"/>
<path
android:pathData="M228,200C228.33,200.66 228.66,201.32 229,202C228.07,204.03 227.07,206.04 226,208C225.34,206.68 224.68,205.36 224,204C225.32,202.68 226.64,201.36 228,200Z"
android:fillColor="#7964B1"/>
<path
android:pathData="M235,191C235.66,191.99 236.32,192.98 237,194C235.68,195.65 234.36,197.3 233,199C232.67,197.68 232.34,196.36 232,195C232.66,195 233.32,195 234,195C234.33,193.68 234.66,192.36 235,191Z"
android:fillColor="#7D71A8"/>
<path
android:pathData="M394,99C395.98,100.65 397.96,102.3 400,104C399.01,104.66 398.02,105.32 397,106C394,102.38 394,102.38 394,99Z"
android:fillColor="#7F72A4"/>
</vector>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">pardakht_novin_app</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.PSP" parent="android:Theme.Material.Light.NoActionBar" />
</resources>
@@ -2,8 +2,8 @@ package com.example.tis_app.di
import com.example.core.domain.Printer import com.example.core.domain.Printer
import com.example.core.domain.PspService import com.example.core.domain.PspService
import com.example.p3.P3Printer import com.example.p3_pardakht_novin.P3PardakhtNovinPrinter
import com.example.p3.P3Service import com.example.p3_pardakht_novin.P3PardakhtNovinService
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@@ -13,8 +13,8 @@ import dagger.hilt.components.SingletonComponent
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
abstract class PspModule { abstract class PspModule {
@Binds @Binds
abstract fun bindPspService(impl: P3Service): PspService abstract fun bindPspService(impl: P3PardakhtNovinService): PspService
@Binds @Binds
abstract fun bindPrinter(impl: P3Printer): Printer abstract fun bindPrinter(impl: P3PardakhtNovinPrinter): Printer
} }
@@ -0,0 +1,20 @@
package com.example.tis_sep.di
import com.example.core.PspService
import com.example.ps4.Ps4Service
import com.example.ps4.P4Printer
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
abstract class PspModule {
@Binds
abstract fun bindPspService(impl: Ps4Service): PspService
@Binds
abstract fun bindPrinter(impl: P4Printer): Printer
}
@@ -0,0 +1,24 @@
package com.example.tis_sep.di
import android.content.Intent
import com.example.core.PspService
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
object PspModule {
@Provides
fun providePspService(): PspService = object : PspService {
override fun pay(amount: Double, onMessage: (String) -> Unit) {
onMessage("Standard: Payment not supported")
}
override fun print(text: String, onMessage: (String) -> Unit) {
onMessage("Standard: Printing not supported")
}
override fun createPurchaseIntent(amount: Long): Intent? = null
override fun decodePosResponse(response: String?): Map<String, Any>? = null
}
}
@@ -0,0 +1,17 @@
package com.example.pardakht_novin_app
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
@@ -18,5 +18,5 @@ class Ps4Service @Inject constructor() : PspService {
override fun decodePosResponse( override fun decodePosResponse(
id: String?, id: String?,
activityResult: ActivityResult activityResult: ActivityResult
): PaymentResultEntity? = null ): PaymentResultEntity = PaymentResultEntity.success()
} }
+6 -1
View File
@@ -25,8 +25,13 @@ dependencyResolutionManagement {
rootProject.name = "PSP" rootProject.name = "PSP"
include(":app") include(":app")
include(":tis_app") include(":tis_app")
include(":p3")
include(":ps4") include(":ps4")
include(":core") include(":core")
include(":design_system") include(":design_system")
include(":stage_app") include(":stage_app")
// P3 Parent and children
include(":p3")
include(":p3:p3_pardakht_novin")
include(":p3:p3_sepehr")
include(":pardakht_novin_app")
+16 -7
View File
@@ -23,13 +23,19 @@ android {
flavorDimensions.add("module") flavorDimensions.add("module")
productFlavors { productFlavors {
create("p3") { create("p3_novin") {
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
dimension = "module" dimension = "module"
applicationId = "com.example.stage_app_p3" applicationId = "com.example.stage_app_p3_novin"
manifestPlaceholders["appName"] = "TisP3" manifestPlaceholders["appName"] = "TisP3Novin"
}
create("p3_sepehr") {
versionCode = 1
versionName = "1.0"
dimension = "module"
applicationId = "com.example.stage_app_p3_sepehr"
manifestPlaceholders["appName"] = "TisP3Sepehr"
} }
create("ps4") { create("ps4") {
versionCode = 1 versionCode = 1
@@ -44,11 +50,14 @@ android {
dimension = "module" dimension = "module"
applicationId = "com.example.stage_app" applicationId = "com.example.stage_app"
manifestPlaceholders["appName"] = "Tis" manifestPlaceholders["appName"] = "Tis"
} }
} }
buildTypes { buildTypes {
debug {
applicationIdSuffix = ".debug"
signingConfig = signingConfigs.getByName("debug")
}
release { release {
isMinifyEnabled = false isMinifyEnabled = false
proguardFiles( proguardFiles(
@@ -67,14 +76,14 @@ android {
compose = true compose = true
buildConfig = true buildConfig = true
} }
} }
dependencies { dependencies {
implementation(project(":core")) implementation(project(":core"))
implementation(project(":design_system")) implementation(project(":design_system"))
"p3Implementation"(project(":p3"))
"p3_novinImplementation"(project(":p3:p3_pardakht_novin"))
"p3_sepehrImplementation"(project(":p3:p3_sepehr"))
"ps4Implementation"(project(":ps4")) "ps4Implementation"(project(":ps4"))
implementation(libs.hilt.android) implementation(libs.hilt.android)
@@ -54,13 +54,13 @@ fun StageWebViewScreen(
activityResult = result activityResult = result
) )
val jsResult = viewModel.postPaymentResult(paymentResultEntity) val jsResult = viewModel.postPaymentResult(paymentResultEntity)
val currentWebView = webView webView?.let {
if (currentWebView == null) { it.post {
Log.w(stageWebViewTag, "Skipping onPaymentResult JS callback: WebView is not ready yet.") it.evaluateJavascript(jsResult, null)
} else {
currentWebView.post {
currentWebView.evaluateJavascript(jsResult, null)
} }
} ?: run {
Log.w(stageWebViewTag, "Skipping onPaymentResult JS callback: WebView is not ready yet.")
} }
isPaymentProcessing = false isPaymentProcessing = false
} }
@@ -32,17 +32,23 @@ class StageViewModel @Inject constructor(
id: String?, id: String?,
activityResult: ActivityResult activityResult: ActivityResult
): PaymentResultEntity { ): PaymentResultEntity {
return PaymentResultEntity( val mockPaymentResult = PaymentResultEntity.success(
id = id, id = id,
status = com.example.core.model.PaymentResultStatus.ERROR, message = "تراکنش با موفقیت انجام شد",
data = PaymentResultEntity.PaymentResultDataEntity(
terminalId = "123456", terminalId = "123456",
stan = "123456", stan = "123456",
rrn = "123456", rrn = "123456789012",
responseCode = "01", responseCode = "00",
customerCardNO = "1234567890123456", customerCardNO = "603799******1234",
transactionDateTime = "14030101120000", transactionDateTime = "1403/05/20 12:30:45"
description = "تراکنش ناموفق"
) )
)
return mockPaymentResult
// return pspService.decodePosResponse(
// id = id,
// activityResult = activityResult
// )
} }
private val mock = listOf<PrintEntity>( private val mock = listOf<PrintEntity>(
@@ -2,8 +2,8 @@ package com.example.tis_app.di
import com.example.core.domain.Printer import com.example.core.domain.Printer
import com.example.core.domain.PspService import com.example.core.domain.PspService
import com.example.p3.P3Printer import com.example.p3_pardakht_novin.P3Printer
import com.example.p3.P3Service import com.example.p3_pardakht_novin.P3Service
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
+13 -14
View File
@@ -22,28 +22,27 @@ android {
flavorDimensions.add("module") flavorDimensions.add("module")
productFlavors { productFlavors {
create("p3") { create("p3_pardakht_novin") {
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
dimension = "module" dimension = "module"
applicationId = "com.example.tis_app_p3" applicationId = "com.example.tis_app_p3_pardakht_novin"
manifestPlaceholders["appName"] = "تیس" manifestPlaceholders["appName"] = "تیس (نوین)"
} }
create("ps4") { create("p3_sepehr") {
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
dimension = "module" dimension = "module"
applicationId = "com.example.tis_app_p4" applicationId = "com.example.tis_app_p3_sepehr"
manifestPlaceholders["appName"] = "تیس" } manifestPlaceholders["appName"] = "تیس (سپهر)"
create("standard") { }
versionCode = 1
versionName = "1.0"
dimension = "module"
applicationId = "com.example.tis_app"
manifestPlaceholders["appName"] = "تیس" }
} }
buildTypes { buildTypes {
debug {
applicationIdSuffix = ".debug"
signingConfig = signingConfigs.getByName("debug")
}
release { release {
isMinifyEnabled = false isMinifyEnabled = false
proguardFiles( proguardFiles(
@@ -66,9 +65,9 @@ android {
dependencies { dependencies {
implementation(project(":core")) implementation(project(":core"))
implementation(project(":design_system")) implementation(project(":design_system"))
"p3Implementation"(project(":p3"))
"ps4Implementation"(project(":ps4")) "p3_pardakht_novinImplementation"(project(":p3:p3_pardakht_novin"))
"p3_sepehrImplementation"(project(":p3:p3_sepehr"))
implementation(libs.hilt.android) implementation(libs.hilt.android)
ksp(libs.hilt.compiler) ksp(libs.hilt.compiler)
@@ -1,5 +1,6 @@
package com.example.tis_app.screen package com.example.tis_app.screen
import android.util.Log
import android.webkit.WebView import android.webkit.WebView
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts 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.getAndroidId
import com.example.core.getDeviceName import com.example.core.getDeviceName
import com.example.design_system.PspWebView import com.example.design_system.PspWebView
import com.example.design_system.util.WebViewManager
import com.example.tis_app.viewmodel.WebViewModel import com.example.tis_app.viewmodel.WebViewModel
private const val tisWebUrl = "https://tis.shift-am.ir" private const val tisWebUrl = "192.168.128.73:5000"
private const val tisWebViewTag = "TisWebViewScreen"
@Composable @Composable
fun TisWebViewScreen( fun TisWebViewScreen(
@@ -29,10 +30,11 @@ fun TisWebViewScreen(
) { ) {
val context = LocalContext.current 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 currentPaymentId by rememberSaveable { mutableStateOf<String?>(null) }
var isPaymentProcessing by rememberSaveable { mutableStateOf(false) } var isPaymentProcessing by rememberSaveable { mutableStateOf(false) }
var isPrinting by rememberSaveable { mutableStateOf(false) } var isPrinting by rememberSaveable { mutableStateOf(false) }
var initialCallbackSent by rememberSaveable { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val deviceInfo by remember { val deviceInfo by remember {
mutableStateOf( mutableStateOf(
@@ -50,17 +52,16 @@ fun TisWebViewScreen(
val paymentResultEntity = viewModel.paymentResult( val paymentResultEntity = viewModel.paymentResult(
id = currentPaymentId, id = currentPaymentId,
activityResult = result 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 isPaymentProcessing = false
} }
PspWebView( PspWebView(
@@ -86,6 +87,15 @@ fun TisWebViewScreen(
}, },
onDeviceInfo = { onDeviceInfo = {
deviceInfo 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( fun paymentResult(
id: String?, id: String?,
activityResult: ActivityResult activityResult: ActivityResult
): PaymentResultEntity? { ): PaymentResultEntity {
return pspService.decodePosResponse( return pspService.decodePosResponse(
id = id, id = id,
activityResult = activityResult activityResult = activityResult
@@ -0,0 +1,20 @@
package com.example.tis_app.di
import com.example.core.domain.Printer
import com.example.core.domain.PspService
import com.example.p3_pardakht_novin.P3PardakhtNovinPrinter
import com.example.p3_pardakht_novin.P3PardakhtNovinService
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
abstract class PspModule {
@Binds
abstract fun bindPspService(impl: P3PardakhtNovinService): PspService
@Binds
abstract fun bindPrinter(impl: P3PardakhtNovinPrinter): Printer
}