Compare commits

..

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

162 changed files with 674 additions and 4598 deletions

3
.gitignore vendored
View File

@ -21,8 +21,5 @@ xcuserdata
!*.xcodeproj/project.xcworkspace/
!*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings
**/*.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
composeApp/release/
composeApp/data/
BankingPersistence/data/

View File

@ -1,114 +0,0 @@
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
alias(libs.plugins.sqldelight)
}
kotlin {
jvmToolchain(11)
jvm()
js {
moduleName = "BankingPersistence"
binaries.executable()
browser()
}
androidTarget {
}
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "BankingPersistence"
isStatic = true
}
}
applyDefaultHierarchyTemplate()
sourceSets {
commonMain.dependencies {
implementation(libs.banking.client.model)
implementation(libs.fints4k.banking.client)
implementation(libs.kotlinx.datetime)
implementation(libs.sqldelight.runtime)
implementation(libs.sqldelight.coroutines.extensions)
implementation(libs.sqldelight.paging.extensions)
implementation(libs.klf)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
implementation(libs.coroutines.test)
}
jvmMain.dependencies {
implementation(libs.sqldelight.sqlite.driver)
}
jvmTest.dependencies {
implementation(libs.kotlin.test.junit)
}
androidMain.dependencies {
implementation(libs.sqldelight.android.driver)
}
iosMain.dependencies {
implementation(libs.sqldelight.native.driver)
}
}
}
sqldelight {
databases {
create("BankmeisterDb") {
packageName.set("net.codinux.banking.persistence")
generateAsync = true
schemaOutputDirectory = file("src/commonMain/sqldelight/databases")
}
}
}
android {
namespace = "net.codinux.banking.persistence"
compileSdk = libs.versions.android.compileSdk.get().toInt()
// sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
defaultConfig {
minSdk = libs.versions.android.minSdk.get().toInt()
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
}
}

View File

@ -1,7 +0,0 @@
package net.codinux.banking.persistence
import android.content.Context
object AndroidContext {
lateinit var applicationContext: Context
}

View File

@ -1,10 +0,0 @@
package net.codinux.banking.persistence
import app.cash.sqldelight.async.coroutines.synchronous
import app.cash.sqldelight.db.QueryResult
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.db.SqlSchema
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
actual fun createSqlDriverDriver(dbName: String, schema: SqlSchema<QueryResult.AsyncValue<Unit>>, version: Long): SqlDriver =
AndroidSqliteDriver(schema.synchronous(), AndroidContext.applicationContext, dbName)

View File

@ -1,69 +0,0 @@
package net.codinux.banking.persistence
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.client.model.Amount
import net.codinux.banking.client.model.BankAccess
import net.codinux.banking.client.model.securitiesaccount.Holding
import net.codinux.banking.persistence.entities.AccountTransactionEntity
import net.codinux.banking.persistence.entities.BankAccountEntity
import net.codinux.banking.persistence.entities.HoldingEntity
import net.codinux.banking.persistence.entities.BankAccessEntity
import net.codinux.banking.persistence.entities.UiSettingsEntity
import net.codinux.banking.ui.model.AccountTransactionViewModel
import net.codinux.banking.ui.model.settings.AppSettings
import net.codinux.banking.ui.model.settings.ImageSettings
interface BankingRepository {
fun getAppSettings(): AppSettings?
suspend fun saveAppSettings(settings: AppSettings)
fun getUiSettings(): UiSettingsEntity?
suspend fun saveUiSettings(settings: UiSettingsEntity)
fun getImageSettings(id: String): ImageSettings?
suspend fun saveImageSettings(settings: ImageSettings)
fun getAllBanks(): List<BankAccessEntity>
suspend fun persistBank(bank: BankAccess): BankAccessEntity
suspend fun updateBank(bank: BankAccessEntity, loginName: String, password: String, bankName: String?)
suspend fun updateBank(bank: BankAccessEntity, serializedClientData: String?)
suspend fun updateAccount(account: BankAccountEntity, userSetDisplayName: String?, hideAccount: Boolean, includeInAutomaticAccountsUpdate: Boolean)
suspend fun updateAccount(account: BankAccountEntity, balance: Amount, lastAccountUpdateTime: Instant, retrievedTransactionsFrom: LocalDate?)
suspend fun deleteBank(bank: BankAccessEntity)
suspend fun persistTransactions(bankAccount: BankAccountEntity, transactions: List<AccountTransaction>): List<AccountTransactionEntity>
suspend fun persistHoldings(bankAccount: BankAccountEntity, holdings: List<Holding>): List<HoldingEntity>
suspend fun updateHoldings(holdings: List<HoldingEntity>)
suspend fun deleteHoldings(holdings: List<HoldingEntity>)
fun getAllAccountTransactionsAsViewModel(): List<AccountTransactionViewModel>
fun getAllAccountTransactions(): List<AccountTransactionEntity>
fun getAllTransactionsForBank(bank: BankAccessEntity): List<AccountTransactionEntity>
fun getTransactionById(transactionId: Long): AccountTransactionEntity?
suspend fun updateTransaction(transaction: AccountTransactionEntity, userSetOtherPartyName: String?, userSetReference: String?, notes: String?)
}

View File

@ -1,17 +0,0 @@
package net.codinux.banking.persistence.entities
import net.codinux.banking.ui.model.settings.TransactionsGrouping
class UiSettingsEntity(
val showBalance: Boolean,
val transactionsGrouping: TransactionsGrouping,
val showTransactionsInAlternatingColors: Boolean,
val showBankIcons: Boolean,
val showColoredAmounts: Boolean
)

View File

@ -1,11 +0,0 @@
package net.codinux.banking.ui.model.settings
class ImageSettings(
val id: String,
var height: Int,
var frequency: Int? = null // only needed for flicker code
) {
override fun toString() = "$id $height${if (frequency != null) " (frequency = $frequency)" else ""}"
}

View File

@ -1,14 +0,0 @@
CREATE TABLE IF NOT EXISTS ImageSettings (
id TEXT PRIMARY KEY,
height INTEGER NOT NULL,
width INTEGER, -- not used right now, add it just in case
frequency INTEGER
);
ALTER TABLE Holding DROP COLUMN quantity;
ALTER TABLE Holding ADD COLUMN quantity REAL;

View File

@ -1,10 +0,0 @@
package net.codinux.banking.persistence
import app.cash.sqldelight.async.coroutines.synchronous
import app.cash.sqldelight.db.QueryResult
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.db.SqlSchema
import app.cash.sqldelight.driver.native.NativeSqliteDriver
actual fun createSqlDriverDriver(dbName: String, schema: SqlSchema<QueryResult.AsyncValue<Unit>>, version: Long): SqlDriver =
NativeSqliteDriver(schema.synchronous(), dbName)

View File

@ -1,9 +0,0 @@
package net.codinux.banking.persistence
import app.cash.sqldelight.db.QueryResult
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.db.SqlSchema
actual fun createSqlDriverDriver(dbName: String, schema: SqlSchema<QueryResult.AsyncValue<Unit>>, version: Long): SqlDriver {
throw NotImplementedError("TODO")
}

View File

@ -1,79 +0,0 @@
package net.codinux.banking.persistence
import app.cash.sqldelight.async.coroutines.synchronous
import app.cash.sqldelight.db.QueryResult
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.db.SqlSchema
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
import java.io.File
import java.nio.file.Files
import kotlin.io.path.Path
import kotlin.io.path.absolutePathString
val dataDirectory: File = determineDataDirectory()
actual fun createSqlDriverDriver(dbName: String, schema: SqlSchema<QueryResult.AsyncValue<Unit>>, version: Long): SqlDriver {
val dbDir = File(dataDirectory, "db").also { it.mkdirs() }
val databaseFile = File(dbDir, dbName)
return JdbcSqliteDriver("jdbc:sqlite:${databaseFile.path}").also { driver ->
schema.synchronous().also { schema ->
if (databaseFile.exists() == false) {
schema.create(driver)
}
schema.migrate(driver, schema.version, version)
}
}
}
private fun setSchemaVersion(driver: SqlDriver, schemaVersion: Int) {
driver.execute(null, "PRAGMA schema_version=$schemaVersion;", 0)
}
private fun determineDataDirectory(): File {
val currentDir = Path(System.getProperty("user.dir"))
// if the current directory is writable, use that one (the default for development)
val dataDir = if (Files.isWritable(currentDir)) { // couldn't believe it, but java.io.File returned folder is writable for "C:\\Program Files\\"
File(currentDir.absolutePathString(), "data")
} else { // otherwise use .bankmeister dir in user's home dir (the default for releases)
File(determineOsDependentUserDataDir(), ".bankmeister")
}
return dataDir.also { it.mkdirs() }
}
private fun determineOsDependentUserDataDir(): File {
val userHomeString = System.getProperty("user.home")
val userHome = File(userHomeString)
val windowsLocalAppDataDir = System.getenv("LOCALAPPDATA")?.takeUnless { it.isBlank() }
return if (windowsLocalAppDataDir != null) {
File(windowsLocalAppDataDir)
} else if (userHomeString.startsWith("/")) {
val osName = System.getProperty("os.name")
if (osName.contains("mac", true) || osName.contains("darwin", true)) { // macOS
File(userHome, "Library/Application Support")
} else if (osName.contains("nux")) { // Linux
val localShareDirectory = File(userHome, ".local/share")
val configDir = File(userHome, ".config")
if (localShareDirectory.exists()) {
localShareDirectory
} else if (configDir.exists()) {
configDir
} else {
userHome
}
} else { // unknown
userHome
}
} else if (userHomeString.length > 3 && userHomeString[1] == ':' && userHomeString[2] == '\\') { // Windows, but LOCALAPPDATA is not set
userHome // File(userHome, "AppData\Local")
} else {
userHome
}
}

View File

@ -1,7 +1,7 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.compose.desktop.tasks.AbstractJarsFlattenTask
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
plugins {
@ -11,23 +11,18 @@ plugins {
alias(libs.plugins.compose.compiler)
alias(libs.plugins.kotlinxSerialization)
alias(libs.plugins.sqldelight)
}
kotlin {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
// suppresses compiler warning: [EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING] 'expect'/'actual' classes (including interfaces, objects, annotations, enums, and 'actual' typealiases) are in Beta.
freeCompilerArgs.add("-Xexpect-actual-classes")
}
js {
moduleName = "Bankmeister"
moduleName = "composeApp"
browser {
val projectDirPath = project.projectDir.path
commonWebpackConfig {
outputFileName = "Bankmeister.js"
outputFileName = "composeApp.js"
devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply {
static = (static ?: mutableListOf()).apply {
// Serve sources to debug inside browser
@ -55,36 +50,26 @@ kotlin {
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "BankmeisterFramework"
isStatic = false
}
// don't know why but this has to be added here, adding it in BankingPersistence.build.gradle.kt does not work
iosTarget.binaries.forEach { binary ->
if (binary is org.jetbrains.kotlin.gradle.plugin.mpp.Framework) {
binary.linkerOpts.add("-lsqlite3") // without this we get a lot of "Undefined symbol _co_touchlab_sqliter..." errors in Xcode
baseName = "ComposeApp"
isStatic = true
}
}
}
applyDefaultHierarchyTemplate()
sourceSets {
val desktopMain by getting
commonMain.dependencies {
implementation(project(":BankingPersistence"))
implementation(libs.banking.client.model)
implementation(libs.fints4k.banking.client)
implementation(libs.bank.finder)
implementation(libs.epcqrcode)
implementation(libs.kcsv)
implementation(libs.klf)
implementation(libs.kotlinx.serializable)
implementation(libs.sqldelight.runtime)
implementation(libs.sqldelight.coroutines.extensions)
implementation(libs.sqldelight.paging.extensions)
// UI
implementation(compose.runtime)
implementation(compose.foundation)
@ -107,20 +92,12 @@ kotlin {
androidMain.dependencies {
implementation(compose.preview)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.fragment) // to fix bug IllegalArgumentException: Can only use lower 16 bits for requestCode
implementation(libs.androidx.biometric)
implementation(libs.favre.bcrypt)
// for reading EPC QR Codes from camera
implementation(libs.zxing.core)
implementation(libs.camerax.camera2)
implementation(libs.camerax.view)
implementation(libs.camerax.lifecycle)
implementation(libs.sqldelight.android.driver)
}
iosMain.dependencies {
nativeMain.dependencies {
implementation(libs.sqldelight.native.driver)
}
jvmTest.dependencies {
@ -131,10 +108,21 @@ kotlin {
implementation(compose.desktop.currentOs)
implementation(libs.kotlinx.coroutines.swing)
implementation(libs.favre.bcrypt)
implementation(libs.sqldelight.sqlite.driver)
implementation(libs.logback)
implementation(libs.janino)
}
}
}
sqldelight {
databases {
create("BankmeisterDb") {
packageName.set("net.codinux.banking.dataaccess")
generateAsync = true
schemaOutputDirectory = file("src/commonMain/sqldelight/databases")
}
}
}
@ -152,8 +140,8 @@ android {
applicationId = "net.codinux.banking.android" // the appId of the old Bankmeister app to be able to use the old PlayStore entry
minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 21
versionName = "1.0.0-Alpha-15"
versionCode = 10
versionName = "1.0.0-Alpha-12"
}
packaging {
resources {
@ -201,34 +189,11 @@ compose.desktop {
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
modules("java.sql", "java.naming") // java.naming is required by logback
packageName = "Bankmeister"
packageVersion = "0.9.0" // minor version < 1 (DMG) and dashes as in '1.0.0-Alpha-14' (DMG, MSI, RPM) are not allowed
packageName = "net.codinux.banking.ui"
packageVersion = "1.0.0"
description = "Datenschutzfreundliche Multi-Banking App für die meisten deutschen Banken"
copyright = "© 2024 codinux GmbH & Co.KG. All rights reserved."
vendor = "codinux GmbH & Co.KG"
macOS {
bundleID = "net.codinux.banking.ui"
appCategory = "public.app-category.finance"
dmgPackageVersion = "1.0.0"
iconFile = project.file("../docs/res/AppIcons/distributions/AppIcon.icns")
}
windows {
// a unique ID, which enables users to update an app via installer, when an updated version is newer, than an installed version.
// The value must remain constant for a single application. See [the link](https://wixtoolset.org/documentation/manual/v3/howtos/general/generate_guids.html) for details on generating a UUID.
upgradeUuid = "F62896E2-382E-4311-9683-1AB3AA4EB9E7"
menu = true
iconFile = project.file("../docs/res/AppIcons/distributions/AppIcon.ico")
}
linux {
iconFile = project.file("../docs/res/AppIcons/distributions/AppIcon.png")
}
}
buildTypes.release.proguard {
@ -237,58 +202,3 @@ compose.desktop {
}
}
}
gradle.taskGraph.whenReady {
tasks.named<AbstractJarsFlattenTask>("flattenJars") {
removeThirdPartySignaturesFromJar()
}
tasks.named<AbstractJarsFlattenTask>("flattenReleaseJars") {
removeThirdPartySignaturesFromJar()
}
}
// Signatures of third party libraries get copied to output jar's META-INF folder so that java -jar refuses to run created uber jar:
// Error: A JNI error has occurred, please check your installation and try again
// Exception in thread "main" java.lang.SecurityException: Invalid signature file digest for Manifest main attributes
// at java.base/sun.security.util.SignatureFileVerifier.processImpl(SignatureFileVerifier.java:340)
// at java.base/sun.security.util.SignatureFileVerifier.process(SignatureFileVerifier.java:282)
// at java.base/java.util.jar.JarVerifier.processEntry(JarVerifier.java:276)
//
// -> remove signatures of third party libraries from jar's META-INF folder
fun AbstractJarsFlattenTask.removeThirdPartySignaturesFromJar() {
val outputJar = (this.flattenedJar as? FileSystemLocationProperty<*>)?.asFile?.get()
doLast {
if (outputJar != null && outputJar.exists()) {
val extractedFilesFolder = File(outputJar.parentFile, "extracted").also { it.mkdirs() }
extractedFilesFolder.deleteRecursively()
project.copy { // unzip jar file
from(project.zipTree(outputJar))
into(extractedFilesFolder)
}
// Remove unwanted META-INF files (*.SF, *.DSA, *.RSA)
project.fileTree(extractedFilesFolder.resolve("META-INF")).matching {
include("*.SF", "*.DSA", "*.RSA")
}.forEach {
it.delete() // Delete the matching signature files
}
outputJar.delete() // Remove the original JAR
// Zip the modified content back into a new JAR using Ant
ant.withGroovyBuilder {
"zip"(
"destfile" to outputJar,
"basedir" to extractedFilesFolder
)
}
// Clean up the temporary directory
extractedFilesFolder.deleteRecursively()
}
}
}

View File

@ -2,10 +2,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
<uses-permission android:name="android.permission.CAMERA" />
<application
android:allowBackup="true"

View File

@ -1,55 +1,30 @@
package net.codinux.banking.ui
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.fragment.app.FragmentActivity
import net.codinux.banking.persistence.AndroidContext
import net.codinux.banking.ui.service.AuthenticationService
import net.codinux.banking.ui.service.BiometricAuthenticationService
class MainActivity : FragmentActivity() {
private val request = ActivityResultContracts.RequestMultiplePermissions()
private val activityResultLauncher = registerForActivityResult(request) { }
import app.cash.sqldelight.async.coroutines.synchronous
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
import net.codinux.banking.dataaccess.BankmeisterDb
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.service.ImageService
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidContext.applicationContext = this.applicationContext
ImageService.context = this.applicationContext
AuthenticationService.biometricAuthenticationService = BiometricAuthenticationService(this)
DI.setRepository(AndroidSqliteDriver(BankmeisterDb.Schema.synchronous(), this, "Bankmeister.db"))
setContent {
App()
}
}
fun requestPermissions(requiredPermissions: List<String>): Boolean {
val requiredPermissionsArray = requiredPermissions.toTypedArray()
activityResultLauncher.launch(requiredPermissionsArray)
var result = request.getSynchronousResult(baseContext, requiredPermissionsArray)
while (result == null) {
result = request.getSynchronousResult(baseContext, requiredPermissionsArray)
}
return if (result.value != null) {
val allPermissionsGranted = result.value.entries.filter { it.key in requiredPermissions }.all { it.value == true }
allPermissionsGranted
} else {
false
}
}
}
@Preview
@Composable
fun AppAndroidPreview() {

View File

@ -1,10 +1,8 @@
package net.codinux.banking.ui
import android.os.Build
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.CoroutineDispatcher
@ -13,18 +11,6 @@ import kotlinx.coroutines.Dispatchers
actual val Dispatchers.IOorDefault: CoroutineDispatcher
get() = Dispatchers.IO
actual fun KeyEvent.isBackButtonPressedEvent(): Boolean =
this.nativeKeyEvent.keyCode == android.view.KeyEvent.KEYCODE_BACK
@Composable
actual fun systemPaddings(): PaddingValues = PaddingValues(0.dp)
actual fun addKeyboardVisibilityListener(onKeyboardVisibilityChanged: (Boolean) -> Unit) {
// TODO: may implement, but currently only relevant for iOS
}
@Composable
actual fun rememberScreenSizeInfo(): ScreenSizeInfo {
val config = LocalConfiguration.current

View File

@ -10,8 +10,8 @@ import net.codinux.banking.ui.forms.RoundedCornersCard
@Preview
@Composable
fun HoldingListItemPreview() {
val holding1 = Holding("MUL Amundi MSCI World V", null, null, 1693.0, "EUR", Amount("18578.04"), Amount("16.888"), -0.35f, Amount("17944.48"), Amount("16.828"))
val holding2 = Holding("NVIDIA Corp.", null, null, 214.0, "EUR", Amount("21455.36"), Amount("100.18"), 8.8f, Amount("19872.04"), Amount("92.04"))
val holding1 = Holding("MUL Amundi MSCI World V", null, null, 1693, "EUR", Amount("18578.04"), Amount("16.888"), -0.35f, Amount("17944.48"), Amount("16.828"))
val holding2 = Holding("NVIDIA Corp.", null, null, 214, "EUR", Amount("21455.36"), Amount("100.18"), 8.8f, Amount("19872.04"), Amount("92.04"))
RoundedCornersCard {
Column {

View File

@ -29,19 +29,6 @@ fun EnterTanDialogPreview_TanImage() {
val bank = BankViewInfo("10010010", "Ihr krasser Login Name", "Phantasie Bank", BankingGroup.Comdirect)
val tanChallenge = TanChallenge(TanChallengeType.Image, ActionRequiringTan.TransferMoney, "Geben Sie die TAN ein", tanMethod.identifier, listOf(tanMethod), null, emptyList(), tanImage, null, bank)
EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { }
}
@Preview
@Composable
fun EnterTanDialogPreview_TanImage_DecodingError() {
val tanMethod = TanMethod("photoTAN-Verfahren", TanMethodType.photoTan, "902", 6, AllowedTanFormat.Numeric)
val tanImage = TanImage(null, null, "Ja Hoppla, da ist dann wohl etwas schief gelaufen. Hinterfragen Sie Ihre Existenz woran das liegen könnte!")
val bank = BankViewInfo("10010010", "Ihr krasser Login Name", "Phantasie Bank", BankingGroup.Comdirect)
val tanChallenge = TanChallenge(TanChallengeType.Image, ActionRequiringTan.GetAccountInfo, "Geben Sie die TAN ein", tanMethod.identifier, listOf(tanMethod), null, emptyList(), tanImage, null, bank)
EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { }
@ -54,7 +41,7 @@ fun EnterTanDialogPreview_WithMultipleTanMedia() { // shows that dialog is reall
val tanImage = TanImage("image/png", tanImageBytes)
val tanMethods = listOf(
TanMethod("chipTAN optisch", TanMethodType.ChipTanFlickerCode, "911", 6, AllowedTanFormat.Numeric),
TanMethod("chipTAN optisch", TanMethodType.ChipTanFlickercode, "911", 6, AllowedTanFormat.Numeric),
TanMethod("chipTAN-QR", TanMethodType.ChipTanQrCode, "913", 6, AllowedTanFormat.Numeric)
)
@ -73,20 +60,10 @@ fun EnterTanDialogPreview_WithMultipleTanMedia() { // shows that dialog is reall
@Preview
@Composable
fun EnterTanDialogPreview_FlickerCode() {
val tanMethods = listOf(TanMethod("chipTAN Flickercode", TanMethodType.ChipTanFlickerCode, "902"))
fun EnterTanDialogPreview_Flickercode() {
val tanMethods = listOf(TanMethod("chipTAN Flickercode", TanMethodType.ChipTanFlickercode, "902"))
val bank = BankViewInfo("12345678", "SupiDupiNutzer", "Abzockbank", BankingGroup.Postbank)
val tanChallenge = TanChallenge(TanChallengeType.FlickerCode, ActionRequiringTan.GetTanMedia, "Geben Sie die TAN ein", tanMethods.first().identifier, tanMethods, bank = bank, flickerCode = FlickerCode("100880077104", "0604800771040F"))
EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { }
}
@Preview
@Composable
fun EnterTanDialogPreview_FlickerCode_DecodingError() {
val tanMethods = listOf(TanMethod("chipTAN Flickercode", TanMethodType.ChipTanFlickerCode, "902"))
val bank = BankViewInfo("12345678", "SupiDupiNutzer", "Abzockbank", BankingGroup.Postbank)
val tanChallenge = TanChallenge(TanChallengeType.FlickerCode, ActionRequiringTan.ChangeTanMedium, "Geben Sie die TAN ein", tanMethods.first().identifier, tanMethods, bank = bank, flickerCode = FlickerCode("100880077104", null, decodingError = "Ja Hoppla, da ist dann wohl etwas schief gelaufen."))
val tanChallenge = TanChallenge(TanChallengeType.Flickercode, ActionRequiringTan.GetAccountInfo, "Geben Sie die TAN ein", tanMethods.first().identifier, tanMethods, bank = bank, flickerCode = FlickerCode("", ""))
EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { }
}

View File

@ -1,24 +0,0 @@
package net.codinux.banking.ui.forms
import androidx.compose.runtime.*
import androidx.compose.ui.tooling.preview.Preview
@Preview
@Composable
fun SegmentedControlPreview() {
SegmentedControl(
options = listOf("Option 1", "Option 2", "Option 3"),
selectedOption = "Option 1",
onOptionSelected = { }
)
}
@Preview
@Composable
fun SegmentedControlPreview_OnlyTwoOptions() {
SegmentedControl(
options = listOf("Option 1", "Option 2"),
selectedOption = "Option 2",
onOptionSelected = { }
)
}

View File

@ -1,15 +0,0 @@
package net.codinux.banking.ui.screens
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import net.codinux.banking.ui.model.settings.AppAuthenticationMethod
import net.codinux.banking.ui.model.settings.AppSettings
@Preview
@Composable
fun ProtectAppSettingsDialogPreview() {
val appSettings = AppSettings(AppAuthenticationMethod.Password)
ProtectAppSettingsDialog(appSettings) { }
}

View File

@ -1,29 +0,0 @@
package net.codinux.banking.ui.service
import at.favre.lib.crypto.bcrypt.BCrypt
import net.codinux.banking.ui.model.AuthenticationResult
actual object AuthenticationService {
internal var biometricAuthenticationService: BiometricAuthenticationService? = null
actual fun hashPassword(password: String): String =
BCrypt.withDefaults().hashToString(12, password.toCharArray())
actual fun checkPassword(password: String, hashedPassword: String): Boolean =
BCrypt.verifyer().verify(password.toCharArray(), hashedPassword).verified
actual val supportsBiometricAuthentication: Boolean
get() = biometricAuthenticationService?.supportsBiometricAuthentication ?: false
actual fun authenticateWithBiometrics(authenticationResult: (AuthenticationResult) -> Unit) {
if (biometricAuthenticationService != null) {
biometricAuthenticationService!!.authenticate(null, authenticationResult)
} else {
authenticationResult(AuthenticationResult(false, "Biometrics is not supported"))
}
}
}

View File

@ -1,66 +0,0 @@
package net.codinux.banking.ui.service
import android.os.Build
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import net.codinux.banking.ui.R
import net.codinux.banking.ui.model.AuthenticationResult
import javax.crypto.Cipher
class BiometricAuthenticationService(
private val activity: FragmentActivity
) {
private val biometricManager: BiometricManager = BiometricManager.from(activity)
private val allowedAuthenticators = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) BIOMETRIC_STRONG
else BIOMETRIC_STRONG or BIOMETRIC_WEAK
val supportsBiometricAuthentication: Boolean by lazy {
biometricManager.canAuthenticate(allowedAuthenticators) == BiometricManager.BIOMETRIC_SUCCESS
}
fun authenticate(cipher: Cipher?, authenticationResult: (AuthenticationResult) -> Unit) {
val executor = ContextCompat.getMainExecutor(this.activity)
val biometricPrompt = BiometricPrompt(activity, executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errorString: CharSequence) {
super.onAuthenticationError(errorCode, errorString)
authenticationResult(AuthenticationResult(false, errorString.toString()))
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
authenticationResult(AuthenticationResult(true))
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
authenticationResult(AuthenticationResult(false))
}
})
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(this.activity.getString(R.string.activity_login_authenticate_with_biometrics_prompt))
//.setSubtitle() // TODO: add subtitle?
.setNegativeButtonText(this.activity.getString(android.R.string.cancel)) // is not allowed when device credentials are allowed
.setAllowedAuthenticators(allowedAuthenticators)
.build()
if (cipher == null) {
biometricPrompt.authenticate(promptInfo)
} else {
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
}
}
}

View File

@ -1,16 +1,22 @@
package net.codinux.banking.ui.service
import android.content.Context
import android.graphics.BitmapFactory
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import net.codinux.banking.persistence.AndroidContext
import net.codinux.log.Log
import java.io.File
import java.net.URL
import java.security.MessageDigest
private val cacheDir by lazy { File(AndroidContext.applicationContext.cacheDir, "imageCache").also { it.mkdirs() } }
object ImageService {
lateinit var context: Context
}
private val cacheDir by lazy { File(ImageService.context.cacheDir, "imageCache").also { it.mkdirs() } }
private val messageDigest = MessageDigest.getInstance("SHA-256")

View File

@ -1,13 +0,0 @@
package net.codinux.banking.ui.service
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.content.ContextCompat
object PermissionsService {
fun allPermissionsGranted(baseContext: Context, permissions: List<String>) = permissions.all {
ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
}
}

View File

@ -1,186 +0,0 @@
package net.codinux.banking.ui.service
import android.Manifest
import android.content.pm.PackageManager
import android.view.ScaleGestureDetector
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import com.google.zxing.*
import com.google.zxing.common.HybridBinarizer
import com.google.zxing.qrcode.QRCodeReader
import net.codinux.banking.persistence.AndroidContext
import net.codinux.banking.ui.MainActivity
import net.codinux.log.logger
import java.nio.ByteBuffer
import java.util.concurrent.Executors
actual object QrCodeService {
private val RequiredPermissions = listOf(Manifest.permission.CAMERA)
private val cameraExecutor = Executors.newCachedThreadPool()
private val log by logger()
actual val supportsReadingQrCodesFromCamera = hasCamera()
private fun hasCamera(): Boolean = AndroidContext.applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
@Composable
actual fun readQrCodeFromCamera(resultCallback: (QrCodeReadResult) -> Unit) {
val mainActivity = LocalLifecycleOwner.current as MainActivity // we only have MainActivity, so we can be sure that LocalLifecycleOwner.current is MainActivity
if (PermissionsService.allPermissionsGranted(AndroidContext.applicationContext, RequiredPermissions) == false &&
mainActivity.requestPermissions(RequiredPermissions) == false) {
return // we don't have the permission to start the camera
}
val previewView = remember {
PreviewView(mainActivity)
}
setupCameraView(previewView, mainActivity, resultCallback)
AndroidView(factory = { previewView }, modifier = Modifier.fillMaxSize())
}
private fun setupCameraView(previewView: PreviewView, mainActivity: MainActivity, resultCallback: (QrCodeReadResult) -> Unit) {
val cameraProviderFuture = ProcessCameraProvider.getInstance(mainActivity)
cameraProviderFuture.addListener({
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
val imageAnalyzer = ImageAnalysis.Builder()
.build()
.also {
it.setAnalyzer(cameraExecutor, QrCodeImageAnalyzer(resultCallback))
}
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
val camera = cameraProvider.bindToLifecycle(mainActivity, cameraSelector, preview, imageAnalyzer)
configureCameraControl(camera, previewView, mainActivity)
} catch (e: Exception) {
log.error(e) { "Use case binding failed" }
}
}, ContextCompat.getMainExecutor(mainActivity))
}
private fun configureCameraControl(
camera: Camera,
previewView: PreviewView,
mainActivity: MainActivity
) {
// Listen to pinch gestures
val listener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
// Get the camera's current zoom ratio
val currentZoomRatio = camera.cameraInfo.zoomState.value?.zoomRatio ?: 0F
// Get the pinch gesture's scaling factor
val delta = detector.scaleFactor
// Update the camera's zoom ratio. This is an asynchronous operation that returns
// a ListenableFuture, allowing you to listen to when the operation completes.
camera.cameraControl.setZoomRatio(currentZoomRatio * delta)
return true // Return true, as the event was handled
}
}
val scaleGestureDetector = ScaleGestureDetector(mainActivity, listener)
previewView.setOnTouchListener { _, event ->
scaleGestureDetector.onTouchEvent(event)
true
}
}
}
class QrCodeImageAnalyzer(private val resultCallback: (QrCodeReadResult) -> Unit) : ImageAnalysis.Analyzer {
private val reader = QRCodeReader()
private val readerHints = readerHintsForCharset(Charsets.UTF_8.name())
private val log by logger()
override fun analyze(image: ImageProxy) {
try {
val bitmap = getBinaryBitmap(image)
val result = reader.decode(bitmap, readerHints)
if (result != null && result.text != null) {
this.resultCallback(QrCodeReadResult(result.text))
}
} catch (e: Throwable) {
if (e !is NotFoundException) {
log.error(e) { "Could not decode image to QR code" }
}
}
image.close() // to continue image analysis / avoid blocking production of further images
}
private fun ByteBuffer.toIntArray(): IntArray {
val bytes = this.toByteArray()
val pixels = IntArray(bytes.size)
bytes.indices.forEach { index ->
pixels[index] = bytes[index].toInt() and 0xFF
}
return pixels
}
private fun ByteBuffer.toByteArray(): ByteArray {
rewind() // Rewind the buffer to zero
val data = ByteArray(remaining())
get(data) // Copy the buffer into a byte array
return data // Return the byte array
}
private fun getBinaryBitmap(image: ImageProxy): BinaryBitmap {
val buffer = image.planes[0].buffer
val bitmapBuffer = buffer.toIntArray()
val luminanceSource = RGBLuminanceSource(image.width, image.height, bitmapBuffer)
return BinaryBitmap(HybridBinarizer(luminanceSource))
}
private fun readerHintsForCharset(charset: String): Map<DecodeHintType, *> = buildMap {
// put(DecodeHintType.TRY_HARDER, true) // optimize for accuracy, not speed
put(DecodeHintType.CHARACTER_SET, charset)
}
}

View File

@ -1,5 +1,3 @@
<resources>
<string name="app_name">Bankmeister</string>
<string name="activity_login_authenticate_with_biometrics_prompt">Authentifizieren Sich sich um die App zu entsperren</string>
</resources>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -0,0 +1,47 @@
package net.codinux.banking.dataaccess
import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.client.model.BankAccess
import net.codinux.banking.client.model.securitiesaccount.Holding
import net.codinux.banking.dataaccess.entities.AccountTransactionEntity
import net.codinux.banking.dataaccess.entities.BankAccountEntity
import net.codinux.banking.dataaccess.entities.HoldingEntity
import net.codinux.banking.dataaccess.entities.BankAccessEntity
import net.codinux.banking.ui.model.AccountTransactionViewModel
import net.codinux.banking.ui.model.settings.AppSettings
import net.codinux.banking.ui.settings.UiSettings
interface BankingRepository {
fun getAppSettings(): AppSettings?
suspend fun saveAppSettings(settings: AppSettings)
fun getUiSettings(settings: UiSettings)
suspend fun saveUiSettings(settings: UiSettings)
fun getAllBanks(): List<BankAccessEntity>
suspend fun persistBank(bank: BankAccess): BankAccessEntity
suspend fun persistTransactions(bankAccount: BankAccountEntity, transactions: List<AccountTransaction>): List<AccountTransactionEntity>
suspend fun persistHoldings(bankAccount: BankAccountEntity, holdings: List<Holding>): List<HoldingEntity>
suspend fun updateHoldings(holdings: List<HoldingEntity>)
suspend fun deleteHoldings(holdings: List<HoldingEntity>)
fun getAllAccountTransactionsAsViewModel(): List<AccountTransactionViewModel>
fun getAllAccountTransactions(): List<AccountTransactionEntity>
fun getAllTransactionsForBank(bank: BankAccessEntity): List<AccountTransactionEntity>
fun getTransactionById(transactionId: Long): AccountTransactionEntity?
}

View File

@ -1,20 +1,15 @@
package net.codinux.banking.persistence
package net.codinux.banking.dataaccess
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.client.model.Amount
import net.codinux.banking.client.model.BankAccess
import net.codinux.banking.client.model.securitiesaccount.Holding
import net.codinux.banking.persistence.entities.AccountTransactionEntity
import net.codinux.banking.persistence.entities.BankAccountEntity
import net.codinux.banking.persistence.entities.HoldingEntity
import net.codinux.banking.persistence.entities.BankAccessEntity
import net.codinux.banking.persistence.entities.UiSettingsEntity
import net.codinux.banking.dataaccess.entities.AccountTransactionEntity
import net.codinux.banking.dataaccess.entities.BankAccountEntity
import net.codinux.banking.dataaccess.entities.HoldingEntity
import net.codinux.banking.dataaccess.entities.BankAccessEntity
import net.codinux.banking.ui.model.AccountTransactionViewModel
import net.codinux.banking.ui.model.settings.AppSettings
import net.codinux.banking.ui.model.settings.ImageSettings
import net.codinux.banking.ui.model.settings.TransactionsGrouping
import net.codinux.banking.ui.settings.UiSettings
class InMemoryBankingRepository(
banks: Collection<BankAccess> = emptyList(),
@ -28,9 +23,7 @@ class InMemoryBankingRepository(
private val transactions = transactions.map { map(it) }.toMutableList()
private var uiSettings: UiSettingsEntity = UiSettingsEntity(true, TransactionsGrouping.Month, true, true, true)
private var imageSettings = mutableMapOf<String, ImageSettings>()
private lateinit var uiSettings: UiSettings
override fun getAppSettings(): AppSettings? = appSettings
@ -39,17 +32,12 @@ class InMemoryBankingRepository(
this.appSettings = settings
}
override fun getUiSettings() = this.uiSettings
override suspend fun saveUiSettings(settings: UiSettingsEntity) {
override fun getUiSettings(settings: UiSettings) {
this.uiSettings = settings
}
override fun getImageSettings(id: String) = imageSettings[id]
override suspend fun saveImageSettings(settings: ImageSettings) {
imageSettings[settings.id] = settings
override suspend fun saveUiSettings(settings: UiSettings) {
this.uiSettings = settings
}
@ -61,27 +49,6 @@ class InMemoryBankingRepository(
return entity
}
override suspend fun updateBank(bank: BankAccessEntity, loginName: String, password: String, bankName: String?) {
// no-op
}
override suspend fun updateBank(bank: BankAccessEntity, serializedClientData: String?) {
// no-op
}
override suspend fun updateAccount(account: BankAccountEntity, userSetDisplayName: String?, hideAccount: Boolean, includeInAutomaticAccountsUpdate: Boolean) {
// no-op
}
override suspend fun updateAccount(account: BankAccountEntity, balance: Amount, lastAccountUpdateTime: Instant, retrievedTransactionsFrom: LocalDate?) {
// no-op
}
override suspend fun deleteBank(bank: BankAccessEntity) {
this.banks.remove(bank)
}
override suspend fun persistTransactions(bankAccount: BankAccountEntity, transactions: List<AccountTransaction>): List<AccountTransactionEntity> {
throw NotImplementedError("Lazy developer, method is not implemented")
}
@ -109,10 +76,6 @@ class InMemoryBankingRepository(
override fun getTransactionById(transactionId: Long): AccountTransactionEntity? =
getAllAccountTransactions().firstOrNull { it.id == transactionId }
override suspend fun updateTransaction(transaction: AccountTransactionEntity, userSetOtherPartyName: String?, userSetReference: String?, notes: String?) {
// no-op
}
private fun map(bank: BankAccess) = BankAccessEntity(
nextId++,

View File

@ -1,53 +1,25 @@
package net.codinux.banking.persistence
package net.codinux.banking.dataaccess
import app.cash.sqldelight.db.QueryResult
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.db.SqlSchema
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.client.model.Amount
import net.codinux.banking.client.model.BankAccess
import net.codinux.banking.client.model.BankAccount
import net.codinux.banking.client.model.BankAccountFeatures
import net.codinux.banking.client.model.BankAccountType
import net.codinux.banking.client.model.BankingGroup
import net.codinux.banking.client.model.*
import net.codinux.banking.client.model.securitiesaccount.Holding
import net.codinux.banking.client.model.tan.AllowedTanFormat
import net.codinux.banking.client.model.tan.MobilePhoneTanMedium
import net.codinux.banking.client.model.tan.TanGeneratorTanMedium
import net.codinux.banking.client.model.tan.TanMedium
import net.codinux.banking.client.model.tan.TanMediumStatus
import net.codinux.banking.client.model.tan.TanMediumType
import net.codinux.banking.client.model.tan.TanMethod
import net.codinux.banking.client.model.tan.TanMethodType
import net.codinux.banking.client.fints4k.FinTs4kMapper
import net.codinux.banking.persistence.entities.*
import net.codinux.banking.client.model.tan.*
import net.codinux.banking.dataaccess.entities.*
import net.codinux.banking.ui.model.AccountTransactionViewModel
import net.codinux.banking.ui.model.settings.*
import net.codinux.banking.ui.model.TransactionsGrouping
import net.codinux.banking.ui.model.settings.AppAuthenticationMethod
import net.codinux.banking.ui.model.settings.AppSettings
import net.codinux.banking.ui.model.settings.ImageSettings
import net.codinux.banking.ui.settings.UiSettings
import net.codinux.log.logger
import kotlin.enums.EnumEntries
import kotlin.js.JsName
import kotlin.jvm.JvmName
expect fun createSqlDriverDriver(dbName: String, schema: SqlSchema<QueryResult.AsyncValue<Unit>>, version: Long): SqlDriver
open class SqliteBankingRepository : BankingRepository {
companion object {
val TanMethodTypesToMigrate = mapOf(
"ChipTanManuell" to TanMethodType.ChipTanManual.name,
"ChipTanFlickercode" to TanMethodType.ChipTanFlickerCode.name
)
}
private val schema = BankmeisterDb.Schema
private val sqlDriver = createSqlDriverDriver("Bankmeister.db", schema, 2L)
open class SqliteBankingRepository(
sqlDriver: SqlDriver
) : BankingRepository {
private val database = BankmeisterDb(sqlDriver)
@ -80,30 +52,18 @@ open class SqliteBankingRepository : BankingRepository {
}
override fun getUiSettings(): UiSettingsEntity? {
return settingsQueries.getUiSettings { _, transactionsGrouping, showBalance, showBankIcons, showColoredAmounts, showTransactionsInAlternatingColors ->
UiSettingsEntity(
showBalance,
mapToEnum(transactionsGrouping, TransactionsGrouping.entries),
showTransactionsInAlternatingColors,
showBankIcons,
showColoredAmounts
)
override fun getUiSettings(settings: UiSettings) {
settingsQueries.getUiSettings { _, transactionsGrouping, showBalance, showBankIcons, showColoredAmounts, showTransactionsInAlternatingColors ->
settings.transactionsGrouping.value = mapToEnum(transactionsGrouping, TransactionsGrouping.entries)
settings.showBalance.value = showBalance
settings.showBankIcons.value = showBankIcons
settings.showColoredAmounts.value = showColoredAmounts
settings.showTransactionsInAlternatingColors.value = showTransactionsInAlternatingColors
}.executeAsOneOrNull()
}
override suspend fun saveUiSettings(settings: UiSettingsEntity) {
settingsQueries.upsertUiSettings(mapEnum(settings.transactionsGrouping), settings.showBalance, settings.showBankIcons, settings.showColoredAmounts, settings.showTransactionsInAlternatingColors)
}
override fun getImageSettings(id: String): ImageSettings? =
settingsQueries.getImageSettings(id) { height, frequency ->
ImageSettings(id, mapToInt(height), mapToInt(frequency))
}.executeAsOneOrNull()
override suspend fun saveImageSettings(settings: ImageSettings) {
settingsQueries.upsertImageSettings(settings.id, mapInt(settings.height), mapInt(settings.frequency))
override suspend fun saveUiSettings(settings: UiSettings) {
settingsQueries.upsertUiSettings(mapEnum(settings.transactionsGrouping.value), settings.showBalance.value, settings.showBankIcons.value, settings.showColoredAmounts.value, settings.showTransactionsInAlternatingColors.value)
}
@ -113,9 +73,9 @@ open class SqliteBankingRepository : BankingRepository {
val tanMedia = getAllTanMedia().groupBy { it.bankId }.mapValues { it.value.toMutableList() }
val holdings = getAllHoldings().groupBy { it.accountId }
return bankQueries.getAllBanks { id, domesticBankCode, loginName, password, bankName, bic, customerName, userId, selectedTanMethodIdentifier, selectedTanMediumIdentifier, bankingGroup, serverAddress, countryCode, serializedClientData, userSetDisplayName, displayIndex, iconUrl, wrongCredentialsEntered ->
return bankQueries.getAllBanks { id, domesticBankCode, loginName, password, bankName, bic, customerName, userId, selectedTanMethodIdentifier, selectedTanMediumIdentifier, bankingGroup, serverAddress, countryCode, userSetDisplayName, clientData, displayIndex, iconUrl, wrongCredentialsEntered ->
BankAccessEntity(id, domesticBankCode, loginName, password, bankName, bic, customerName, userId, getAccountsOfBank(id, bankAccounts, holdings), selectedTanMethodIdentifier, tanMethods[id] ?: mutableListOf(), selectedTanMediumIdentifier, tanMedia[id] ?: mutableListOf(),
bankingGroup?.let { BankingGroup.valueOf(it) }, serverAddress, countryCode, userSetDisplayName, displayIndex.toInt(), iconUrl, wrongCredentialsEntered, null, serializedClientData)
bankingGroup?.let { BankingGroup.valueOf(it) }, serverAddress, countryCode, userSetDisplayName, displayIndex.toInt(), iconUrl, wrongCredentialsEntered)
}.executeAsList()
}
@ -129,7 +89,7 @@ open class SqliteBankingRepository : BankingRepository {
return bankQueries.transactionWithResult {
bankQueries.insertBank(bank.domesticBankCode, bank.loginName, bank.password, bank.bankName, bank.bic,
bank.customerName, bank.userId, bank.selectedTanMethodIdentifier, bank.selectedTanMediumIdentifier,
bank.bankingGroup?.name, bank.serverAddress, bank.countryCode, bank.serializedClientData, bank.userSetDisplayName, bank.displayIndex.toLong(), bank.iconUrl, bank.wrongCredentialsEntered
bank.bankingGroup?.name, bank.serverAddress, bank.countryCode, null, bank.userSetDisplayName, bank.displayIndex.toLong(), bank.iconUrl, bank.wrongCredentialsEntered
)
val bankId = getLastInsertedId() // getLastInsertedId() / last_insert_rowid() has to be called in a transaction with the insert operation, otherwise it will not work
@ -143,58 +103,6 @@ open class SqliteBankingRepository : BankingRepository {
}
}
override suspend fun updateBank(bank: BankAccessEntity, loginName: String, password: String, userSetDisplayName: String?) {
bankQueries.transaction {
if (bank.loginName != loginName) {
bankQueries.updateBankLoginName(loginName, bank.id)
}
if (bank.password != password) {
bankQueries.updateBankPassword(password, bank.id)
}
if (bank.userSetDisplayName != userSetDisplayName) {
bankQueries.updateBankUserSetDisplayName(userSetDisplayName, bank.id)
}
}
}
override suspend fun updateBank(bank: BankAccessEntity, serializedClientData: String?) {
bankQueries.transaction {
if (serializedClientData != null) {
bankQueries.updateBankClientData(serializedClientData, bank.id)
}
}
}
override suspend fun updateAccount(account: BankAccountEntity, userSetDisplayName: String?, hideAccount: Boolean, includeInAutomaticAccountsUpdate: Boolean) {
bankQueries.transaction {
if (account.userSetDisplayName != userSetDisplayName) {
bankQueries.updateBankAccountUserSetDisplayName(userSetDisplayName, account.id)
}
if (account.hideAccount != hideAccount) {
bankQueries.updateBankAccountHideAccount(hideAccount, account.id)
}
if (account.includeInAutomaticAccountsUpdate != includeInAutomaticAccountsUpdate) {
bankQueries.updateBankAccountIncludeInAutomaticAccountsUpdate(includeInAutomaticAccountsUpdate, account.id)
}
}
}
override suspend fun updateAccount(account: BankAccountEntity, balance: Amount, lastAccountUpdateTime: Instant, retrievedTransactionsFrom: LocalDate?) {
bankQueries.updateBankAccount(mapAmount(balance), mapInstant(lastAccountUpdateTime), mapDate(retrievedTransactionsFrom), account.id)
}
override suspend fun deleteBank(bank: BankAccessEntity) {
bankQueries.transaction {
accountTransactionQueries.deleteTransactionsByBankId(bankId = bank.id)
bankQueries.deleteBank(bank.id)
}
}
fun getAllBankAccounts(): List<BankAccountEntity> = bankQueries.getAllBankAccounts { id, bankId, identifier, subAccountNumber, iban, productName, accountHolderName, type, currency, accountLimit, isAccountTypeSupportedByApplication, features, balance, serverTransactionsRetentionDays, lastAccountUpdateTime, retrievedTransactionsFrom, userSetDisplayName, displayIndex, hideAccount, includeInAutomaticAccountsUpdate ->
BankAccountEntity(
@ -259,7 +167,7 @@ open class SqliteBankingRepository : BankingRepository {
bankId,
displayName,
mapToEnum(type, TanMethodType.entries, FinTs4kMapper.TanMethodTypesToMigrate),
mapToEnum(type, TanMethodType.entries),
identifier,
mapToInt(maxTanInputLength),
mapToEnum(allowedTanFormat, AllowedTanFormat.entries),
@ -349,7 +257,7 @@ open class SqliteBankingRepository : BankingRepository {
protected open fun getAllHoldings(): List<HoldingEntity> =
accountTransactionQueries.selectAllHoldings { id, bankId, accountId, name, isin, wkn, quantity, currency, totalBalance, marketValue, performancePercentage, totalCostPrice, averageCostPrice, pricingTime, buyingDate ->
HoldingEntity(id, bankId, accountId, name, isin, wkn, quantity, currency, mapToAmount(totalBalance), mapToAmount(marketValue), performancePercentage?.toFloat(), mapToAmount(totalCostPrice), mapToAmount(averageCostPrice), mapToInstant(pricingTime), mapToDate(buyingDate))
HoldingEntity(id, bankId, accountId, name, isin, wkn, mapToInt(quantity), currency, mapToAmount(totalBalance), mapToAmount(marketValue), performancePercentage?.toFloat(), mapToAmount(totalCostPrice), mapToAmount(averageCostPrice), mapToInstant(pricingTime), mapToDate(buyingDate))
}.executeAsList()
override suspend fun persistHoldings(bankAccount: BankAccountEntity, holdings: List<Holding>): List<HoldingEntity> =
@ -366,7 +274,7 @@ open class SqliteBankingRepository : BankingRepository {
holding.name, holding.isin, holding.wkn,
holding.quantity, holding.currency,
mapInt(holding.quantity), holding.currency,
mapAmount(holding.totalBalance), mapAmount(holding.marketValue),
holding.performancePercentage?.toDouble(),
@ -383,7 +291,7 @@ open class SqliteBankingRepository : BankingRepository {
holdings.onEach { holding ->
accountTransactionQueries.updateHolding(
holding.name, holding.isin, holding.wkn,
holding.quantity, holding.currency,
mapInt(holding.quantity), holding.currency,
mapAmount(holding.totalBalance), mapAmount(holding.marketValue),
holding.performancePercentage?.toDouble(),
@ -407,8 +315,8 @@ open class SqliteBankingRepository : BankingRepository {
override fun getAllAccountTransactionsAsViewModel(): List<AccountTransactionViewModel> =
accountTransactionQueries.getAllTransactionsAsViewModel { id, bankId, accountId, amount, currency, reference, valueDate, otherPartyName, postingText, userSetReference, userSetOtherPartyName ->
AccountTransactionViewModel(id, bankId, accountId, mapToAmount(amount), currency, reference, mapToDate(valueDate), otherPartyName, postingText, userSetReference, userSetOtherPartyName)
accountTransactionQueries.getAllTransactionsAsViewModel { id, bankId, accountId, amount, currency, reference, valueDate, otherPartyName, postingText, userSetDisplayName, userSetOtherPartyName ->
AccountTransactionViewModel(id, bankId, accountId, mapToAmount(amount), currency, reference, mapToDate(valueDate), otherPartyName, postingText, userSetDisplayName, userSetOtherPartyName)
}.executeAsList()
override fun getAllAccountTransactions(): List<AccountTransactionEntity> {
@ -471,22 +379,6 @@ open class SqliteBankingRepository : BankingRepository {
return AccountTransactionEntity(getLastInsertedId(), bankId, accountId, transaction)
}
override suspend fun updateTransaction(transaction: AccountTransactionEntity, userSetOtherPartyName: String?, userSetReference: String?, notes: String?) {
accountTransactionQueries.transaction {
if (transaction.userSetOtherPartyName != userSetOtherPartyName) {
accountTransactionQueries.updateAccountTransactionUserSetOtherPartyName(userSetOtherPartyName, transaction.id)
}
if (transaction.userSetReference != userSetReference) {
accountTransactionQueries.updateAccountTransactionUserSetOReference(userSetReference, transaction.id)
}
if (transaction.notes != notes) {
accountTransactionQueries.updateAccountTransactionNotes(notes, transaction.id)
}
}
}
private fun getLastInsertedId(): Long =
bankQueries.getLastInsertedId().executeAsOne()
@ -605,16 +497,7 @@ open class SqliteBankingRepository : BankingRepository {
private fun <E : Enum<E>> mapEnum(enum: Enum<E>): String = enum.name
private fun <E : Enum<E>> mapToEnum(enumName: String, values: EnumEntries<E>): E =
try {
values.first { it.name == enumName }
} catch (e: Throwable) {
log.error(e) { "Could not map enumName '$enumName' to ${values.first()::class}"}
throw e
}
private fun <E : Enum<E>> mapToEnum(enumName: String, values: EnumEntries<E>, enumNamesToMigrate: Map<String, String>): E =
mapToEnum(enumNamesToMigrate[enumName] ?: enumName, values)
private fun <E : Enum<E>> mapToEnumNullable(enumName: String, values: EnumEntries<E>): E? {
val mapped = values.firstOrNull { it.name == enumName }

View File

@ -1,4 +1,4 @@
package net.codinux.banking.persistence.entities
package net.codinux.banking.dataaccess.entities
import kotlinx.datetime.LocalDate
import net.codinux.banking.client.model.AccountTransaction
@ -116,4 +116,9 @@ class AccountTransactionEntity(
transaction.isReversal,
)
override val identifier: String by lazy {
"$bankId ${super.identifier}"
}
}

View File

@ -1,7 +1,8 @@
package net.codinux.banking.persistence.entities
package net.codinux.banking.dataaccess.entities
import net.codinux.banking.client.model.BankAccess
import net.codinux.banking.client.model.BankingGroup
import net.codinux.banking.client.model.tan.TanMedium
class BankAccessEntity(
val id: Long,
@ -32,10 +33,7 @@ class BankAccessEntity(
displayIndex: Int = 0,
iconUrl: String? = null,
wrongCredentialsEntered: Boolean = false,
clientData: Any? = null,
serializedClientData: String? = null
wrongCredentialsEntered: Boolean = false
) : BankAccess(domesticBankCode, loginName, password, bankName, bic, customerName, userId, accounts, selectedTanMethodIdentifier, tanMethods, selectedTanMediumIdentifier, tanMedia, bankingGroup, serverAddress, countryCode) {
init {
@ -44,9 +42,6 @@ class BankAccessEntity(
this.iconUrl = iconUrl
this.wrongCredentialsEntered = wrongCredentialsEntered
this.clientData = clientData
this.serializedClientData = serializedClientData
}
@ -60,14 +55,4 @@ class BankAccessEntity(
bank.iconUrl, bank.wrongCredentialsEntered,
)
override val accountsSorted: List<BankAccountEntity>
get() = accounts.sortedBy { it.displayIndex }
override val tanMethodsSorted: List<TanMethodEntity>
get() = tanMethods.sortedBy { it.identifier }
override val tanMediaSorted: List<TanMediumEntity>
get() = tanMedia.sortedBy { it.status }
}

View File

@ -1,4 +1,4 @@
package net.codinux.banking.persistence.entities
package net.codinux.banking.dataaccess.entities
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate

View File

@ -1,4 +1,4 @@
package net.codinux.banking.persistence.entities
package net.codinux.banking.dataaccess.entities
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
@ -15,7 +15,7 @@ class HoldingEntity(
isin: String? = null,
wkn: String? = null,
quantity: Double? = null,
quantity: Int? = null,
currency: String? = null,
totalBalance: Amount? = null,

View File

@ -1,4 +1,4 @@
package net.codinux.banking.persistence.entities
package net.codinux.banking.dataaccess.entities
import net.codinux.banking.client.model.tan.*

View File

@ -1,4 +1,4 @@
package net.codinux.banking.persistence.entities
package net.codinux.banking.dataaccess.entities
import net.codinux.banking.client.model.tan.AllowedTanFormat
import net.codinux.banking.client.model.tan.TanMethod

View File

@ -7,14 +7,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
import net.codinux.banking.persistence.BankingRepository
import net.codinux.banking.persistence.SqliteBankingRepository
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.model.settings.AppAuthenticationMethod
import net.codinux.banking.ui.screens.LoginScreen
import net.codinux.banking.ui.screens.MainScreen
import net.codinux.log.Log
import net.codinux.log.LoggerFactory
import org.jetbrains.compose.ui.tooling.preview.Preview
@ -24,40 +19,20 @@ private val typography = Typography(
@Composable
@Preview
fun App(repository: BankingRepository? = null) {
fun App() {
LoggerFactory.defaultLoggerName = "net.codinux.banking.ui.Bankmeister"
val colors = MaterialTheme.colors.copy(primary = Colors.Primary, primaryVariant = Colors.PrimaryDark, onPrimary = Color.White,
secondary = Colors.Accent, secondaryVariant = Colors.Accent, onSecondary = Color.White)
var isInitialized by remember { mutableStateOf(false) }
try {
if (isInitialized == false) {
DI.setRepository(repository ?: SqliteBankingRepository()) // setting repository sets AppSettings, which is required below to determine if user needs to log in
}
} catch (e: Throwable) {
Log.error(e) { "Could not set repository" }
}
val appSettings = DI.uiState.appSettings.collectAsState().value
var isLoggedIn by remember(appSettings.authenticationMethod) { mutableStateOf(appSettings.authenticationMethod == AppAuthenticationMethod.None) }
val coroutineScope = rememberCoroutineScope()
MaterialTheme(colors = colors, typography = typography) {
if (isLoggedIn == false) {
LoginScreen(appSettings) {
isLoggedIn = true
}
} else {
MainScreen()
}
}
LaunchedEffect(isInitialized) {

View File

@ -1,8 +1,6 @@
package net.codinux.banking.ui
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.CoroutineDispatcher
@ -11,15 +9,6 @@ import kotlinx.coroutines.Dispatchers
expect val Dispatchers.IOorDefault: CoroutineDispatcher
expect fun KeyEvent.isBackButtonPressedEvent(): Boolean
@Composable
expect fun systemPaddings(): PaddingValues
expect fun addKeyboardVisibilityListener(onKeyboardVisibilityChanged: (Boolean) -> Unit)
@Composable
expect fun rememberScreenSizeInfo(): ScreenSizeInfo

View File

@ -40,17 +40,12 @@ fun BottomBar(showMenuDrawer: Boolean = true) {
val coroutineScope = rememberCoroutineScope()
fun toggleDrawerState() {
coroutineScope.launch {
uiState.drawerState.value.toggle()
}
}
BottomAppBar {
if (showMenuDrawer) {
IconButton(
onClick = { toggleDrawerState() }
onClick = { coroutineScope.launch {
uiState.drawerState.value.toggle()
} }
) {
Icon(Icons.Filled.Menu, contentDescription = "Open Navigation Drawer with sidebar menu")
}
@ -66,18 +61,14 @@ fun BottomBar(showMenuDrawer: Boolean = true) {
val selectedAccount = transactionsFilter.selectedAccount
val title = if (selectedAccount == null) {
if (banks.isEmpty()) {
"Bankmeister"
} else {
"Alle Konten"
}
} else if (selectedAccount.bankAccount != null) {
selectedAccount.bankAccount.displayName
} else {
selectedAccount.bank.displayName
}
Text(title, color = color, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.clickable { toggleDrawerState() })
Text(title, color = color, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}

View File

@ -1,26 +1,19 @@
package net.codinux.banking.ui.appskeleton
import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusTarget
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.*
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import net.codinux.banking.ui.composables.CloseButton
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.config.Internationalization
import net.codinux.banking.ui.forms.RoundedCornersCard
import net.codinux.banking.ui.forms.Select
import net.codinux.banking.ui.isBackButtonPressedEvent
import net.codinux.banking.ui.model.settings.TransactionsGrouping
import net.codinux.banking.ui.model.TransactionsGrouping
private val uiState = DI.uiState
@ -44,30 +37,14 @@ fun FilterBar() {
val months = listOf("Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember" /*, "1. Quartal", "2. Quartal", "3. Quartal", "4. Quartal" */, null)
val filterBarFocus = remember { FocusRequester() }
Box(
contentAlignment = Alignment.BottomEnd,
modifier = Modifier.fillMaxSize().zIndex(100f)
.padding(bottom = 64.dp, end = 74.dp).focusable(true).focusRequester(filterBarFocus).focusTarget().onKeyEvent { event ->
if (event.isBackButtonPressedEvent() || event.key == Key.Escape) {
DI.uiState.showFilterBar.value = false
true
} else {
false
}
}
.padding(bottom = 64.dp, end = 74.dp)
) {
Column(Modifier.height(190.dp).width(390.dp)) {
Column(Modifier.height(230.dp).width(390.dp)) {
RoundedCornersCard(cornerSize = 4.dp, shadowElevation = 24.dp) {
Column(Modifier.fillMaxWidth().background(Color.White).padding(16.dp)) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically) {
CloseButton("Filterbar schließen", size = 24.dp) {
uiState.showFilterBar.value = false
}
}
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Text("Umsätze", Modifier.width(labelsWidth))
@ -104,14 +81,13 @@ fun FilterBar() {
)
}
}
}
}
}
}
LaunchedEffect(filterBarFocus) {
filterBarFocus.requestFocus() // focus filter bar so that it receives key events to handle e.g. Escape button press
Row(Modifier.padding(top = 10.dp)) {
Text("Zum Schließen bitte wieder auf das Filter Icon klicken, Zurück Button etc. funtioniert nicht (herzlichen Undank UI Framework!)")
}
}
}
}
}
}

View File

@ -1,113 +0,0 @@
package net.codinux.banking.ui.appskeleton
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.config.Style.FabMenuSpacing
import net.codinux.banking.ui.config.Style.FabSize
import net.codinux.banking.ui.config.Style.SmallFabSize
import net.codinux.banking.ui.model.ShowTransferMoneyDialogData
import net.codinux.banking.ui.service.QrCodeService
private val uiState = DI.uiState
@Composable
fun FloatingActionMenu(
showFloatingActionMenu: Boolean,
menuItemClicked: () -> Unit
) {
val fabVisibilityAnimation = animateFloatAsState(targetValue = if (showFloatingActionMenu) 1f else 0f)
val bottomPadding = FabSize + FabSize / 2
val accountsThatSupportMoneyTransfer = uiState.accountsThatSupportMoneyTransfer.collectAsState().value
val coroutineScope = rememberCoroutineScope()
fun handleClick(action: () -> Unit) {
menuItemClicked()
coroutineScope.launch {
delay(50)
action()
}
}
if (fabVisibilityAnimation.value > 0) {
Box(Modifier.fillMaxSize().padding(bottom = bottomPadding, end = 12.dp), contentAlignment = Alignment.BottomEnd) {
Column(Modifier, horizontalAlignment = Alignment.End) {
FloatingActionMenuItem("Überweisungs-QR-Code erstellen", "EPC QR Code erstellen") {
handleClick {
uiState.showCreateEpcQrCodeScreen.value = true
}
}
if (QrCodeService.supportsReadingQrCodesFromCamera) {
FloatingActionMenuItem("Überweisungs-QR-Code lesen", "Neue Überweisung mit Daten aus EPC QR Code (GiroCode, scan2Code, Zahlen mit Code, ...)", enabled = accountsThatSupportMoneyTransfer.isNotEmpty()) {
handleClick {
uiState.showTransferMoneyFromEpcQrCodeScreen.value = true
}
}
}
FloatingActionMenuItem("Überweisung", "Neue Überweisung", enabled = accountsThatSupportMoneyTransfer.isNotEmpty()) {
handleClick {
uiState.showTransferMoneyDialogData.value = ShowTransferMoneyDialogData()
}
}
FloatingActionMenuItem("Konto", "Neues Konto hinzufügen") {
handleClick {
uiState.showAddAccountDialog.value = true
}
}
}
}
}
}
@Composable
fun FloatingActionMenuItem(
label: String,
contentDescription: String,
enabled: Boolean = true,
onClick: () -> Unit
) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(bottom = FabMenuSpacing).clickable(enabled) { onClick() }) {
Text(label, fontSize = 16.sp, color = contentColorFor(MaterialTheme.colors.secondary).copy(if (enabled) 1f else ContentAlpha.disabled), modifier = Modifier.padding(end = 8.dp).background(MaterialTheme.colors.secondary).padding(horizontal = 20.dp, vertical = 4.dp)) // the same background color as the FAB
FloatingActionButton(
shape = CircleShape,
modifier = Modifier.padding(end = (FabSize - SmallFabSize) / 2).size(SmallFabSize),
onClick = {
if (enabled) {
onClick()
}
}
) {
Icon(Icons.Filled.Add, contentDescription = contentDescription, tint = LocalContentColor.current.copy(if (enabled) LocalContentAlpha.current else ContentAlpha.disabled))
}
}
}

View File

@ -5,10 +5,8 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Message
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.SaveAs
import androidx.compose.material.icons.outlined.Key
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
@ -25,7 +23,7 @@ import net.codinux.banking.ui.composables.settings.UiSettings
import net.codinux.banking.ui.composables.text.ItemDivider
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.extensions.rememberVerticalScroll
import net.codinux.banking.ui.model.ShowTransferMoneyDialogData
import org.jetbrains.compose.resources.imageResource
private val uiState = DI.uiState
@ -56,11 +54,11 @@ private val VerticalSpacing = 8.dp
fun SideMenuContent() {
val drawerState = uiState.drawerState.collectAsState().value
val accounts = uiState.accounts.collectAsState().value
val accounts = uiState.banks.collectAsState().value
val coroutineScope = rememberCoroutineScope()
Column(Modifier.fillMaxSize().background(Colors.DrawerContentBackground).rememberVerticalScroll()) {
Column(Modifier.fillMaxSize().background(Colors.DrawerContentBackground).verticalScroll(ScrollState(0), enabled = true)) {
Column(Modifier.fillMaxWidth().height(HeaderHeight.dp).background(HeaderBackground).padding(16.dp)) {
Spacer(Modifier.weight(1f))
@ -68,12 +66,12 @@ fun SideMenuContent() {
Text("Bankmeister", color = Color.White, modifier = Modifier.padding(top = 16.dp, bottom = 8.dp))
Text("Version 1.0.0 Alpha 15", color = Color.LightGray)
Text("Version 1.0.0 Alpha 12", color = Color.LightGray)
}
ItemDivider(color = Colors.DrawerDivider)
Column(Modifier.padding(vertical = 24.dp).padding(start = 16.dp, end = 4.dp)) {
Column(Modifier.padding(horizontal = 16.dp, vertical = 24.dp)) {
Column(Modifier.height(ItemHeight), verticalArrangement = Arrangement.Center) {
Text("Konten", color = textColor)
}
@ -96,6 +94,19 @@ fun SideMenuContent() {
drawerState.close()
}
}
if (accounts.isNotEmpty()) {
Spacer(Modifier.height(VerticalSpacing))
NavigationMenuItem(itemModifier, "Neue Überweisung", textColor, horizontalPadding = ItemHorizontalPadding,
icon = { Icon(Icons.Filled.Add, "Neue Überweisung", Modifier.size(iconSize), tint = textColor) }) {
uiState.showTransferMoneyDialogData.value = ShowTransferMoneyDialogData()
coroutineScope.launch {
drawerState.close()
}
}
}
}
if (accounts.isNotEmpty()) {
@ -112,24 +123,6 @@ fun SideMenuContent() {
drawerState.close()
}
}
NavigationMenuItem(itemModifier, "Appzugang schützen", textColor, horizontalPadding = ItemHorizontalPadding,
icon = { Icon(Icons.Outlined.Key, "Appzugang durch Passwort oder Biometrieeingabe schützen", Modifier.size(iconSize), tint = textColor) }) {
uiState.showProtectAppSettingsScreen.value = true
coroutineScope.launch {
drawerState.close()
}
}
NavigationMenuItem(itemModifier, "Feedback", textColor, horizontalPadding = ItemHorizontalPadding,
icon = { Icon(Icons.AutoMirrored.Filled.Message, "Feedback an die Entwickler geben", Modifier.size(iconSize), tint = textColor) }) {
uiState.showFeedbackScreen.value = true
coroutineScope.launch {
drawerState.close()
}
}
}
}
}

View File

@ -13,7 +13,7 @@ import androidx.compose.ui.unit.dp
import net.codinux.banking.client.model.BankAccess
import net.codinux.banking.client.model.BankViewInfo
import net.codinux.banking.ui.config.DI
import net.codinux.banking.bankfinder.BankInfo
import net.codinux.banking.ui.model.BankInfo
import net.dankito.banking.banklistcreator.prettifier.BankingGroupMapper
private val bankIconService = DI.bankIconService
@ -31,7 +31,7 @@ private val bankingGroupMapper = BankingGroupMapper()
@Composable
fun BankIcon(bank: BankInfo, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, fallbackIcon: ImageVector? = null) {
val iconUrl by remember(bank.bic) { mutableStateOf(bankIconService.findIconForBank(bank.name, bank.bic, bankingGroupMapper.getBankingGroup(bank.name, bank.bic ?: ""))) }
val iconUrl by remember(bank.bic) { mutableStateOf(bankIconService.findIconForBank(bank.name, bank.bic, bankingGroupMapper.getBankingGroup(bank.name, bank.bic))) }
BankIcon(iconUrl, modifier, iconModifier, fallbackIcon = fallbackIcon)
}

View File

@ -10,8 +10,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import net.codinux.banking.persistence.entities.BankAccountEntity
import net.codinux.banking.persistence.entities.BankAccessEntity
import net.codinux.banking.dataaccess.entities.BankAccountEntity
import net.codinux.banking.dataaccess.entities.BankAccessEntity
import net.codinux.banking.ui.config.DI
private val uiState = DI.uiState
@ -44,7 +44,7 @@ fun BanksList(
accountSelected?.invoke(bank, null)
}
bank.accountsSorted.filterNot { it.hideAccount }.forEach { account ->
bank.accounts.sortedBy { it.displayIndex }.forEach { account ->
NavigationMenuItem(itemModifier, account.displayName, textColor, iconSize, IconTextSpacing, itemHorizontalPadding, bankAccount = account) {
accountSelected?.invoke(bank, account)
}

View File

@ -1,22 +0,0 @@
package net.codinux.banking.ui.composables
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.size
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.TextButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import net.codinux.banking.ui.config.Style
@Composable
fun CloseButton(contentDescription: String = "Dialog schließen", color: Color = Style.ListItemHeaderTextColor, size: Dp = 32.dp, onClick: () -> Unit) {
TextButton(onClick, colors = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent), contentPadding = PaddingValues(0.dp), modifier = Modifier.size(size)) {
Icon(Icons.Filled.Close, contentDescription = contentDescription, Modifier.size(size), tint = color)
}
}

View File

@ -2,13 +2,13 @@ package net.codinux.banking.ui.composables
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ContentAlpha
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@ -19,8 +19,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import net.codinux.banking.persistence.entities.BankAccountEntity
import net.codinux.banking.persistence.entities.BankAccessEntity
import net.codinux.banking.dataaccess.entities.BankAccountEntity
import net.codinux.banking.dataaccess.entities.BankAccessEntity
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI
@ -94,8 +94,6 @@ fun NavigationMenuItem(
bankAccount.balance
} else if (bank != null) {
calculator.calculateBalanceOfBankAccess(bank)
} else if (text == "Alle Konten") {
calculator.calculateBalanceOfAllAccounts(DI.uiState.accounts.value)
} else {
null
}
@ -103,20 +101,9 @@ fun NavigationMenuItem(
if (balance != null) {
Text(
formatUtil.formatAmount(balance, calculator.getTransactionsCurrency(emptyList())),
color = if (showColoredAmounts) formatUtil.getColorForAmount(balance, showColoredAmounts) else textColor,
modifier = Modifier.padding(start = 8.dp)
color = formatUtil.getColorForAmount(balance, showColoredAmounts),
modifier = Modifier.padding(start = 4.dp)
)
}
if (bank != null) {
if (bankAccount == null) {
Column(Modifier.clickable { DI.uiState.showBankSettingsScreenForBank.value = bank }.padding(start = 8.dp).size(24.dp)) {
Icon(Icons.Outlined.Settings, "Zu Kontoeinstellungen wechseln", tint = textColor, modifier = Modifier.size(24.dp))
}
}
}
if (bankAccount != null || bank == null) { // show a place holder to match Settings icon's width
Spacer(Modifier.padding(start = 8.dp).size(24.dp))
}
}
}

View File

@ -6,7 +6,7 @@ import androidx.compose.runtime.*
import kotlinx.coroutines.launch
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.dialogs.*
import net.codinux.banking.ui.screens.*
import net.codinux.banking.ui.screens.ExportScreen
import net.codinux.banking.ui.state.UiState
private val formatUtil = DI.formatUtil
@ -15,16 +15,7 @@ private val formatUtil = DI.formatUtil
fun StateHandler(uiState: UiState, snackbarHostState: SnackbarHostState) {
val showAddAccountDialog by uiState.showAddAccountDialog.collectAsState()
val showTransferMoneyDialogData by uiState.showTransferMoneyDialogData.collectAsState()
val showTransferMoneyFromEpcQrCodeScreen by uiState.showTransferMoneyFromEpcQrCodeScreen.collectAsState()
val showCreateEpcQrCodeScreen by uiState.showCreateEpcQrCodeScreen.collectAsState()
val showAccountTransactionDetailsScreenForId by uiState.showAccountTransactionDetailsScreenForId.collectAsState()
val showBankSettingsScreenForBank by uiState.showBankSettingsScreenForBank.collectAsState()
val showBankAccountSettingsScreenForAccount by uiState.showBankAccountSettingsScreenForAccount.collectAsState()
val showExportScreen by uiState.showExportScreen.collectAsState()
val showFeedbackScreen by uiState.showFeedbackScreen.collectAsState()
val showProtectAppSettingsScreen by uiState.showProtectAppSettingsScreen.collectAsState()
val tanChallengeReceived by uiState.tanChallengeReceived.collectAsState()
val bankingClientError by uiState.bankingClientErrorOccurred.collectAsState()
@ -41,42 +32,10 @@ fun StateHandler(uiState: UiState, snackbarHostState: SnackbarHostState) {
TransferMoneyDialog(data) { uiState.showTransferMoneyDialogData.value = null }
}
if (showTransferMoneyFromEpcQrCodeScreen) {
TransferMoneyFromQrCodeScreen { uiState.showTransferMoneyFromEpcQrCodeScreen.value = false }
}
if (showCreateEpcQrCodeScreen) {
CreateEpcQrCodeScreen { uiState.showCreateEpcQrCodeScreen.value = false }
}
showAccountTransactionDetailsScreenForId?.let { transactionId ->
DI.bankingService.getTransaction(transactionId)?.let { transaction ->
AccountTransactionDetailsScreen(transaction) { uiState.showAccountTransactionDetailsScreenForId.value = null }
}
}
showBankSettingsScreenForBank?.let { bank ->
BankSettingsScreen(bank) { uiState.showBankSettingsScreenForBank.value = null }
}
showBankAccountSettingsScreenForAccount?.let { account ->
BankAccountSettingsScreen(account) { uiState.showBankAccountSettingsScreenForAccount.value = null }
}
if (showExportScreen) {
ExportScreen { uiState.showExportScreen.value = false }
}
if (showFeedbackScreen) {
FeedbackScreen { uiState.showFeedbackScreen.value = false }
}
if (showProtectAppSettingsScreen) {
ProtectAppSettingsDialog(uiState.appSettings.value) { uiState.showProtectAppSettingsScreen.value = false }
}
tanChallengeReceived?.let { tanChallengeReceived ->
EnterTanDialog(tanChallengeReceived) {

View File

@ -1,24 +0,0 @@
package net.codinux.banking.ui.composables.authentification
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Fingerprint
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import net.codinux.banking.ui.model.AuthenticationResult
import net.codinux.banking.ui.service.AuthenticationService
import net.codinux.banking.ui.service.safelyAuthenticateWithBiometrics
@Composable
fun BiometricAuthenticationButton(authenticationResult: (AuthenticationResult) -> Unit) {
Row(Modifier.fillMaxWidth().padding(horizontal = 16.dp), horizontalArrangement = Arrangement.Center) {
Button({ AuthenticationService.safelyAuthenticateWithBiometrics(authenticationResult) }, enabled = AuthenticationService.supportsBiometricAuthentication) {
Icon(Icons.Outlined.Fingerprint, "Sich mittels Biometrie authentifizieren", Modifier.size(84.dp))
}
}
}

View File

@ -13,7 +13,7 @@ import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.config.Internationalization
import net.codinux.banking.ui.forms.BooleanOption
import net.codinux.banking.ui.forms.Select
import net.codinux.banking.ui.model.settings.TransactionsGrouping
import net.codinux.banking.ui.model.TransactionsGrouping
@Composable
fun UiSettings(modifier: Modifier, textColor: Color = Color.Unspecified) {

View File

@ -1,46 +0,0 @@
package net.codinux.banking.ui.composables.tan
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import net.codinux.banking.ui.service.tan.Bit
@Composable
fun ChipTanFlickerCodeStripeView(stripe: Bit, width: Dp, showTanGeneratorMarker: Boolean = false) {
Column(Modifier.width(width).fillMaxHeight()) {
val markerHeight = width * 0.5f
val triangleSize = markerHeight.value * LocalDensity.current.density
Column(Modifier.padding(bottom = 4.dp).width(width).height(markerHeight), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Bottom) {
if (showTanGeneratorMarker) {
val path = Path().apply {
// Line to the top-right corner of the triangle
lineTo(triangleSize, 0f)
// Line to the bottom-center point of the triangle
lineTo(triangleSize / 2, triangleSize)
// Close the path (line back to the starting point)
close()
}
Canvas(modifier = Modifier.size(markerHeight)) {
drawPath(path, Color.White, 1f, Fill)
}
}
}
Column(Modifier.fillMaxSize().background(if (stripe.isHigh) Color.White else Color.Black)) {
}
}
}

View File

@ -1,193 +0,0 @@
package net.codinux.banking.ui.composables.tan
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.DirectionsRun
import androidx.compose.material.icons.automirrored.filled.DirectionsWalk
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import net.codinux.banking.client.model.tan.FlickerCode
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.model.Config.NewLine
import net.codinux.banking.ui.service.tan.Bit
import net.codinux.banking.ui.service.tan.FlickerCodeAnimator
import net.codinux.banking.ui.service.tan.Step
private const val FrequencyStepSize = 2
private val DefaultStripesHeight = 240.dp
private val StripesHeightStepSize = 7.dp
private val StripesWidthStepSize = 2.dp
private val SpaceBetweenStripesStepSize = 1.dp
private val bankingService = DI.bankingService
@Composable
fun ChipTanFlickerCodeView(flickerCode: FlickerCode, textColor: Color = Color.Black) {
val animator = remember { FlickerCodeAnimator() }
val flickerCodeSettings by remember { mutableStateOf(bankingService.getImageSettingsOrCreateDefault("FlickerCode", 240)) }
val sizeFactor by remember { mutableStateOf(
if (flickerCodeSettings.height == 240) {
1
} else {
val diff = flickerCodeSettings.height.dp - DefaultStripesHeight
(diff / StripesHeightStepSize).toInt()
}
) }
var stripesHeight by remember { mutableStateOf(DefaultStripesHeight + StripesHeightStepSize.times(sizeFactor)) }
var stripesWidth by remember { mutableStateOf(45.dp + StripesWidthStepSize.times(sizeFactor)) }
var spaceBetweenStripes by remember { mutableStateOf(15.dp + SpaceBetweenStripesStepSize.times(sizeFactor)) }
var frequency by remember { mutableStateOf(flickerCodeSettings.frequency ?: FlickerCodeAnimator.DefaultFrequency) }
var isPaused by remember { mutableStateOf(false) }
var step by remember { mutableStateOf(Step(Bit.High, Bit.High, Bit.High, Bit.High, Bit.High)) }
val coroutineScope = rememberCoroutineScope()
fun setSize(width: Dp, height: Dp, spaceBetween: Dp) {
stripesWidth = width
stripesHeight = height
spaceBetweenStripes = spaceBetween
flickerCodeSettings.height = height.value.toInt()
bankingService.saveImageSettingsDebounced(flickerCodeSettings, coroutineScope)
}
fun decreaseSize() {
if (spaceBetweenStripes - SpaceBetweenStripesStepSize > 0.dp) {
setSize(
stripesWidth - StripesWidthStepSize,
stripesHeight - StripesHeightStepSize,
spaceBetweenStripes - SpaceBetweenStripesStepSize
)
}
}
fun increaseSize() { // set also an upper limit to size?
setSize(
stripesWidth + StripesWidthStepSize,
stripesHeight + StripesHeightStepSize,
spaceBetweenStripes + SpaceBetweenStripesStepSize
)
}
fun setFrequency(newFrequency: Int) {
frequency = newFrequency
animator.setFrequency(newFrequency)
flickerCodeSettings.frequency = newFrequency
bankingService.saveImageSettingsDebounced(flickerCodeSettings, coroutineScope)
}
fun decreaseFrequency() {
if (frequency - FrequencyStepSize >= FlickerCodeAnimator.MinFrequency) {
setFrequency(frequency - FrequencyStepSize)
}
}
fun increaseFrequency() {
if (frequency + FrequencyStepSize <= FlickerCodeAnimator.MaxFrequency) {
setFrequency(frequency + FrequencyStepSize)
}
}
fun toggleIsPaused() {
isPaused = !isPaused
if (isPaused) {
animator.pause()
} else {
animator.resume()
}
}
Column(Modifier.fillMaxWidth()) {
flickerCode.decodingError?.let {
Text("Hier sollte eigentlich ein FlickerCode stehen, dieser konnte jedoch nicht dekodiert werden:${NewLine}${flickerCode.decodingError}", color = MaterialTheme.colors.error, modifier = Modifier.padding(vertical = 8.dp))
}
flickerCode.parsedDataSet?.let {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
ImageSizeControls(true, true, textColor, { decreaseSize() }, { increaseSize() })
Spacer(Modifier.width(16.dp))
Text("Geschw.", color = textColor)
IconButton({ decreaseFrequency() }, enabled = frequency - FrequencyStepSize > 0) {
Icon(Icons.AutoMirrored.Filled.DirectionsWalk, contentDescription = "Frequenz verkleinern", Modifier.size(28.dp), tint = textColor)
}
IconButton({ increaseFrequency() }, enabled = frequency - FrequencyStepSize > 0) {
Icon(Icons.AutoMirrored.Filled.DirectionsRun, contentDescription = "Frequenz vergrößern", Modifier.size(28.dp), tint = textColor)
}
IconButton({ toggleIsPaused() }) {
if (isPaused) {
Icon(Icons.Filled.PlayArrow, "FlickerCode Animation wieder starten", Modifier.size(28.dp), tint = textColor)
} else {
Icon(Icons.Filled.Pause, "FlickerCode Animation pausieren", Modifier.size(28.dp), tint = textColor)
}
}
}
Row(Modifier.background(Color.Black).padding(vertical = 20.dp), verticalAlignment = Alignment.CenterVertically) {
Row(Modifier.fillMaxWidth().height(stripesHeight).background(Color.Black), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
ChipTanFlickerCodeStripeView(step.bit1, stripesWidth, true)
Spacer(Modifier.width(spaceBetweenStripes))
ChipTanFlickerCodeStripeView(step.bit2, stripesWidth)
Spacer(Modifier.width(spaceBetweenStripes))
ChipTanFlickerCodeStripeView(step.bit3, stripesWidth)
Spacer(Modifier.width(spaceBetweenStripes))
ChipTanFlickerCodeStripeView(step.bit4, stripesWidth)
Spacer(Modifier.width(spaceBetweenStripes))
ChipTanFlickerCodeStripeView(step.bit5, stripesWidth, true)
}
}
}
}
DisposableEffect(animator) {
animator.setFrequency(frequency)
animator.animateFlickerCode(flickerCode, coroutineScope) {
step = it
}
onDispose {
animator.stop()
}
}
}

View File

@ -1,28 +0,0 @@
package net.codinux.banking.ui.composables.tan
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ZoomIn
import androidx.compose.material.icons.filled.ZoomOut
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun ImageSizeControls(decreaseEnabled: Boolean, increaseEnabled: Boolean, textColor: Color = Color.Black, onDecreaseImageSize: () -> Unit, onIncreaseImageSize: () -> Unit) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Größe", color = textColor, modifier = Modifier.padding(end = 6.dp))
TextButton({ onDecreaseImageSize() }, enabled = decreaseEnabled, modifier = Modifier.width(48.dp), colors = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent)) {
Icon(Icons.Filled.ZoomOut, contentDescription = "Bild verkleinern", Modifier.size(28.dp), tint = textColor)
}
TextButton({ onIncreaseImageSize() }, enabled = increaseEnabled, modifier = Modifier.width(48.dp), colors = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent)) {
Icon(Icons.Filled.ZoomIn, contentDescription = "Bild vergrößern", Modifier.size(28.dp), tint = textColor)
}
}
}

View File

@ -1,54 +0,0 @@
package net.codinux.banking.ui.composables.tan
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.service.createImageBitmap
private val bankingService = DI.bankingService
@Composable
fun ImageView(
imageBytes: ByteArray,
imageSettingsId: String,
contentDescription: String,
initialImageHeight: Int = 300,
minImageHeight: Int = 0,
maxImageHeight: Int? = null,
changeImageSizeStep: Int = 25,
textColor: Color = Colors.MaterialThemeTextColor,
) {
val imageSettings = bankingService.getImageSettingsOrCreateDefault(imageSettingsId, initialImageHeight)
var imageHeight by remember { mutableStateOf(imageSettings.height) }
val coroutineScope = rememberCoroutineScope()
fun changeImageSize(by: Int) {
imageHeight += by
imageSettings.height = imageHeight
bankingService.saveImageSettingsDebounced(imageSettings, coroutineScope)
}
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
ImageSizeControls(imageHeight > minImageHeight, maxImageHeight == null || imageHeight < maxImageHeight, textColor, { changeImageSize(-changeImageSizeStep) }) { changeImageSize(changeImageSizeStep) }
}
Row(Modifier.fillMaxWidth().padding(top = 6.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
Image(createImageBitmap(imageBytes), contentDescription, Modifier.height(imageHeight.dp), contentScale = ContentScale.FillHeight)
}
}

View File

@ -3,15 +3,14 @@ package net.codinux.banking.ui.composables.text
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import net.codinux.banking.ui.config.Style
@Composable
fun HeaderText(title: String, modifier: Modifier = Modifier, textAlign: TextAlign = TextAlign.Start, textColor: Color = Style.HeaderTextColor) {
fun HeaderText(title: String, modifier: Modifier = Modifier, textAlign: TextAlign = TextAlign.Start) {
Text(
title,
color = textColor,
color = Style.HeaderTextColor,
fontSize = Style.HeaderFontSize,
fontWeight = Style.HeaderFontWeight,
modifier = modifier,

View File

@ -13,14 +13,14 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import net.codinux.banking.client.model.Amount
import net.codinux.banking.persistence.entities.BankAccessEntity
import net.codinux.banking.persistence.entities.HoldingEntity
import net.codinux.banking.client.model.securitiesaccount.Holding
import net.codinux.banking.dataaccess.entities.BankAccessEntity
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.config.Style
import net.codinux.banking.ui.forms.RoundedCornersCard
import net.codinux.banking.ui.model.AccountTransactionViewModel
import net.codinux.banking.ui.model.settings.TransactionsGrouping
import net.codinux.banking.ui.model.TransactionsGrouping
import net.codinux.banking.ui.service.TransactionsGroupingService
private val calculator = DI.calculator
@ -31,7 +31,7 @@ private val formatUtil = DI.formatUtil
fun GroupedTransactionsListItems(
modifier: Modifier,
transactionsToDisplay: List<AccountTransactionViewModel>,
holdingsToDisplay: List<HoldingEntity>,
holdingsToDisplay: List<Holding>,
banksById: Map<Long, BankAccessEntity>,
transactionsGrouping: TransactionsGrouping
) {
@ -65,9 +65,9 @@ fun GroupedTransactionsListItems(
RoundedCornersCard {
Column(Modifier.background(Color.White)) {
holdingsToDisplay.forEachIndexed { index, holding ->
key(holding.id) {
// key(statementOfHoldings.id) {
HoldingListItem(holding, index % 2 == 1, index < holdingsToDisplay.size - 1)
}
// }
}
}
}
@ -76,7 +76,6 @@ fun GroupedTransactionsListItems(
}
items(groupedByDate.keys.sortedDescending()) { groupingDate ->
key(groupingDate.toEpochDays()) {
Column(Modifier.fillMaxWidth()) {
Text(
text = DI.formatUtil.formatGroupingDate(groupingDate, transactionsGrouping),
@ -124,5 +123,4 @@ fun GroupedTransactionsListItems(
}
}
}
}
}

View File

@ -70,7 +70,7 @@ fun HoldingListItem(holding: Holding, isOddItem: Boolean = false, isNotLastItem:
Row(Modifier.weight(1f).padding(end = 6.dp), verticalAlignment = Alignment.CenterVertically) {
// TODO: set maxLines = 1 and TextOverflow.Ellipsis
if (holding.quantity != null) {
Text(formatUtil.formatQuantity(holding.quantity) + " Stück, ")
Text(holding.quantity.toString() + " Stück, ")
}
if (holding.averageCostPrice != null) {

View File

@ -51,11 +51,11 @@ fun TransactionListItem(bank: BankAccess?, transaction: AccountTransactionViewMo
DI.uiState.showTransferMoneyDialogData.value = ShowTransferMoneyDialogData(
DI.uiState.banks.value.firstNotNullOf { it.accounts.firstOrNull { it.id == transaction.accountId } },
transaction.otherPartyName, // we don't use userSetOtherPartyName here on purpose
transaction.otherPartyName,
transactionEntity?.otherPartyBankId,
transactionEntity?.otherPartyAccountId,
if (withSameData) transaction.amount else null,
if (withSameData) transaction.reference else null // we don't use userSetReference here on purpose
if (withSameData) transaction.reference else null
)
}
}
@ -66,7 +66,6 @@ fun TransactionListItem(bank: BankAccess?, transaction: AccountTransactionViewMo
.background(color = backgroundColor)
.pointerInput(Unit) {
detectTapGestures(
onTap = { DI.uiState.showAccountTransactionDetailsScreenForId.value = transaction.id },
onLongPress = {
if (transaction.otherPartyName != null) { // TODO: also check if IBAN is set
showMenuAt = DpOffset(it.x.dp, it.y.dp - bottomPadding)
@ -83,7 +82,7 @@ fun TransactionListItem(bank: BankAccess?, transaction: AccountTransactionViewMo
}
Text(
text = transaction.userSetOtherPartyName ?: transaction.otherPartyName ?: transaction.postingText ?: "",
text = transaction.otherPartyName ?: transaction.postingText ?: "",
Modifier.fillMaxWidth(),
color = Style.ListItemHeaderTextColor,
fontWeight = Style.ListItemHeaderWeight,
@ -95,7 +94,7 @@ fun TransactionListItem(bank: BankAccess?, transaction: AccountTransactionViewMo
Spacer(modifier = Modifier.height(6.dp))
Text(
text = transaction.userSetReference ?: transaction.reference ?: "",
text = transaction.reference ?: "",
Modifier.fillMaxWidth(),
maxLines = 1,
overflow = TextOverflow.Ellipsis
@ -121,7 +120,7 @@ fun TransactionListItem(bank: BankAccess?, transaction: AccountTransactionViewMo
offset = showMenuAt ?: DpOffset.Zero,
) {
DropdownMenuItem({ newMoneyTransferToOtherParty(false) }) {
Text("Neue Überweisung an ${transaction.userSetOtherPartyName ?: transaction.otherPartyName} ...") // really use userSetOtherPartyName here as we don't use it in ShowTransferMoneyDialogData
Text("Neue Überweisung an ${transaction.otherPartyName} ...")
}
DropdownMenuItem({ newMoneyTransferToOtherParty(true) }) {

View File

@ -12,7 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.model.settings.TransactionsGrouping
import net.codinux.banking.ui.model.TransactionsGrouping
import net.codinux.banking.ui.settings.UiSettings
import net.codinux.banking.ui.state.UiState
import org.jetbrains.compose.ui.tooling.preview.Preview
@ -69,9 +69,9 @@ fun TransactionsList(uiState: UiState, uiSettings: UiSettings, isMobile: Boolean
} else {
LazyColumn(transactionsListModifier, contentPadding = PaddingValues(top = 8.dp, bottom = 16.dp)) {
itemsIndexed(holdingsToDisplay) { index, holding ->
key(holding.id) {
// key(holding.isin) {
HoldingListItem(holding, index % 2 == 1, index < holdingsToDisplay.size - 1)
}
// }
}
itemsIndexed(transactionsToDisplay) { index, transaction ->

View File

@ -22,9 +22,6 @@ object Colors {
val BackgroundColorLight = Color("#FFFFFF")
val MaterialThemeTextColor = Color(0xFF4F4F4F) // to match dialog's text color of Material theme
val DrawerContentBackground = BackgroundColorDark
val DrawerPrimaryText = PrimaryTextColorDark
@ -37,16 +34,6 @@ object Colors {
val CodinuxSecondaryColor = Color(251, 187, 33)
val FormLabelTextColor = Color(0xFF494949)
val FormValueTextColor = Color(0xFF999999)
val FormListItemTextColor = FormLabelTextColor
val DestructiveColor = Color(0xFFff3b30)
val Zinc100 = Color(244, 244, 245)
val Zinc100_50 = Zinc100.copy(alpha = 0.5f)

View File

@ -1,7 +1,9 @@
package net.codinux.banking.ui.config
import net.codinux.banking.persistence.BankingRepository
import net.codinux.banking.persistence.InMemoryBankingRepository
import app.cash.sqldelight.db.SqlDriver
import net.codinux.banking.dataaccess.BankingRepository
import net.codinux.banking.dataaccess.InMemoryBankingRepository
import net.codinux.banking.dataaccess.SqliteBankingRepository
import net.codinux.banking.ui.Platform
import net.codinux.banking.ui.getPlatform
import net.codinux.banking.ui.service.*
@ -29,22 +31,18 @@ object DI {
val accountTransactionsFilterService = AccountTransactionsFilterService()
val epcQrCodeService = EpcQrCodeService()
val uiService = UiService()
var bankingRepository: BankingRepository = InMemoryBankingRepository(emptyList())
val bankingService by lazy { BankingService(uiState, uiSettings, uiService, bankingRepository, bankFinder) }
val bankingService by lazy { BankingService(uiState, uiSettings, bankingRepository, bankFinder) }
fun setRepository(sqlDriver: SqlDriver) = setRepository(SqliteBankingRepository(sqlDriver))
fun setRepository(repository: BankingRepository) {
this.bankingRepository = repository
repository.getAppSettings()?.let { // otherwise it's the first app start, BankingService will take care of this case
uiState.appSettings.value = it
}
}

View File

@ -1,9 +1,7 @@
package net.codinux.banking.ui.config
import net.codinux.banking.client.model.BankAccountType
import net.codinux.banking.client.model.tan.ActionRequiringTan
import net.codinux.banking.ui.model.settings.TransactionsGrouping
import net.codinux.banking.ui.model.settings.AppAuthenticationMethod
import net.codinux.banking.ui.model.TransactionsGrouping
object Internationalization {
@ -13,12 +11,6 @@ object Internationalization {
const val ErrorTransferMoney = "Überweisung konnte nicht ausgeführt werden"
const val ErrorReadEpcQrCode = "Überweisungsdaten konnten nicht aus dem QR Code ausgelesen werden"
const val ErrorSaveToDatabase = "Daten konnten nicht in der Datenbank gespeichert werden"
const val ErrorBiometricAuthentication = "Biometrische Authentifizierung fehlgeschlagen"
fun getTextForActionRequiringTan(action: ActionRequiringTan): String = when (action) {
ActionRequiringTan.GetAnonymousBankInfo,
@ -38,23 +30,4 @@ object Internationalization {
TransactionsGrouping.None -> "Nicht gruppieren"
}
fun translate(accountType: BankAccountType): String = when (accountType) {
BankAccountType.CheckingAccount -> "Girokonto"
BankAccountType.SavingsAccount -> "Sparkonto"
BankAccountType.FixedTermDepositAccount -> "Festgeldkonto"
BankAccountType.SecuritiesAccount -> "Wertpapierdepot"
BankAccountType.LoanAccount -> "Darlehenskonto"
BankAccountType.CreditCardAccount -> "Kreditkartenkonto"
BankAccountType.FundDeposit -> "Fondsdepot"
BankAccountType.BuildingLoanContract -> "Bausparvertrag"
BankAccountType.InsuranceContract -> "Versicherungsvertrag"
BankAccountType.Other -> "Sonstige"
}
fun translate(authenticationMethod: AppAuthenticationMethod): String = when (authenticationMethod) {
AppAuthenticationMethod.None -> "Ungeschützt"
AppAuthenticationMethod.Password -> "Passwort"
AppAuthenticationMethod.Biometric -> "Biometrie"
}
}

View File

@ -19,15 +19,6 @@ object Style {
val ListItemHeaderWeight = FontWeight.Medium // couldn't believe it, the FontWeights look different on Desktop and Android
val FabSize = 56.dp
val SmallFabSize = 46.dp
val FabSpacing = 16.dp
val FabMenuSpacing = FabSpacing / 2
val DividerThickness = 1.dp
}

View File

@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.*
@ -19,11 +20,9 @@ import net.codinux.banking.ui.IOorDefault
import net.codinux.banking.ui.composables.BankIcon
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.extensions.ImeNext
import net.codinux.banking.ui.forms.*
import net.codinux.banking.ui.forms.OutlinedTextField
import net.codinux.banking.bankfinder.BankInfo
import net.codinux.log.Log
import net.codinux.banking.ui.model.BankInfo
private val bankingService = DI.bankingService
@ -38,7 +37,7 @@ fun AddAccountDialog(
var selectedBank by remember { mutableStateOf<BankInfo?>(null) }
var loginName by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var retrieveAllTransactions by remember { mutableStateOf(true) }
var retrieveAllTransactions by remember { mutableStateOf(false) }
val isRequiredDataEntered by remember(selectedBank, loginName, password) {
derivedStateOf { selectedBank != null && loginName.length > 3 && password.length > 3 }
@ -70,12 +69,7 @@ fun AddAccountDialog(
isAddingAccount = true
addAccountJob = coroutineScope.launch(Dispatchers.IOorDefault) {
val successful = try {
DI.bankingService.addAccount(bank, loginName, password, retrieveAllTransactions)
} catch (e: Throwable) {
Log.error(e) { "Could not add account for $bank" }
false
}
val successful = DI.bankingService.addAccount(bank, loginName, password, retrieveAllTransactions)
addAccountJob = null
@ -142,11 +136,9 @@ fun AddAccountDialog(
}
Row(Modifier.fillMaxWidth().padding(top = 6.dp)) {
Text(bank.bankCode, color = textColor)
Text(bank.domesticBankCode, color = textColor)
Text((bank.bic ?: "").padEnd(11, ' '), color = textColor, modifier = Modifier.padding(horizontal = 8.dp))
Text("${bank.postalCode} ${bank.city}", Modifier.weight(1f), color = if (supportsFinTs) Color.Gray else textColor)
Text("${bank.postalCode} ${bank.city}", Modifier.weight(1f).padding(start = 8.dp), color = if (supportsFinTs) Color.Gray else textColor)
}
}
}
@ -163,7 +155,7 @@ fun AddAccountDialog(
onValueChange = { loginName = it },
label = { Text("Login Name") },
modifier = Modifier.fillMaxWidth().focusRequester(loginNameFocus),
keyboardOptions = KeyboardOptions.ImeNext
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next)
)
Spacer(modifier = Modifier.height(12.dp))

View File

@ -11,10 +11,9 @@ fun ApplicationErrorDialog(error: ApplicationError, onDismiss: (() -> Unit)? = n
ErroneousAction.AddAccount -> Internationalization.ErrorAddAccount
ErroneousAction.UpdateAccountTransactions -> Internationalization.ErrorUpdateAccountTransactions
ErroneousAction.TransferMoney -> Internationalization.ErrorTransferMoney
ErroneousAction.ReadEpcQrCode -> Internationalization.ErrorReadEpcQrCode
ErroneousAction.SaveToDatabase -> Internationalization.ErrorSaveToDatabase
ErroneousAction.BiometricAuthentication -> Internationalization.ErrorBiometricAuthentication
}
ErrorDialog(error.errorMessage, title, error.exception, onDismiss = onDismiss)
// add exception stacktrace?
ErrorDialog(error.errorMessage, title, onDismiss = onDismiss)
}

View File

@ -3,12 +3,9 @@ package net.codinux.banking.ui.dialogs
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -16,25 +13,17 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import net.codinux.banking.ui.PlatformType
import net.codinux.banking.ui.addKeyboardVisibilityListener
import net.codinux.banking.ui.composables.CloseButton
import net.codinux.banking.ui.composables.text.HeaderText
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.config.Style
import net.codinux.banking.ui.extensions.applyPlatformSpecificPaddingIf
import net.codinux.banking.ui.extensions.copy
import net.codinux.banking.ui.extensions.verticalScroll
import net.codinux.banking.ui.forms.*
@Composable
fun BaseDialog(
title: String,
centerTitle: Boolean = false,
confirmButtonTitle: String = "OK",
confirmButtonEnabled: Boolean = true,
dismissButtonTitle: String = "Abbrechen",
showProgressIndicatorOnConfirmButton: Boolean = false,
useMoreThanPlatformDefaultWidthOnMobile: Boolean = false,
onDismiss: () -> Unit,
@ -44,26 +33,25 @@ fun BaseDialog(
) {
val overwriteDefaultWidth = useMoreThanPlatformDefaultWidthOnMobile && DI.platform.isMobile
var isKeyboardVisible by remember { mutableStateOf(false) }
Dialog(onDismissRequest = onDismiss, if (overwriteDefaultWidth) properties.copy(usePlatformDefaultWidth = false) else properties) {
RoundedCornersCard(Modifier.let { if (overwriteDefaultWidth) it.fillMaxWidth(0.95f) else it }) {
Column(Modifier.applyPlatformSpecificPaddingIf(overwriteDefaultWidth && isKeyboardVisible, 8.dp).background(Color.White).padding(horizontal = 8.dp).verticalScroll()) {
Column(Modifier.background(Color.White).padding(8.dp)) {
Row(Modifier.fillMaxWidth().padding(bottom = 8.dp).height(32.dp), verticalAlignment = Alignment.CenterVertically) {
HeaderText(title, Modifier.fillMaxWidth().weight(1f), textColor = Style.ListItemHeaderTextColor, textAlign = if (centerTitle) TextAlign.Center else TextAlign.Start)
Row(Modifier.fillMaxWidth()) {
HeaderText(title, Modifier.padding(top = 8.dp, bottom = 16.dp).weight(1f))
if (DI.platform.type != PlatformType.Android) { // for iOS it's also relevant due to the missing back gesture / back button
CloseButton(onClick = onDismiss)
if (DI.platform.isDesktop) {
TextButton(onDismiss, colors = ButtonDefaults.buttonColors(contentColor = Colors.Zinc700, backgroundColor = Color.Transparent)) {
Icon(Icons.Filled.Close, contentDescription = "Close dialog", Modifier.size(32.dp))
}
}
}
content()
Row(Modifier.fillMaxWidth().padding(top = 8.dp)) {
Row(Modifier.fillMaxWidth()) {
TextButton(onClick = onDismiss, Modifier.weight(0.5f)) {
Text(dismissButtonTitle, color = Colors.CodinuxSecondaryColor, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth())
Text("Abbrechen", color = Colors.CodinuxSecondaryColor, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth())
}
TextButton(
@ -73,7 +61,7 @@ fun BaseDialog(
) {
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
if (showProgressIndicatorOnConfirmButton) {
CircularProgressIndicator(Modifier.padding(end = 6.dp).size(36.dp), color = Colors.CodinuxSecondaryColor)
CircularProgressIndicator(Modifier.padding(end = 6.dp), color = Colors.CodinuxSecondaryColor)
}
Text(confirmButtonTitle, color = Colors.CodinuxSecondaryColor, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth())
@ -83,13 +71,4 @@ fun BaseDialog(
}
}
}
LaunchedEffect(Unit) {
if (DI.platform.type == PlatformType.iOS) { // on iOS top dialog part gets hidden by top system bar when soft keyboard is visible -> apply system padding then
addKeyboardVisibilityListener { visible ->
isKeyboardVisible = visible
}
}
}
}

View File

@ -1,32 +0,0 @@
package net.codinux.banking.ui.dialogs
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun ConfirmDialog(
text: String,
title: String? = null,
confirmButtonTitle: String = "Ja",
dismissButtonTitle: String = "Nein",
onDismiss: () -> Unit,
onConfirm: () -> Unit
) {
BaseDialog(
title = title ?: "",
centerTitle = true,
confirmButtonTitle = confirmButtonTitle,
dismissButtonTitle = dismissButtonTitle,
onDismiss = { onDismiss() },
onConfirm = { onConfirm(); onDismiss() }
) {
Text(text, textAlign = TextAlign.Center, lineHeight = 22.sp, modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp))
}
}

View File

@ -1,34 +1,33 @@
package net.codinux.banking.ui.dialogs
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ZoomIn
import androidx.compose.material.icons.filled.ZoomOut
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import net.codinux.banking.client.model.tan.ActionRequiringTan
import net.codinux.banking.client.model.tan.AllowedTanFormat
import net.codinux.banking.client.model.tan.EnterTanResult
import net.codinux.banking.client.model.tan.*
import net.codinux.banking.ui.composables.BankIcon
import net.codinux.banking.ui.composables.tan.ChipTanFlickerCodeView
import net.codinux.banking.ui.composables.tan.ImageView
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.config.Internationalization
import net.codinux.banking.ui.forms.CaptionText
import net.codinux.banking.ui.forms.OutlinedTextField
import net.codinux.banking.ui.forms.Select
import net.codinux.banking.ui.model.Config.NewLine
import net.codinux.banking.ui.model.TanChallengeReceived
import net.codinux.banking.ui.model.error.ErroneousAction
import net.codinux.banking.ui.service.createImageBitmap
import net.codinux.log.Log
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
@ -42,7 +41,9 @@ fun EnterTanDialog(tanChallengeReceived: TanChallengeReceived, onDismiss: () ->
val isNotADecoupledTanMethod = !!!isDecoupledMethod
var showSelectingTanMediumNotImplementedWarning by remember { mutableStateOf(false) }
var tanImageHeight by remember { mutableStateOf(250) }
val minTanImageHeight = 100
val maxTanImageHeight = 500
val textFieldFocus = remember { FocusRequester() }
@ -97,10 +98,10 @@ fun EnterTanDialog(tanChallengeReceived: TanChallengeReceived, onDismiss: () ->
Text("${challenge.bank.bankName}, Nutzer ${challenge.bank.loginName}${challenge.account?.let { ", Konto ${it.productName ?: it.identifier}" } ?: ""}")
}
Row(Modifier.padding(top = 6.dp), verticalAlignment = Alignment.CenterVertically) {
Text("TAN benötigt ")
Text(Internationalization.getTextForActionRequiringTan(challenge.forAction), fontWeight = FontWeight.Bold)
}
Text(
"TAN benötigt ${Internationalization.getTextForActionRequiringTan(challenge.forAction)}",
Modifier.padding(top = 6.dp)
)
}
@ -109,9 +110,19 @@ fun EnterTanDialog(tanChallengeReceived: TanChallengeReceived, onDismiss: () ->
"TAN Verfahren",
challenge.availableTanMethods.sortedBy { it.identifier },
challenge.selectedTanMethod,
{ tanMethod -> tanChallengeReceived.callback(EnterTanResult(null, tanMethod)) },
{ tanMethod ->
if (tanMethod.type != TanMethodType.ChipTanFlickercode) {
tanChallengeReceived.callback(EnterTanResult(null, tanMethod))
}
},
{ it.displayName }
) { tanMethod -> Text(tanMethod.displayName) }
) { tanMethod ->
if (tanMethod.type == TanMethodType.ChipTanFlickercode) {
Text(tanMethod.displayName + " (noch nicht implementiert)", color = MaterialTheme.colors.onSurface.copy(ContentAlpha.disabled))
} else {
Text(tanMethod.displayName)
}
}
}
if (challenge.availableTanMedia.isNotEmpty()) {
@ -120,35 +131,39 @@ fun EnterTanDialog(tanChallengeReceived: TanChallengeReceived, onDismiss: () ->
"TAN Medium",
challenge.availableTanMedia.sortedBy { it.status }.map { it.displayName },
challenge.selectedTanMedium?.displayName ?: "<Keines ausgewählt>",
{ showSelectingTanMediumNotImplementedWarning = true }, // TODO: change TanMedium
{ Log.info { "User selected TanMedium $it" } }, // TODO: change TanMethod
{ it }
)
}
if (showSelectingTanMediumNotImplementedWarning) {
CaptionText("Es tut uns Leid, aber das Ändern des TAN Mediums ist gegenwärtig noch nicht implementiert", Colors.DestructiveColor, Arrangement.Start)
}
}
if (challenge.tanImage != null || challenge.flickerCode != null) {
Column(Modifier.fillMaxWidth().padding(top = 6.dp)) {
val textColor = Colors.MaterialThemeTextColor // to match dialog's text color of Material theme
challenge.flickerCode?.let { flickerCode ->
ChipTanFlickerCodeView(flickerCode, textColor)
if (challenge.flickerCode != null) {
Text("Es tut uns Leid, für die TAN müsste ein Flickercode angezeigt werden, was wir noch nicht implementiert haben.")
Text("Bitte wählen Sie ein anderes TAN Verfahren, z. B. chipTAN-QrCode oder manuelle TAN Eingabe wie chipTAN manuell.", Modifier.padding(top = 6.dp))
}
challenge.tanImage?.let { tanImage ->
tanImage.decodingError?.let {
Text("Hier sollte eigentlich das TAN Bild angezeigt werden, dieses konnte jedoch nicht dekodiert werden:$NewLine${tanImage.decodingError}", color = MaterialTheme.colors.error, modifier = Modifier.padding(vertical = 8.dp))
if (tanImage.decodingSuccessful) {
val imageBytes = Base64.decode(tanImage.imageBytesBase64)
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
Text("Größe")
Spacer(Modifier.width(6.dp))
TextButton({ tanImageHeight -= 25}, enabled = tanImageHeight > minTanImageHeight, modifier = Modifier.width(48.dp), colors = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent)) {
Icon(Icons.Filled.ZoomOut, contentDescription = "Bild mit enkodierter TAN verkleiner", Modifier.size(28.dp))
}
Spacer(Modifier.width(6.dp))
TextButton({ tanImageHeight += 25}, enabled = tanImageHeight < maxTanImageHeight, modifier = Modifier.width(48.dp), colors = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent)) {
Icon(Icons.Filled.ZoomIn, contentDescription = "Bild mit enkodierter TAN vergrößern", Modifier.size(28.dp))
}
}
tanImage.imageBytesBase64?.let { imageBytesBase64 ->
val imageBytes = Base64.decode(imageBytesBase64)
// if it becomes necessary may also add the bank to ImageSettings.id to make ImageSettings bank specific
ImageView(imageBytes, challenge.selectedTanMethod.type.toString(), "Bild mit enkodierter TAN", 250, 100, 500, textColor = textColor)
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
Image(createImageBitmap(imageBytes), "Bild mit enkodierter TAN", Modifier.height(tanImageHeight.dp), contentScale = ContentScale.FillHeight)
}
}
}
}

View File

@ -7,32 +7,23 @@ import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.window.DialogProperties
import net.codinux.banking.ui.composables.text.HeaderText
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.extensions.verticalScroll
import net.codinux.banking.ui.model.Config.NewLine
import net.codinux.banking.ui.config.Style
@Composable
fun ErrorDialog(
text: String,
title: String? = null,
exception: Throwable? = null,
confirmButtonText: String = "OK",
onDismiss: (() -> Unit)? = null
) {
val effectiveText = if (exception == null) text else {
"$text${NewLine}${NewLine}Fehlermeldung:${NewLine}${exception.stackTraceToString()}"
}
AlertDialog(
text = { Text(effectiveText, Modifier.verticalScroll()) },
text = { Text(text) },
title = { title?.let {
HeaderText(title, Modifier.fillMaxWidth(), TextAlign.Center)
} },
properties = if (exception == null) DialogProperties() else DialogProperties(usePlatformDefaultWidth = false),
onDismissRequest = { onDismiss?.invoke() },
confirmButton = {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {

View File

@ -20,9 +20,7 @@ import net.codinux.banking.ui.IOorDefault
import net.codinux.banking.ui.composables.BankIcon
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.extensions.ImeNext
import net.codinux.banking.ui.forms.AutocompleteTextField
import net.codinux.banking.ui.forms.CaptionText
import net.codinux.banking.ui.forms.OutlinedTextField
import net.codinux.banking.ui.forms.Select
import net.codinux.banking.ui.model.ShowTransferMoneyDialogData
@ -42,9 +40,10 @@ fun TransferMoneyDialog(
) {
val banks = uiState.banks.value
val accountsToBank = banks.sortedBy { it.displayIndex }
.flatMap { bank -> bank.accountsSorted.map { it to bank } }.toMap()
.flatMap { bank -> bank.accounts.sortedBy { it.displayIndex }.map { it to bank } }.toMap()
val accountsSupportingTransferringMoney = uiState.accountsThatSupportMoneyTransfer.collectAsState().value
val accountsSupportingTransferringMoney = banks.flatMap { it.accounts }
.filter { it.supportsMoneyTransfer }
if (accountsSupportingTransferringMoney.isEmpty()) {
uiState.applicationErrorOccurred(ErroneousAction.TransferMoney, "Keines Ihrer Konten unterstützt das Überweisen von Geld")
@ -57,7 +56,7 @@ fun TransferMoneyDialog(
var recipientName by remember { mutableStateOf(data.recipientName ?: "") }
var recipientAccountIdentifier by remember { mutableStateOf(data.recipientAccountIdentifier ?: "") }
var amount by remember { mutableStateOf(data.amount?.toString() ?: "") }
var amount by remember { mutableStateOf(data.amount.toString()) }
var paymentReference by remember { mutableStateOf(data.reference ?: "") }
val accountSupportsInstantTransfer by remember(senderAccount) { derivedStateOf { senderAccount.supportsInstantTransfer } }
var instantTransfer by remember { mutableStateOf(false) }
@ -77,8 +76,6 @@ fun TransferMoneyDialog(
val amountFocus = remember { FocusRequester() }
val referenceFocus = remember { FocusRequester() }
val verticalSpace = 8.dp
var isInitialized by remember { mutableStateOf(false) }
@ -123,7 +120,7 @@ fun TransferMoneyDialog(
BaseDialog(
title = "Neue Überweisung",
title = "Neue Überweisung ...",
confirmButtonTitle = "Überweisen",
confirmButtonEnabled = isRequiredDataEntered && isTransferringMoney == false,
showProgressIndicatorOnConfirmButton = isTransferringMoney,
@ -187,8 +184,6 @@ fun TransferMoneyDialog(
}
}
CaptionText("${recipientName.length} / 70")
Spacer(modifier = Modifier.height(verticalSpace))
OutlinedTextField(
@ -196,7 +191,7 @@ fun TransferMoneyDialog(
onValueChange = { recipientAccountIdentifier = it },
label = { Text("IBAN") },
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions.ImeNext
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next)
)
Row(Modifier.padding(vertical = verticalSpace).fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
@ -210,13 +205,12 @@ fun TransferMoneyDialog(
getItemTitle = { suggestion -> suggestion.amount.toString() },
onEnteredTextChanged = { amount = it },
onSelectedItemChanged = {
amount = it?.amount.toString()
if (it != null) {
amount = it.amount.toString()
paymentReference = it.reference
referenceFocus.requestFocus()
}
},
fetchSuggestions = { query -> recipientFinder.findAmountPaymentDataForIban(recipientAccountIdentifier, query) }
fetchSuggestions = { recipientFinder.findPaymentDataForIban(recipientAccountIdentifier) }
) { paymentDataSuggestion ->
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Text(formatUtil.formatAmount(paymentDataSuggestion.amount, paymentDataSuggestion.currency), Modifier.widthIn(min = 60.dp), textAlign = TextAlign.End)
@ -231,13 +225,12 @@ fun TransferMoneyDialog(
AutocompleteTextField(
"Verwendungszweck (optional)",
paymentReference,
dropdownMaxHeight = 175.dp, // when showing more items than on Android autocomplete dropdown covers soft keyboard
minTextLengthForSearch = 1,
modifier = Modifier.focusRequester(referenceFocus),
dropdownMaxHeight = 250.dp,
minTextLengthForSearch = 0,
getItemTitle = { suggestion -> suggestion.reference },
onEnteredTextChanged = { paymentReference = it },
onSelectedItemChanged = { paymentReference = it?.reference ?: "" },
fetchSuggestions = { query -> recipientFinder.findReferencePaymentDataForIban(recipientAccountIdentifier, query) }
fetchSuggestions = { recipientFinder.findPaymentDataForIban(recipientAccountIdentifier) }
) { paymentDataSuggestion ->
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Text(formatUtil.formatAmount(paymentDataSuggestion.amount, paymentDataSuggestion.currency), Modifier.widthIn(min = 60.dp), textAlign = TextAlign.End)
@ -246,7 +239,12 @@ fun TransferMoneyDialog(
}
}
CaptionText("${paymentReference.length} / 140")
Row(Modifier.fillMaxWidth().padding(top = 4.dp), horizontalArrangement = Arrangement.End) {
Text(
text = "${paymentReference.length} / 140",
style = MaterialTheme.typography.caption
)
}
Row(Modifier.padding(top = verticalSpace), verticalAlignment = Alignment.CenterVertically) {
@ -275,13 +273,7 @@ fun TransferMoneyDialog(
coroutineScope.launch {
recipientFinder.updateData(bankingService.getAllAccountTransactions()) // only a bit problematic: if in the meantime new transactions are retrieved, then RecipientFinder doesn't contain the newly retrieved transactions
if (data.recipientName.isNullOrBlank()) {
recipientNameFocus.requestFocus()
} else if (data.amount == null) {
amountFocus.requestFocus()
} else {
referenceFocus.requestFocus()
}
}
}
}

View File

@ -1,11 +0,0 @@
package net.codinux.banking.ui.extensions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
val KeyboardOptions.Companion.ImeNext: KeyboardOptions
get() = KeyboardOptions(imeAction = ImeAction.Next)
val KeyboardOptions.Companion.ImeDone: KeyboardOptions
get() = KeyboardOptions(imeAction = ImeAction.Done)

View File

@ -1,49 +0,0 @@
package net.codinux.banking.ui.extensions
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import net.codinux.banking.ui.systemPaddings
import net.codinux.log.Log
fun Modifier.verticalScroll() = this.verticalScroll(ScrollState(0), enabled = true)
@Composable
fun Modifier.rememberVerticalScroll() = this.verticalScroll(rememberScrollState())
fun Modifier.horizontalScroll() = this.horizontalScroll(ScrollState(0), enabled = true)
@Composable
fun Modifier.rememberHorizontalScroll() = this.horizontalScroll(rememberScrollState())
@Composable
// we need to support three different cases:
// - normal, non fullscreen dialog, either useMoreThanPlatformDefaultWidthOnMobile is false or soft keyboard is hidden -> apply default vertical padding
// - normal, non fullscreen dialog, useMoreThanPlatformDefaultWidthOnMobile is true and soft keyboard is visible = applyPlatformPadding == true -> on iOS apply platform padding as
// otherwise dialog title gets hidden by upper system bar, on all other platforms default vertical padding
// - fullscreen dialog -> on iOS apply platform padding as otherwise dialog title gets hidden by upper system bar, on all other platforms default vertical padding
fun Modifier.applyPlatformSpecificPaddingIf(applyPlatformPadding: Boolean, minVerticalPadding: Dp = 0.dp): Modifier =
if (applyPlatformPadding) {
this.applyPlatformSpecificPadding(minVerticalPadding)
} else if (minVerticalPadding > 0.dp) {
this.padding(vertical = minVerticalPadding)
} else {
this
}
@Composable
fun Modifier.applyPlatformSpecificPadding(minVerticalPadding: Dp = 0.dp): Modifier {
val systemPaddings = systemPaddings()
return this.padding(
top = maxOf(minVerticalPadding, systemPaddings.calculateTopPadding()),
bottom = maxOf(minVerticalPadding, systemPaddings.calculateBottomPadding())
).also {
Log.info { "Applied padding: ${systemPaddings.calculateTopPadding()}, ${systemPaddings.calculateBottomPadding()}" }
}
}

View File

@ -30,7 +30,6 @@ fun <T> AutocompleteTextField(
modifier: Modifier = Modifier,
textFieldFocus: FocusRequester = remember { FocusRequester() },
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
onEnterPressed: (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
fetchSuggestions: suspend (query: String) -> Collection<T> = { emptyList() },
suggestionContent: @Composable (T) -> Unit
@ -103,8 +102,7 @@ fun <T> AutocompleteTextField(
)
}
},
leadingIcon = leadingIcon,
onEnterPressed = onEnterPressed
leadingIcon = leadingIcon
)
// due to a bug (still not fixed since 2021) in ExposedDropdownMenu its popup has a maximum width of 800 pixel / 320dp which is too less to fit

View File

@ -1,23 +0,0 @@
package net.codinux.banking.ui.forms
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun CaptionText(text: String, color: Color = Color.Unspecified, horizontalArrangement: Arrangement.Horizontal = Arrangement.End) {
Row(Modifier.fillMaxWidth().padding(top = 4.dp), horizontalArrangement = horizontalArrangement) {
Text(
text = text,
style = MaterialTheme.typography.caption,
color = color
)
}
}

View File

@ -1,56 +0,0 @@
package net.codinux.banking.ui.forms
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Check
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import net.codinux.banking.ui.config.Colors
@Composable
fun FormListItem(label: String, itemHeight: Dp = 32.dp, onClick: (() -> Unit)? = null) {
FormListItemImpl(label, itemHeight = itemHeight, onClick = onClick)
}
@Composable
fun SelectableFormListItem(
label: String,
isSelected: Boolean = false,
selectedIconContentDescription: String? = null,
itemHeight: Dp = 32.dp,
onClick: (() -> Unit)? = null
) {
FormListItemImpl(label, true, isSelected, selectedIconContentDescription, itemHeight, onClick)
}
@Composable
private fun FormListItemImpl(
label: String,
isSelectable: Boolean = false,
isSelected: Boolean = false,
selectedIconContentDescription: String? = null,
itemHeight: Dp = 32.dp,
onClick: (() -> Unit)? = null
) {
Row(Modifier.fillMaxWidth().height(itemHeight).clickable { onClick?.invoke() }.padding(4.dp), verticalAlignment = Alignment.CenterVertically) {
if (isSelectable) {
Column(Modifier.padding(end = 8.dp).size(24.dp)) {
if (isSelected) {
Icon(Icons.Outlined.Check, selectedIconContentDescription ?: "Item ist ausgewählt", tint = Colors.FormListItemTextColor)
}
}
}
Text(label, color = Colors.FormListItemTextColor, fontSize = 16.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}

View File

@ -1,38 +0,0 @@
package net.codinux.banking.ui.forms
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import net.codinux.banking.ui.config.Colors
@Composable
fun LabelledValue(label: String, value: String?, valueTextColor: Color? = null, labelMaxLines: Int = 1) {
if (value != null) {
Column(Modifier.fillMaxWidth()) {
Text(
text = label,
modifier = Modifier.padding(top = 12.dp, bottom = 2.dp),
fontSize = 15.sp,
color = Colors.FormLabelTextColor,
maxLines = labelMaxLines,
overflow = TextOverflow.Ellipsis
)
Text(
text = value,
modifier = Modifier.padding(bottom = 4.dp),
fontSize = 15.sp,
color = valueTextColor ?: Colors.FormValueTextColor
)
}
}
}

View File

@ -17,16 +17,7 @@ import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
@Composable // try BasicSecureTextField
fun PasswordTextField(
password: String = "",
label: String = "Passwort",
modifier: Modifier = Modifier,
keyboardOptions: KeyboardOptions? = null,
isError: Boolean = false,
forceHidePassword: Boolean? = null,
onEnterPressed: (() -> Unit)? = null,
onChange: (String) -> Unit
) {
fun PasswordTextField(password: String = "", label: String = "Passwort", forceHidePassword: Boolean? = null, onEnterPressed: (() -> Unit)? = null, onChange: (String) -> Unit) {
var passwordVisible by remember { mutableStateOf(false) }
@ -38,8 +29,7 @@ fun PasswordTextField(
value = password,
onValueChange = { onChange(it) },
label = { Text(label) },
modifier = modifier.fillMaxWidth(),
isError = isError,
modifier = Modifier.fillMaxWidth(),
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
trailingIcon = {
val visibilityIcon = if (passwordVisible) {
@ -53,7 +43,7 @@ fun PasswordTextField(
modifier = Modifier.size(24.dp).clickable { passwordVisible = !passwordVisible }
)
},
keyboardOptions = keyboardOptions?.copy(keyboardType = KeyboardType.Password) ?: KeyboardOptions(keyboardType = KeyboardType.Password),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
onEnterPressed = onEnterPressed
)
}

View File

@ -1,28 +0,0 @@
package net.codinux.banking.ui.forms
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import net.codinux.banking.ui.config.Colors
@Composable
fun SectionHeader(title: String, topPadding: Boolean = true) {
Text(
text = title,
modifier = Modifier.fillMaxWidth().let {
if (topPadding) {
it.padding(top = 24.dp)
} else {
it
}
},
color = Colors.CodinuxSecondaryColor,
fontSize = 16.sp
)
}

View File

@ -1,68 +0,0 @@
package net.codinux.banking.ui.forms
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Divider
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import net.codinux.banking.ui.config.Colors
@Composable
fun <T> SegmentedControl(
options: Collection<T>,
selectedOption: T,
modifier: Modifier = Modifier,
color: Color = Colors.Accent,
cornerSize: Dp = 8.dp,
getOptionDisplayText: ((T) -> String)? = null,
onOptionSelected: (T) -> Unit
) {
Row(horizontalArrangement = Arrangement.Center) {
Row(modifier.height(48.dp).border(2.dp, color, RoundedCornerShape(cornerSize))) {
options.forEachIndexed { index, option ->
val isSelected = option == selectedOption
val backgroundColor = if (isSelected) color else Color.Transparent
val textColor = if (isSelected) Color.White else color
Box(
modifier = Modifier
.clickable { onOptionSelected(option) }
.fillMaxHeight()
.weight(1f)
.let {
if (index == 0) {
it.background(backgroundColor, RoundedCornerShape(topStart = cornerSize, bottomStart = cornerSize))
} else if (index == options.size - 1) {
it.background(backgroundColor, RoundedCornerShape(topEnd = cornerSize, bottomEnd = cornerSize))
} else {
it.background(backgroundColor)
}
}
.padding(vertical = 8.dp),
contentAlignment = Alignment.Center
) {
Text(
text = getOptionDisplayText?.invoke(option) ?: option.toString(),
color = textColor,
textAlign = TextAlign.Center
)
}
if (index < options.size - 1) {
Divider(Modifier.fillMaxHeight().width(1.dp), color = color)
}
}
}
}
}

View File

@ -3,7 +3,7 @@ package net.codinux.banking.ui.model
import kotlinx.datetime.LocalDate
import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.client.model.Amount
import net.codinux.banking.persistence.entities.AccountTransactionEntity
import net.codinux.banking.dataaccess.entities.AccountTransactionEntity
data class AccountTransactionViewModel(
val id: Long,
@ -17,8 +17,8 @@ data class AccountTransactionViewModel(
val otherPartyName: String? = null,
val postingText: String? = null,
var userSetReference: String? = null,
var userSetOtherPartyName: String? = null
val userSetReference: String? = null,
val userSetOtherPartyName: String? = null
) {
constructor(entity: AccountTransactionEntity) : this(entity.id, entity.bankId, entity.accountId, entity)

View File

@ -1,8 +1,8 @@
package net.codinux.banking.ui.model
import androidx.compose.runtime.*
import net.codinux.banking.persistence.entities.BankAccountEntity
import net.codinux.banking.persistence.entities.BankAccessEntity
import net.codinux.banking.dataaccess.entities.BankAccountEntity
import net.codinux.banking.dataaccess.entities.BankAccessEntity
class AccountTransactionsFilter {

View File

@ -1,17 +0,0 @@
package net.codinux.banking.ui.model
class AuthenticationResult(
val successful: Boolean,
val error: String? = null
) {
override fun toString(): String {
return if (successful) {
"Successful"
}
else {
"Error occurred: $error"
}
}
}

View File

@ -1,7 +1,7 @@
package net.codinux.banking.ui.model
import net.codinux.banking.persistence.entities.BankAccountEntity
import net.codinux.banking.persistence.entities.BankAccessEntity
import net.codinux.banking.dataaccess.entities.BankAccountEntity
import net.codinux.banking.dataaccess.entities.BankAccessEntity
data class BankAccountFilter(
val bank: BankAccessEntity,

View File

@ -0,0 +1,27 @@
package net.codinux.banking.ui.model
import net.codinux.banking.client.model.BankingGroup
import kotlinx.serialization.Serializable
@Serializable
class BankInfo(
val name: String,
val domesticBankCode: String,
val bic: String = "",
val postalCode: String,
val city: String,
val pinTanAddress: String? = null,
val pinTanVersion: String? = null,
val bankingGroup: BankingGroup? = null,
val branchesInOtherCities: List<String> = listOf() // to have only one entry per bank its branches' cities are now stored in branchesInOtherCities so that branches' cities are still searchable
) {
val supportsPinTan: Boolean
get() = pinTanAddress.isNullOrEmpty() == false
val supportsFinTs3_0: Boolean
get() = pinTanVersion == "FinTS V3.0"
override fun toString() = "$domesticBankCode $name $city"
}

View File

@ -1,7 +0,0 @@
package net.codinux.banking.ui.model
data class DecodeEpcQrCodeResult(
val data: ShowTransferMoneyDialogData?,
val error: String? = null,
val charset: String? = null
)

View File

@ -1,7 +1,5 @@
package net.codinux.banking.ui.model
import net.codinux.banking.bankfinder.BankInfo
data class RecipientSuggestion(
val name: String,
val bankIdentifier: String?,

View File

@ -1,7 +1,7 @@
package net.codinux.banking.ui.model
import net.codinux.banking.client.model.Amount
import net.codinux.banking.persistence.entities.BankAccountEntity
import net.codinux.banking.dataaccess.entities.BankAccountEntity
data class ShowTransferMoneyDialogData(
val senderAccount: BankAccountEntity? = null,

View File

@ -1,4 +1,4 @@
package net.codinux.banking.ui.model.settings
package net.codinux.banking.ui.model
enum class TransactionsGrouping {
None,

View File

@ -5,11 +5,5 @@ enum class ErroneousAction {
UpdateAccountTransactions,
TransferMoney,
ReadEpcQrCode,
SaveToDatabase,
BiometricAuthentication
TransferMoney
}

View File

@ -1,161 +0,0 @@
package net.codinux.banking.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import net.codinux.banking.client.model.isNegative
import net.codinux.banking.persistence.entities.AccountTransactionEntity
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.extensions.rememberVerticalScroll
import net.codinux.banking.ui.forms.LabelledValue
import net.codinux.banking.ui.forms.OutlinedTextField
import net.codinux.banking.ui.forms.SectionHeader
private val formatUtil = DI.formatUtil
@Composable
fun AccountTransactionDetailsScreen(transaction: AccountTransactionEntity, onClosed: () -> Unit) {
val isExpense = transaction.amount.isNegative
val account = DI.uiState.banks.value.firstOrNull { it.id == transaction.bankId }?.accounts?.firstOrNull { it.id == transaction.accountId }
val accountCurrency = account?.currency ?: transaction.currency // transaction.currency just as fallback
val showColoredAmounts = DI.uiSettings.showColoredAmounts.collectAsState()
val hasDetailedValues = transaction.customerReference != null ||
transaction.endToEndReference != null || transaction.mandateReference != null
|| transaction.creditorIdentifier != null || transaction.originatorsIdentificationCode != null
|| transaction.compensationAmount != null || transaction.originalAmount != null
|| transaction.deviantOriginator != null || transaction.deviantRecipient != null
|| transaction.referenceWithNoSpecialType != null
|| transaction.journalNumber != null || transaction.textKeyAddition != null
var enteredOtherPartyName by remember { mutableStateOf(transaction.displayedOtherPartyName ?: "") }
var enteredReference by remember { mutableStateOf(transaction.displayedReference ?: "") }
var enteredNotes by remember { mutableStateOf(transaction.notes ?: "") }
val hasDataChanged by remember(enteredOtherPartyName, enteredReference, enteredNotes) {
mutableStateOf(
(enteredOtherPartyName != transaction.userSetOtherPartyName && (transaction.userSetOtherPartyName?.isNotBlank() == true || enteredOtherPartyName.isNotBlank()))
|| (enteredReference != transaction.userSetReference && (transaction.userSetReference?.isNotBlank() == true || enteredReference.isNotBlank()))
|| (enteredNotes != transaction.notes && enteredNotes.isNotBlank())
)
}
val coroutineScope = rememberCoroutineScope()
fun saveChanges() {
coroutineScope.launch {
DI.bankingService.updateAccountTransactionEntity(transaction, enteredOtherPartyName.takeUnless { it.isBlank() }, enteredReference.takeUnless { it.isBlank() }, enteredNotes.takeUnless { it.isBlank() })
}
}
FullscreenViewBase(
"Umsatzdetails",
confirmButtonTitle = "Speichern",
confirmButtonEnabled = hasDataChanged,
showDismissButton = true,
onConfirm = { saveChanges() },
onClosed = onClosed
) {
SelectionContainer {
Column(Modifier.fillMaxSize().rememberVerticalScroll()) {
Column(Modifier.fillMaxWidth()) {
SectionHeader(if (isExpense) "Empfänger*in" else "Zahlende*r", false)
OutlinedTextField(
label = { Text("Name") },
value = enteredOtherPartyName,
onValueChange = { enteredOtherPartyName = it },
modifier = Modifier.fillMaxWidth()
)
LabelledValue("BIC", transaction.otherPartyBankId ?: "")
LabelledValue("IBAN", transaction.otherPartyAccountId ?: "")
}
Column(Modifier.fillMaxWidth()) {
SectionHeader("Betrag, Datum und Verwendungszweck")
LabelledValue("Betrag", formatUtil.formatAmount(transaction.amount, transaction.currency),
formatUtil.getColorForAmount(transaction.amount, showColoredAmounts.value))
LabelledValue("Buchungstext", transaction.postingText ?: "")
OutlinedTextField(
label = { Text("Verwendungszweck") },
value = enteredReference,
onValueChange = { enteredReference = it },
modifier = Modifier.fillMaxWidth().padding(top = 8.dp)
)
LabelledValue("Buchungsdatum", formatUtil.formatDate(transaction.bookingDate))
LabelledValue("Wertstellungsdatum", formatUtil.formatDate(transaction.valueDate))
transaction.openingBalance?.let {
LabelledValue("Tagesanfangssaldo", formatUtil.formatAmount(it, accountCurrency))
}
transaction.closingBalance?.let {
LabelledValue("Tagesendsaldo", formatUtil.formatAmount(it, accountCurrency))
}
OutlinedTextField(
label = { Text("Notizen") },
value = enteredNotes,
onValueChange = { enteredNotes = it },
singleLine = false,
minLines = 2,
maxLines = 3,
modifier = Modifier.fillMaxWidth().padding(top = 8.dp)
)
}
if (hasDetailedValues) {
Column(Modifier.fillMaxWidth()) {
SectionHeader("(Lastschrift-)Details")
LabelledValue("Kundenreferenz", transaction.customerReference)
LabelledValue("Bankreferenz", transaction.bankReference)
LabelledValue("Währungsart und Umsatzbetrag in Ursprungswährung", transaction.furtherInformation)
LabelledValue("Ende-zu-Ende Referenz", transaction.endToEndReference)
LabelledValue("Mandatsreferenz", transaction.mandateReference)
LabelledValue("Gläubiger-Identifikationsnummer", transaction.creditorIdentifier)
LabelledValue("Kennung des Auftraggebers", transaction.originatorsIdentificationCode)
LabelledValue("Betrag der ursprünglichen Lastschrift", transaction.originalAmount)
LabelledValue("Rücklastschrift Auslagenersatz und Bearbeitungsprovision", transaction.compensationAmount)
LabelledValue("Abweichender Überweisender oder Zahlungsempfänger", transaction.deviantOriginator)
LabelledValue("Abweichender Zahlungsempfänger oder Zahlungspflichtiger", transaction.deviantRecipient)
LabelledValue("Verwendungszweck ohne spezielle Bedeutung", transaction.referenceWithNoSpecialType)
LabelledValue("Auszugsnummer", transaction.statementNumber?.toString())
LabelledValue("Blattnummer", transaction.sheetNumber?.toString())
LabelledValue("Primanoten-Nr.", transaction.journalNumber)
// LabelledValue("Kundenreferenz", transaction.textKeyAddition)
LabelledValue("Referenznummer", transaction.orderReferenceNumber)
LabelledValue("Bezugsreferenz", transaction.referenceNumber)
}
}
}
}
}
}

View File

@ -1,100 +0,0 @@
package net.codinux.banking.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import net.codinux.banking.persistence.entities.BankAccountEntity
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.config.Internationalization
import net.codinux.banking.ui.extensions.verticalScroll
import net.codinux.banking.ui.forms.*
@Composable
fun BankAccountSettingsScreen(account: BankAccountEntity, onClosed: () -> Unit) {
var enteredAccountName by remember { mutableStateOf(account.displayName) }
var selectedHideAccount by remember { mutableStateOf(account.hideAccount) }
var selectedIncludeInAutomaticAccountsUpdate by remember { mutableStateOf(account.includeInAutomaticAccountsUpdate) }
val hasDataChanged by remember(enteredAccountName, selectedHideAccount, selectedIncludeInAutomaticAccountsUpdate) {
mutableStateOf(
enteredAccountName != account.displayName
|| selectedHideAccount != account.hideAccount
|| selectedIncludeInAutomaticAccountsUpdate != account.includeInAutomaticAccountsUpdate
)
}
val coroutineScope = rememberCoroutineScope()
fun saveChanges() {
coroutineScope.launch {
DI.bankingService.updateAccount(account, enteredAccountName, selectedHideAccount, selectedIncludeInAutomaticAccountsUpdate)
}
}
FullscreenViewBase(
account.displayName,
confirmButtonTitle = "Speichern",
confirmButtonEnabled = hasDataChanged,
showDismissButton = true,
onConfirm = { saveChanges() },
onClosed = onClosed
) {
Column(Modifier.fillMaxSize().verticalScroll()) {
Column {
SectionHeader("Einstellungen", false)
OutlinedTextField(
label = { Text("Name") },
value = enteredAccountName,
onValueChange = { enteredAccountName = it },
modifier = Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 8.dp)
)
BooleanOption("Bei Kontoaktualisierung einbeziehen (autom. Kontoaktualisierung noch nicht umgesetzt)", selectedIncludeInAutomaticAccountsUpdate) { selectedIncludeInAutomaticAccountsUpdate = it }
BooleanOption("Konto ausblenden", selectedHideAccount) { selectedHideAccount = it }
}
SelectionContainer {
Column {
SectionHeader("Kontodaten") // TODO: add a share icon to copy data
LabelledValue("Kontoinhaber", account.accountHolderName)
LabelledValue("Kontonummer", account.identifier)
LabelledValue("Unterkontenmerkmal", account.subAccountNumber)
LabelledValue("IBAN", account.iban ?: "")
LabelledValue("Typ", Internationalization.translate(account.type))
LabelledValue("Anzahl Tage, für die Umsätze auf dem Server vorgehalten werden", account.serverTransactionsRetentionDays?.toString() ?: "<unbekannt>", labelMaxLines = 2)
LabelledValue("Umsätze zum letzten Mal abgerufen am", account.lastAccountUpdateTime?.let { DI.formatUtil.formatDateTime(it) } ?: "<Noch nicht abgerufen>" )
}
}
Column {
SectionHeader("Unterstützt")
Column(Modifier.padding(top = 8.dp)) {
SelectableFormListItem("Kontostand abrufen", account.supportsBalanceRetrieval, "Unterstützt das Abrufen des Kontostandes")
SelectableFormListItem("Kontoumsätze abrufen", account.supportsTransactionRetrieval, "Unterstützt das Abrufen der Kontoumsätze")
SelectableFormListItem("Überweisen", account.supportsMoneyTransfer, "Unterstützt Überweisungen")
SelectableFormListItem("Echtzeitüberweisung", account.supportsInstantTransfer, "Unterstützt Echtzeitüberweisungen")
}
}
}
}
}

View File

@ -1,150 +0,0 @@
package net.codinux.banking.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
import net.codinux.banking.persistence.entities.BankAccessEntity
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.dialogs.ConfirmDialog
import net.codinux.banking.ui.extensions.verticalScroll
import net.codinux.banking.ui.forms.*
import net.codinux.banking.ui.model.Config.NewLine
@Composable
fun BankSettingsScreen(bank: BankAccessEntity, onClosed: () -> Unit) {
var enteredBankName by remember { mutableStateOf(bank.displayName) }
var enteredLoginName by remember { mutableStateOf(bank.loginName) }
var enteredPassword by remember { mutableStateOf(bank.password ?: "") }
var showDeleteBankAccessConfirmationDialog by remember { mutableStateOf(false) }
val hasDataChanged by remember(enteredBankName, enteredLoginName, enteredPassword) {
mutableStateOf(
(enteredBankName != bank.bankName && (bank.userSetDisplayName == null || enteredBankName != bank.userSetDisplayName))
|| (enteredLoginName != bank.loginName && enteredLoginName.isNotBlank())
|| (enteredPassword != bank.password && enteredPassword.isNotBlank())
)
}
val coroutineScope = rememberCoroutineScope()
fun saveChanges() {
coroutineScope.launch {
DI.bankingService.updateBank(bank, enteredLoginName, enteredPassword, enteredBankName.takeUnless { it.isBlank() })
}
}
if (showDeleteBankAccessConfirmationDialog) {
ConfirmDialog(
title = "${bank.displayName} wirklich löschen?",
text = "Dadurch werden auch alle zum Konto gehörenden Daten wie seine Kontoumsätze unwiderruflich gelöscht.${NewLine}Die Daten können nicht widerhergestellt werden.",
onDismiss = { showDeleteBankAccessConfirmationDialog = false },
onConfirm = {
coroutineScope.launch {
DI.bankingService.deleteBank(bank)
}
onClosed()
}
)
}
FullscreenViewBase(
bank.displayName,
confirmButtonTitle = "Speichern",
confirmButtonEnabled = hasDataChanged,
showDismissButton = true,
onConfirm = { saveChanges() },
onClosed = onClosed
) {
Column(Modifier.fillMaxSize().verticalScroll()) {
Column {
OutlinedTextField(
label = { Text("Name") },
value = enteredBankName,
onValueChange = { enteredBankName = it },
modifier = Modifier.fillMaxWidth()
)
SectionHeader("Online-Banking Zugangsdaten")
OutlinedTextField(
label = { Text("Login Name") },
value = enteredLoginName,
onValueChange = { enteredLoginName = it },
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)
)
PasswordTextField(
password = enteredPassword,
onChange = { enteredPassword = it }
)
}
Column {
SectionHeader("Konten")
Column(Modifier.padding(top = 8.dp)) {
bank.accountsSorted.forEach { account ->
FormListItem(account.displayName, itemHeight = 42.dp) {
DI.uiState.showBankAccountSettingsScreenForAccount.value = account
}
}
}
}
Column {
SectionHeader("Bankdaten")
LabelledValue("Bankleitzahl", bank.domesticBankCode)
LabelledValue("BIC", bank.bic ?: "")
LabelledValue("Kontoinhaber", bank.customerName)
LabelledValue("FinTS Server", bank.serverAddress)
}
Column {
SectionHeader("TAN Verfahren")
Column(Modifier.padding(top = 8.dp)) {
bank.tanMethodsSorted.forEach { tanMethod ->
SelectableFormListItem(tanMethod.displayName, tanMethod == bank.selectedTanMethod, "TAN Verfahren ist ausgewähltes TAN Verfahren")
}
}
}
if (bank.tanMedia.isNotEmpty()) {
Column {
SectionHeader("TAN Medien")
Column(Modifier.padding(top = 8.dp)) {
bank.tanMediaSorted.forEach { tanMedium ->
SelectableFormListItem(tanMedium.displayName, tanMedium == bank.selectedTanMedium, "TAN Medium ist ausgewähltes TAN Medium")
}
}
}
}
Spacer(Modifier.weight(1f))
Column(Modifier.padding(top = 24.dp, bottom = 18.dp)) {
TextButton(modifier = Modifier.fillMaxWidth().height(50.dp), onClick = { showDeleteBankAccessConfirmationDialog = true }) {
Text("Konto löschen", fontSize = 15.sp, color = Colors.DestructiveColor, textAlign = TextAlign.Center)
}
}
}
}
}

View File

@ -1,186 +0,0 @@
package net.codinux.banking.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import net.codinux.banking.persistence.entities.BankAccountEntity
import net.codinux.banking.ui.composables.BankIcon
import net.codinux.banking.ui.composables.tan.ImageView
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.extensions.ImeNext
import net.codinux.banking.ui.extensions.rememberVerticalScroll
import net.codinux.banking.ui.forms.CaptionText
import net.codinux.banking.ui.forms.OutlinedTextField
import net.codinux.banking.ui.forms.Select
import net.codinux.banking.ui.model.Config.NewLine
import net.codinux.log.Log
private val epcQrCodeService = DI.epcQrCodeService
@Composable
fun CreateEpcQrCodeScreen(onClosed: () -> Unit) {
val banks = DI.uiState.banks.collectAsState().value
val accountsWithIban: List<BankAccountEntity?> = buildList {
add(null)
addAll(DI.uiState.accounts.collectAsState().value.filter { it.iban != null })
}
var selectedAccount by remember { mutableStateOf<BankAccountEntity?>(null) }
val bankOfSelectedAccount by remember(selectedAccount) {
derivedStateOf { banks.firstOrNull { it.id == selectedAccount?.bankId } }
}
val amountFocus = remember { FocusRequester() }
var receiverName by remember { mutableStateOf("") }
var iban by remember { mutableStateOf("") }
var bic by remember { mutableStateOf("") }
var amount by remember { mutableStateOf("") }
var reference by remember { mutableStateOf("") }
var informationForUser by remember { mutableStateOf("") }
var epcQrCodeGeneratingError by remember { mutableStateOf<String?>(null) }
val epcQrCodeBytes by remember(receiverName, iban, bic, amount, reference, informationForUser) {
derivedStateOf {
epcQrCodeGeneratingError = null
if (receiverName.isNotBlank() && iban.isNotBlank()) {
try {
epcQrCodeService.generateEpcQrCode(receiverName, iban, bic.takeUnless { it.isBlank() }, amount.takeUnless { it.isBlank() }, reference.takeUnless { it.isBlank() }, informationForUser.takeUnless { it.isBlank() })
} catch (e: Throwable) {
Log.error(e) { "Could not generate EPC QR Code" }
epcQrCodeGeneratingError = e.message
null
}
} else {
null
}
}
}
FullscreenViewBase("EPC QR Code erstellen", "Schließen", onClosed = onClosed) {
Column(Modifier.fillMaxWidth().rememberVerticalScroll()) {
if (epcQrCodeGeneratingError != null) {
Text("QR Code konnte nicht erstellt werden:${NewLine}$epcQrCodeGeneratingError", color = MaterialTheme.colors.error, modifier = Modifier.padding(vertical = 8.dp))
} else if (epcQrCodeBytes == null) {
Text("Mit EPC QR Codes, welche als GiroCode, scan2code, ... vermarktet werden, können Überweisungsdaten ganz einfach von Banking Apps eingelesen werden.")
Text("Hier können Sie Ihren eigenen erstellen, so dass jemand Ihre Überweisungsdaten einlesen und Ihnen ganz schnell Geld überweisen kann.")
} else {
ImageView(epcQrCodeBytes!!, "EpcQrCode", "Erzeugter EPC QR Code", 350, 100, 700)
Row(Modifier.fillMaxWidth().padding(top = 8.dp), horizontalArrangement = Arrangement.Center) {
Text("Scannen Sie diesen Code auf einem anderen Gerät mit einer Banking App, z. B. Bankmeister", textAlign = TextAlign.Center, modifier = Modifier.padding(horizontal = 16.dp).padding(top = 8.dp))
}
}
if (accountsWithIban.size > 1) {
Select(
"Für Konto",
accountsWithIban,
selectedAccount,
{ account ->
selectedAccount = account
if (account != null) {
iban = account.iban ?: ""
bic = banks.firstOrNull { it.id == selectedAccount?.bankId }?.bic ?: ""
receiverName = account.accountHolderName
amountFocus.requestFocus()
}
},
{ account -> account?.displayName ?: "" },
leadingIcon = bankOfSelectedAccount?.let { { BankIcon(bankOfSelectedAccount) } },
dropDownItemContent = { account ->
Row(verticalAlignment = Alignment.CenterVertically) {
BankIcon(banks.firstOrNull { it.id == account?.bankId }, Modifier.padding(end = 6.dp))
Text(account?.displayName ?: "")
}
},
modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 8.dp)
)
}
OutlinedTextField(
label = { Text("Empfänger*in") },
value = receiverName,
onValueChange = { receiverName = it },
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
keyboardOptions = KeyboardOptions.ImeNext
)
CaptionText("${receiverName.length} / 70")
OutlinedTextField(
label = { Text("IBAN") },
value = iban,
onValueChange = { iban = it },
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
keyboardOptions = KeyboardOptions.ImeNext
)
OutlinedTextField(
label = { Text("BIC (optional)") },
value = bic,
onValueChange = { bic = it },
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
keyboardOptions = KeyboardOptions.ImeNext
)
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
OutlinedTextField(
label = { Text("Betrag (optional)") },
value = amount,
onValueChange = { amount = it },
modifier = Modifier.weight(1f).padding(vertical = 8.dp).focusRequester(amountFocus),
keyboardOptions = KeyboardOptions.ImeNext
)
Text(DI.formatUtil.formatCurrency("EUR"), Modifier.padding(start = 4.dp)) // Euro is currently the only supported currency
}
OutlinedTextField(
label = { Text("Verwendungszweck (optional)") },
value = reference,
onValueChange = { reference = it },
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
keyboardOptions = KeyboardOptions.ImeNext
)
CaptionText("${reference.length} / 140")
// not exposing it to user as it's a) not displayed by most apps and b) may causes overflow of used QR code library
// OutlinedTextField(
// label = { Text("Information für den Nutzer (optional)") },
// value = informationForUser,
// onValueChange = { informationForUser = it },
// modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
// keyboardOptions = KeyboardOptions.ImeNext
// )
//
// CaptionText("${informationForUser.length} / 70")
}
}
}

View File

@ -1,5 +1,6 @@
package net.codinux.banking.ui.screens
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.Text
@ -13,18 +14,12 @@ import androidx.compose.ui.text.style.TextAlign
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.codinux.banking.persistence.entities.AccountTransactionEntity
import net.codinux.banking.dataaccess.entities.AccountTransactionEntity
import net.codinux.banking.ui.IOorDefault
import net.codinux.banking.ui.PlatformType
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.extensions.horizontalScroll
import net.codinux.banking.ui.extensions.verticalScroll
import net.codinux.banking.ui.service.BankDataImporterAndExporter
private const val iOSMaxDisplayedDataLength = 20_000
@Composable
fun ExportScreen(onClosed: () -> Unit) {
var transactions: Collection<AccountTransactionEntity>
@ -33,8 +28,6 @@ fun ExportScreen(onClosed: () -> Unit) {
var exportedDataText by remember { mutableStateOf("") }
var exportedDataTextToDisplay by remember { mutableStateOf("") }
val importerExporter = BankDataImporterAndExporter()
val clipboardManager = LocalClipboardManager.current
@ -48,15 +41,12 @@ fun ExportScreen(onClosed: () -> Unit) {
withContext(Dispatchers.Main) {
exportedDataText = initiallyExportedData
exportedDataTextToDisplay = if (DI.platform.type == PlatformType.iOS && exportedDataText.length > iOSMaxDisplayedDataLength) exportedDataText.substring(0,
iOSMaxDisplayedDataLength) + "\r\n...\r\n(Wir mussten die Anzeige abschneiden, da iOS mit längeren Zeichenketten nicht klar kommt (iOS only bug)"
else exportedDataText
isLoadingExportedData = false
}
}
FullscreenViewBase("Umsätze exportieren", onClosed = onClosed) {
Column(Modifier.fillMaxWidth()) {
Column {
Text("Es gibt leider noch keinen \"Datei auswählen Dialog\", ist sehr schwierig plattformübergreifend umzusetzen, deshalb bitte folgenden Text kopieren und in eine Textdatei einfügen:")
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
@ -65,7 +55,13 @@ fun ExportScreen(onClosed: () -> Unit) {
}
}
if (isLoadingExportedData) {
if (isLoadingExportedData == false) {
Column(Modifier.verticalScroll(ScrollState(0), enabled = true).horizontalScroll(ScrollState(0), enabled = true)) {
SelectionContainer {
Text(exportedDataText, fontFamily = FontFamily.Monospace)
}
}
} else {
Column(Modifier.fillMaxSize()) {
Spacer(Modifier.weight(1f))
@ -75,12 +71,6 @@ fun ExportScreen(onClosed: () -> Unit) {
Spacer(Modifier.weight(1f))
}
} else {
Column(Modifier.verticalScroll().horizontalScroll()) {
SelectionContainer(modifier = Modifier.fillMaxSize()) {
Text(exportedDataTextToDisplay, fontFamily = FontFamily.Monospace)
}
}
}
}
}

View File

@ -1,103 +0,0 @@
package net.codinux.banking.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import net.codinux.banking.client.model.BankAccount
import net.codinux.banking.persistence.entities.BankAccountEntity
import net.codinux.banking.ui.composables.BankIcon
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.extensions.horizontalScroll
import net.codinux.banking.ui.extensions.verticalScroll
import net.codinux.banking.ui.forms.SectionHeader
import net.codinux.banking.ui.forms.Select
import net.codinux.banking.ui.model.Config.NewLine
@Composable
fun FeedbackScreen(onClosed: () -> Unit) {
val banks = DI.uiState.banks.collectAsState().value
val messageLog by remember { mutableStateOf(DI.bankingService.getMessageLog()) }
val accountsWithMessageLog: List<BankAccount?> by remember(messageLog) { derivedStateOf {
listOf<BankAccount?>(null) + messageLog.mapNotNull { it.account }.toSet().toList()
} }
var selectedAccount by remember { mutableStateOf<BankAccount?>(null) }
val bankOfSelectedAccount by remember(selectedAccount) {
// TODO: MessageLogEntries of added accounts contain a BankAccount instead of a BankAccountEntity object
derivedStateOf { banks.firstOrNull { it.id == (selectedAccount as? BankAccountEntity)?.bankId } }
}
val displayedLogEntries by remember(messageLog, selectedAccount) { derivedStateOf {
messageLog.filter { selectedAccount == null || it.account == selectedAccount }.sortedBy { it.messageNumber }
} }
val displayedLogEntriesText by remember(displayedLogEntries) {
derivedStateOf { displayedLogEntries.map {
"${it.messageNumberString} ${it.bank?.displayName ?: ""} ${it.account?.displayName ?: ""} ${it.type} ${it.jobType} ${it.messageCategory}${NewLine}${it.message}"
}.joinToString(NewLine + NewLine) }
}
val clipboardManager = LocalClipboardManager.current
FullscreenViewBase("Feedback", onClosed = onClosed) {
Column(Modifier.fillMaxWidth()) {
Text("Man kann uns direkt aus der App heraus noch kein Feedback schicken (kommt noch), aber schon mal das Nachrichten-Protokoll, das ihr bei Fehlern an die (unfähgigen) Entwickler schicken könnt:", modifier = Modifier.padding(horizontal = 24.dp).padding(top = 8.dp), textAlign = TextAlign.Center)
SectionHeader("Nachrichten-Protokoll:") // TODO: add a Switch "Add message protocol"
if (messageLog.isEmpty()) {
Row(Modifier.fillMaxSize().padding(24.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) {
Text("Sie haben noch keine Aktion wie das Abrufen der Kontoumsätze ausgeführt, deshalb gibt es noch kein Nachrichtenprotokoll zu diesen Aktionen", textAlign = TextAlign.Center)
}
} else {
Row(Modifier.fillMaxWidth().padding(top = 6.dp), verticalAlignment = Alignment.CenterVertically) {
Select(
"Konto",
accountsWithMessageLog,
selectedAccount,
{ account -> selectedAccount = account },
{ account -> account?.displayName ?: "Alle Konten" },
Modifier.weight(0.9f),
leadingIcon = bankOfSelectedAccount?.let { { BankIcon(bankOfSelectedAccount) } },
dropDownItemContent = { account ->
Row(verticalAlignment = Alignment.CenterVertically) {
BankIcon(banks.firstOrNull { it.id == (account as? BankAccountEntity)?.bankId }, Modifier.padding(end = 6.dp))
Text(account?.displayName ?: "")
}
},
)
Spacer(Modifier.weight(0.1f))
TextButton({ clipboardManager.setText(AnnotatedString(displayedLogEntriesText))}) {
Text("Kopieren", color = Colors.CodinuxSecondaryColor)
}
}
Column(Modifier.verticalScroll().horizontalScroll().padding(top = 8.dp)) {
SelectionContainer(modifier = Modifier.fillMaxSize()) {
Text(displayedLogEntriesText, fontFamily = FontFamily.Monospace)
}
}
}
}
}
}

View File

@ -3,8 +3,9 @@ package net.codinux.banking.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
@ -12,23 +13,15 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.zIndex
import net.codinux.banking.ui.PlatformType
import net.codinux.banking.ui.composables.CloseButton
import net.codinux.banking.ui.composables.text.HeaderText
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.config.Style
import net.codinux.banking.ui.extensions.applyPlatformSpecificPadding
@Composable
fun FullscreenViewBase(
title: String,
confirmButtonTitle: String = "OK",
confirmButtonEnabled: Boolean = true,
dismissButtonTitle: String = "Abbrechen",
showDismissButton: Boolean = false,
showButtonBar: Boolean = true,
onConfirm: (() -> Unit)? = null,
onClosed: () -> Unit,
content: @Composable () -> Unit
) {
@ -36,13 +29,15 @@ fun FullscreenViewBase(
onClosed,
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
Column(Modifier.fillMaxSize().zIndex(1000f).background(Color.White).applyPlatformSpecificPadding().padding(horizontal = 12.dp)) {
Column(Modifier.fillMaxSize().zIndex(1000f).background(Color.White).padding(8.dp)) {
Row(Modifier.fillMaxWidth().padding(top = 12.dp, bottom = 8.dp).height(32.dp), verticalAlignment = Alignment.CenterVertically) {
HeaderText(title, Modifier.weight(1f), textColor = Style.ListItemHeaderTextColor)
Row(Modifier.fillMaxWidth()) {
HeaderText(title, Modifier.padding(top = 8.dp, bottom = 16.dp).weight(1f))
if (DI.platform.type != PlatformType.Android) { // for iOS it's also relevant due to the missing back gesture / back button
CloseButton(onClick = onClosed)
if (DI.platform.isDesktop) {
TextButton(onClosed, colors = ButtonDefaults.buttonColors(contentColor = Colors.Zinc700, backgroundColor = Color.Transparent)) {
Icon(Icons.Filled.Close, contentDescription = "Close dialog", Modifier.size(32.dp))
}
}
}
@ -50,25 +45,21 @@ fun FullscreenViewBase(
content()
}
if (showButtonBar) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
if (showDismissButton) {
TextButton(onClick = onClosed, Modifier.weight(1f)) {
Text(dismissButtonTitle, color = Colors.CodinuxSecondaryColor)
}
Spacer(Modifier.width(8.dp))
}
// TextButton(onClick = onClosed, Modifier.width(Style.DialogButtonWidth)) {
// Text("Abbrechen", color = Colors.CodinuxSecondaryColor)
// }
//
// Spacer(Modifier.width(8.dp))
TextButton(
modifier = Modifier.weight(1f),
modifier = Modifier.fillMaxWidth(),
enabled = confirmButtonEnabled,
onClick = { onConfirm?.invoke(); onClosed() }
onClick = { /* onConfirm?.invoke() ?: */ onClosed() }
) {
Text(confirmButtonTitle, color = Colors.CodinuxSecondaryColor, textAlign = TextAlign.Center)
}
}
}
}
}
}

View File

@ -1,114 +0,0 @@
package net.codinux.banking.ui.screens
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import bankmeister.composeapp.generated.resources.*
import bankmeister.composeapp.generated.resources.Res
import net.codinux.banking.ui.composables.authentification.BiometricAuthenticationButton
import net.codinux.banking.ui.forms.PasswordTextField
import net.codinux.banking.ui.model.AuthenticationResult
import net.codinux.banking.ui.model.settings.AppAuthenticationMethod
import net.codinux.banking.ui.model.settings.AppSettings
import net.codinux.banking.ui.service.AuthenticationService
import net.codinux.banking.ui.service.safelyAuthenticateWithBiometrics
import org.jetbrains.compose.resources.imageResource
@Composable
fun LoginScreen(appSettings: AppSettings, onLoginSuccess: () -> Unit) {
var password by remember { mutableStateOf("") }
var showError by remember { mutableStateOf(false) }
fun successfullyLoggedIn() {
onLoginSuccess()
}
fun checkPassword() {
if (appSettings.hashedPassword != null && AuthenticationService.checkPassword(password, appSettings.hashedPassword!!)) {
successfullyLoggedIn()
} else {
showError = true
}
}
fun checkBiometricLoginResult(result: AuthenticationResult) {
if (result.successful) {
successfullyLoggedIn()
} else {
showError = true
}
}
Box(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Image(imageResource(Res.drawable.AppIcon_round), "Bankmeister's app icon", Modifier.size(144.dp).padding(bottom = 32.dp))
if (appSettings.authenticationMethod == AppAuthenticationMethod.Password) {
Text("Bitte geben Sie Ihr Passwort ein um die App zu entsperren", style = MaterialTheme.typography.h5, textAlign = TextAlign.Center)
Spacer(modifier = Modifier.height(24.dp))
PasswordTextField(
password = password,
onEnterPressed = { checkPassword() },
isError = showError
) {
password = it
showError = false
}
if (showError) {
Spacer(modifier = Modifier.height(16.dp))
Text("Passwort ist falsch", color = MaterialTheme.colors.error, fontSize = 18.sp)
}
Button(modifier = Modifier.padding(top = 24.dp).width(300.dp).height(50.dp), onClick = { checkPassword() }) {
Text("Login", color = Color.White)
}
}
if (appSettings.authenticationMethod == AppAuthenticationMethod.Biometric) {
if (showError) {
Text("Biometrische Authentifizierung fehlgeschlagen. Bitte versuchen Sie es erneut.", color = MaterialTheme.colors.error, fontSize = 18.sp,
textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp).padding(horizontal = 16.dp))
}
BiometricAuthenticationButton {
checkBiometricLoginResult(it)
}
}
}
}
LaunchedEffect(appSettings.authenticationMethod) {
if (appSettings.authenticationMethod == AppAuthenticationMethod.Biometric) {
AuthenticationService.safelyAuthenticateWithBiometrics { result ->
checkBiometricLoginResult(result)
}
}
}
}

View File

@ -6,7 +6,6 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@ -31,8 +30,6 @@ fun MainScreen() {
val fabPositionAdjustment = if (isMobile) 44.dp // FabSpacing = 16.dp + FAB height (= 56.dp) / 2
else (-10).dp
var showFloatingActionMenu by remember { mutableStateOf(false) }
val desktopDrawerWidth = 350.dp
val uiState = DI.uiState
@ -54,13 +51,9 @@ fun MainScreen() {
FloatingActionButton(
shape = CircleShape,
modifier = Modifier.offset(x = 4.dp, y = fabPositionAdjustment),
onClick = { showFloatingActionMenu = !showFloatingActionMenu }
onClick = { uiState.showAddAccountDialog.value = true }
) {
if (showFloatingActionMenu) {
Icon(Icons.Filled.Close, contentDescription = "Menü zum Hinzufügen eines Kontos, für eine neue Überweisung, ... verstecken")
} else {
Icon(Icons.Filled.Add, contentDescription = "Zeigt ein Menü zum Hinzufügen eines Kontos, für eine neue Überweisung, ... an")
}
Icon(Icons.Filled.Add, contentDescription = "Add a bank account")
}
},
drawerContent = { if (isMobile) SideMenuContent() else null },
@ -92,9 +85,6 @@ fun MainScreen() {
}
}
FloatingActionMenu(showFloatingActionMenu) { showFloatingActionMenu = false }
if (showFilterBar.value) {
FilterBar()
}

View File

@ -1,127 +0,0 @@
package net.codinux.banking.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
import net.codinux.banking.ui.composables.authentification.BiometricAuthenticationButton
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.config.Internationalization
import net.codinux.banking.ui.extensions.ImeDone
import net.codinux.banking.ui.extensions.ImeNext
import net.codinux.banking.ui.extensions.verticalScroll
import net.codinux.banking.ui.forms.PasswordTextField
import net.codinux.banking.ui.forms.SegmentedControl
import net.codinux.banking.ui.model.settings.AppAuthenticationMethod
import net.codinux.banking.ui.model.settings.AppSettings
import net.codinux.banking.ui.service.AuthenticationService
private val buttonHeight = 50.dp
@Composable
fun ProtectAppSettingsDialog(appSettings: AppSettings, onClosed: () -> Unit) {
val currentAuthenticationMethod = appSettings.authenticationMethod
val isBiometricAuthenticationSupported = AuthenticationService.supportsBiometricAuthentication
val supportedAuthenticationMethods = buildList {
add(AppAuthenticationMethod.Password)
if (isBiometricAuthenticationSupported) {
add(AppAuthenticationMethod.Biometric)
}
add(AppAuthenticationMethod.None)
}
var selectedAuthenticationMethod by remember { mutableStateOf(if (appSettings.authenticationMethod == AppAuthenticationMethod.None) AppAuthenticationMethod.Password else appSettings.authenticationMethod) }
var newPassword by remember { mutableStateOf("") }
var confirmedNewPassword by remember { mutableStateOf("") }
var hasAuthenticatedWithBiometric by remember { mutableStateOf(false) }
val isRequiredDataEntered by remember(newPassword, confirmedNewPassword) {
derivedStateOf {
(selectedAuthenticationMethod == AppAuthenticationMethod.Password && newPassword.isNotBlank() && newPassword == confirmedNewPassword)
|| (selectedAuthenticationMethod == AppAuthenticationMethod.Biometric && hasAuthenticatedWithBiometric)
}
}
val coroutineScope = rememberCoroutineScope()
fun saveNewAppProtection() {
coroutineScope.launch {
appSettings.authenticationMethod = selectedAuthenticationMethod
appSettings.hashedPassword = if (selectedAuthenticationMethod == AppAuthenticationMethod.Password) AuthenticationService.hashPassword(newPassword)
else null
DI.bankingService.saveAppSettings(appSettings)
onClosed()
}
}
FullscreenViewBase("Appzugang schützen", showButtonBar = false, onClosed = onClosed) {
Column(Modifier.fillMaxSize().padding(bottom = 8.dp)) {
SegmentedControl(supportedAuthenticationMethods, selectedAuthenticationMethod, Modifier.padding(bottom = 20.dp), getOptionDisplayText = { Internationalization.translate(it) }) {
selectedAuthenticationMethod = it
}
Column(Modifier.weight(1f).verticalScroll()) {
Spacer(Modifier.weight(1f))
if (selectedAuthenticationMethod == AppAuthenticationMethod.None) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
if (currentAuthenticationMethod == AppAuthenticationMethod.None) {
Text("Appzugang ist bereits ungeschützt", fontSize = 18.sp, textAlign = TextAlign.Center)
} else {
Text("Möchten Sie den Appzugangsschutz wirklich entfernen?", fontSize = 18.sp, textAlign = TextAlign.Center)
}
}
}
if (selectedAuthenticationMethod == AppAuthenticationMethod.Password) {
PasswordTextField(newPassword, "Neues Password", keyboardOptions = KeyboardOptions.ImeNext) { newPassword = it }
PasswordTextField(confirmedNewPassword, "Password bestätigen", Modifier.padding(top = 16.dp), keyboardOptions = KeyboardOptions.ImeDone) { confirmedNewPassword = it }
}
if (selectedAuthenticationMethod == AppAuthenticationMethod.Biometric) {
BiometricAuthenticationButton { result ->
hasAuthenticatedWithBiometric = result.successful
}
}
Spacer(Modifier.weight(1f))
}
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
if (selectedAuthenticationMethod == AppAuthenticationMethod.None) {
Button(modifier = Modifier.fillMaxWidth().height(buttonHeight), enabled = currentAuthenticationMethod != AppAuthenticationMethod.None,
colors = ButtonDefaults.buttonColors(Colors.DestructiveColor), onClick = { saveNewAppProtection() }) {
Text("Appzugangsschutz entfernen", color = Color.White)
}
} else {
Button(modifier = Modifier.fillMaxWidth().height(buttonHeight), enabled = isRequiredDataEntered,
colors = ButtonDefaults.buttonColors(Colors.Accent), onClick = { saveNewAppProtection() }) {
Text("Setzen", color = Color.White)
}
}
}
}
}
}

View File

@ -1,30 +0,0 @@
package net.codinux.banking.ui.screens
import androidx.compose.runtime.Composable
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.model.Config.NewLine
import net.codinux.banking.ui.model.error.ErroneousAction
import net.codinux.banking.ui.service.QrCodeService
@Composable
fun TransferMoneyFromQrCodeScreen(onClosed: () -> Unit) {
if (QrCodeService.supportsReadingQrCodesFromCamera) {
FullscreenViewBase("Überweisungsdaten aus QR Code lesen", "Abbrechen", onClosed = onClosed) {
QrCodeService.readQrCodeFromCamera { result ->
onClosed()
if (result.decodedQrCodeText != null) {
val decodingResult = DI.epcQrCodeService.decode(result.decodedQrCodeText)
if (decodingResult.data != null) {
DI.uiState.showTransferMoneyDialogData.value = decodingResult.data
} else if (decodingResult.error != null) {
DI.uiState.applicationErrorOccurred(ErroneousAction.ReadEpcQrCode, decodingResult.error + "${NewLine}${NewLine}Gelesener QR-Code ist:${NewLine}${result.decodedQrCodeText}")
}
}
}
}
}
}

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