Compare commits
10 Commits
2813224eff
...
d447f2991c
Author | SHA1 | Date |
---|---|---|
dankito | d447f2991c | |
dankito | ba156a8512 | |
dankito | 41c2b89c34 | |
dankito | 607eb4c2f5 | |
dankito | 9412f6b7f0 | |
dankito | 4697119c58 | |
dankito | 6e6449e956 | |
dankito | f1c4c8ca13 | |
dankito | a50f55daff | |
dankito | db8d4a7dcd |
|
@ -1,7 +1,6 @@
|
||||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||||
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
|
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
|
||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
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
|
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
|
@ -92,8 +91,11 @@ kotlin {
|
||||||
androidMain.dependencies {
|
androidMain.dependencies {
|
||||||
implementation(compose.preview)
|
implementation(compose.preview)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
|
implementation(libs.androidx.biometric)
|
||||||
|
|
||||||
implementation(libs.sqldelight.android.driver)
|
implementation(libs.sqldelight.android.driver)
|
||||||
|
|
||||||
|
implementation(libs.favre.bcrypt)
|
||||||
}
|
}
|
||||||
|
|
||||||
nativeMain.dependencies {
|
nativeMain.dependencies {
|
||||||
|
@ -110,6 +112,8 @@ kotlin {
|
||||||
|
|
||||||
implementation(libs.sqldelight.sqlite.driver)
|
implementation(libs.sqldelight.sqlite.driver)
|
||||||
|
|
||||||
|
implementation(libs.favre.bcrypt)
|
||||||
|
|
||||||
implementation(libs.logback)
|
implementation(libs.logback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -140,8 +144,8 @@ android {
|
||||||
applicationId = "net.codinux.banking.android" // the appId of the old Bankmeister app to be able to use the old PlayStore entry
|
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()
|
minSdk = libs.versions.android.minSdk.get().toInt()
|
||||||
targetSdk = libs.versions.android.targetSdk.get().toInt()
|
targetSdk = libs.versions.android.targetSdk.get().toInt()
|
||||||
versionCode = 10
|
versionCode = 11
|
||||||
versionName = "1.0.0-Alpha-12"
|
versionName = "1.0.0-Alpha-13"
|
||||||
}
|
}
|
||||||
packaging {
|
packaging {
|
||||||
resources {
|
resources {
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
|
|
@ -1,21 +1,24 @@
|
||||||
package net.codinux.banking.ui
|
package net.codinux.banking.ui
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
import app.cash.sqldelight.async.coroutines.synchronous
|
import app.cash.sqldelight.async.coroutines.synchronous
|
||||||
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
|
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
|
||||||
import net.codinux.banking.dataaccess.BankmeisterDb
|
import net.codinux.banking.dataaccess.BankmeisterDb
|
||||||
import net.codinux.banking.ui.config.DI
|
import net.codinux.banking.ui.config.DI
|
||||||
|
import net.codinux.banking.ui.service.AuthenticationService
|
||||||
|
import net.codinux.banking.ui.service.BiometricAuthenticationService
|
||||||
import net.codinux.banking.ui.service.ImageService
|
import net.codinux.banking.ui.service.ImageService
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : FragmentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
ImageService.context = this.applicationContext
|
ImageService.context = this.applicationContext
|
||||||
|
AuthenticationService.biometricAuthenticationService = BiometricAuthenticationService(this)
|
||||||
|
|
||||||
DI.setRepository(AndroidSqliteDriver(BankmeisterDb.Schema.synchronous(), this, "Bankmeister.db"))
|
DI.setRepository(AndroidSqliteDriver(BankmeisterDb.Schema.synchronous(), this, "Bankmeister.db"))
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
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 = { }
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
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) { }
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Bankmeister</string>
|
<string name="app_name">Bankmeister</string>
|
||||||
|
|
||||||
|
<string name="activity_login_authenticate_with_biometrics_prompt">Authentifizieren Sich sich um die App zu entsperren</string>
|
||||||
</resources>
|
</resources>
|
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
|
@ -9,6 +9,8 @@ import androidx.compose.ui.unit.sp
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import net.codinux.banking.ui.config.Colors
|
import net.codinux.banking.ui.config.Colors
|
||||||
import net.codinux.banking.ui.config.DI
|
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.banking.ui.screens.MainScreen
|
||||||
import net.codinux.log.LoggerFactory
|
import net.codinux.log.LoggerFactory
|
||||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||||
|
@ -25,13 +27,23 @@ fun App() {
|
||||||
val colors = MaterialTheme.colors.copy(primary = Colors.Primary, primaryVariant = Colors.PrimaryDark, onPrimary = Color.White,
|
val colors = MaterialTheme.colors.copy(primary = Colors.Primary, primaryVariant = Colors.PrimaryDark, onPrimary = Color.White,
|
||||||
secondary = Colors.Accent, secondaryVariant = Colors.Accent, onSecondary = Color.White)
|
secondary = Colors.Accent, secondaryVariant = Colors.Accent, onSecondary = Color.White)
|
||||||
|
|
||||||
|
val appSettings = DI.uiState.appSettings.collectAsState().value
|
||||||
|
|
||||||
|
var isLoggedIn by remember(appSettings.authenticationMethod) { mutableStateOf(appSettings.authenticationMethod == AppAuthenticationMethod.None) }
|
||||||
|
|
||||||
var isInitialized by remember { mutableStateOf(false) }
|
var isInitialized by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
|
||||||
MaterialTheme(colors = colors, typography = typography) {
|
MaterialTheme(colors = colors, typography = typography) {
|
||||||
MainScreen()
|
if (isLoggedIn == false) {
|
||||||
|
LoginScreen(appSettings) {
|
||||||
|
isLoggedIn = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
MainScreen()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import androidx.compose.material.Text
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.SaveAs
|
import androidx.compose.material.icons.filled.SaveAs
|
||||||
|
import androidx.compose.material.icons.outlined.Key
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
@ -124,6 +125,15 @@ fun SideMenuContent() {
|
||||||
drawerState.close()
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,7 @@ import androidx.compose.runtime.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import net.codinux.banking.ui.config.DI
|
import net.codinux.banking.ui.config.DI
|
||||||
import net.codinux.banking.ui.dialogs.*
|
import net.codinux.banking.ui.dialogs.*
|
||||||
import net.codinux.banking.ui.screens.AccountTransactionDetailsScreen
|
import net.codinux.banking.ui.screens.*
|
||||||
import net.codinux.banking.ui.screens.BankSettingsScreen
|
|
||||||
import net.codinux.banking.ui.screens.ExportScreen
|
|
||||||
import net.codinux.banking.ui.state.UiState
|
import net.codinux.banking.ui.state.UiState
|
||||||
|
|
||||||
private val formatUtil = DI.formatUtil
|
private val formatUtil = DI.formatUtil
|
||||||
|
@ -17,9 +15,13 @@ private val formatUtil = DI.formatUtil
|
||||||
fun StateHandler(uiState: UiState, snackbarHostState: SnackbarHostState) {
|
fun StateHandler(uiState: UiState, snackbarHostState: SnackbarHostState) {
|
||||||
val showAddAccountDialog by uiState.showAddAccountDialog.collectAsState()
|
val showAddAccountDialog by uiState.showAddAccountDialog.collectAsState()
|
||||||
val showTransferMoneyDialogData by uiState.showTransferMoneyDialogData.collectAsState()
|
val showTransferMoneyDialogData by uiState.showTransferMoneyDialogData.collectAsState()
|
||||||
|
|
||||||
val showAccountTransactionDetailsScreenForId by uiState.showAccountTransactionDetailsScreenForId.collectAsState()
|
val showAccountTransactionDetailsScreenForId by uiState.showAccountTransactionDetailsScreenForId.collectAsState()
|
||||||
val showBankSettingsScreenForBank by uiState.showBankSettingsScreenForBank.collectAsState()
|
val showBankSettingsScreenForBank by uiState.showBankSettingsScreenForBank.collectAsState()
|
||||||
|
val showBankAccountSettingsScreenForAccount by uiState.showBankAccountSettingsScreenForAccount.collectAsState()
|
||||||
|
|
||||||
val showExportScreen by uiState.showExportScreen.collectAsState()
|
val showExportScreen by uiState.showExportScreen.collectAsState()
|
||||||
|
val showProtectAppSettingsScreen by uiState.showProtectAppSettingsScreen.collectAsState()
|
||||||
|
|
||||||
val tanChallengeReceived by uiState.tanChallengeReceived.collectAsState()
|
val tanChallengeReceived by uiState.tanChallengeReceived.collectAsState()
|
||||||
val bankingClientError by uiState.bankingClientErrorOccurred.collectAsState()
|
val bankingClientError by uiState.bankingClientErrorOccurred.collectAsState()
|
||||||
|
@ -36,6 +38,7 @@ fun StateHandler(uiState: UiState, snackbarHostState: SnackbarHostState) {
|
||||||
TransferMoneyDialog(data) { uiState.showTransferMoneyDialogData.value = null }
|
TransferMoneyDialog(data) { uiState.showTransferMoneyDialogData.value = null }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
showAccountTransactionDetailsScreenForId?.let { transactionId ->
|
showAccountTransactionDetailsScreenForId?.let { transactionId ->
|
||||||
DI.bankingService.getTransaction(transactionId)?.let { transaction ->
|
DI.bankingService.getTransaction(transactionId)?.let { transaction ->
|
||||||
AccountTransactionDetailsScreen(transaction) { uiState.showAccountTransactionDetailsScreenForId.value = null }
|
AccountTransactionDetailsScreen(transaction) { uiState.showAccountTransactionDetailsScreenForId.value = null }
|
||||||
|
@ -46,10 +49,19 @@ fun StateHandler(uiState: UiState, snackbarHostState: SnackbarHostState) {
|
||||||
BankSettingsScreen(bank) { uiState.showBankSettingsScreenForBank.value = null }
|
BankSettingsScreen(bank) { uiState.showBankSettingsScreenForBank.value = null }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showBankAccountSettingsScreenForAccount?.let { account ->
|
||||||
|
BankAccountSettingsScreen(account) { uiState.showBankAccountSettingsScreenForAccount.value = null }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (showExportScreen) {
|
if (showExportScreen) {
|
||||||
ExportScreen { uiState.showExportScreen.value = false }
|
ExportScreen { uiState.showExportScreen.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showProtectAppSettingsScreen) {
|
||||||
|
ProtectAppSettingsDialog(uiState.appSettings.value) { uiState.showProtectAppSettingsScreen.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
tanChallengeReceived?.let { tanChallengeReceived ->
|
tanChallengeReceived?.let { tanChallengeReceived ->
|
||||||
EnterTanDialog(tanChallengeReceived) {
|
EnterTanDialog(tanChallengeReceived) {
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -31,13 +31,13 @@ fun UiSettings(modifier: Modifier, textColor: Color = Color.Unspecified) {
|
||||||
|
|
||||||
|
|
||||||
Column(modifier) {
|
Column(modifier) {
|
||||||
BooleanOption("Kontostand anzeigen", showBalance, textColor = textColor) { uiSettings.showBalance.value = it }
|
BooleanOption("Kontostand anzeigen", showBalance, textColor = textColor) { uiSettings.showBalance.value = it }
|
||||||
|
|
||||||
BooleanOption("Umsätze in alternierenden Farben anzeigen", showTransactionsInAlternatingColors, textColor = textColor) { uiSettings.showTransactionsInAlternatingColors.value = it }
|
BooleanOption("Umsätze in alternierenden Farben anzeigen", showTransactionsInAlternatingColors, textColor = textColor) { uiSettings.showTransactionsInAlternatingColors.value = it }
|
||||||
|
|
||||||
BooleanOption("Bank Icons anzeigen", showBankIcons, textColor = textColor) { uiSettings.showBankIcons.value = it }
|
BooleanOption("Bank Icons anzeigen", showBankIcons, textColor = textColor) { uiSettings.showBankIcons.value = it }
|
||||||
|
|
||||||
BooleanOption("Umsätze farbig anzeigen", showColoredAmounts, textColor = textColor) { uiSettings.showColoredAmounts.value = it }
|
BooleanOption("Umsätze farbig anzeigen", showColoredAmounts, textColor = textColor) { uiSettings.showColoredAmounts.value = it }
|
||||||
|
|
||||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text("Umsätze gruppieren", color = textColor)
|
Text("Umsätze gruppieren", color = textColor)
|
||||||
|
|
|
@ -3,14 +3,15 @@ package net.codinux.banking.ui.composables.text
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import net.codinux.banking.ui.config.Style
|
import net.codinux.banking.ui.config.Style
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HeaderText(title: String, modifier: Modifier = Modifier, textAlign: TextAlign = TextAlign.Start) {
|
fun HeaderText(title: String, modifier: Modifier = Modifier, textAlign: TextAlign = TextAlign.Start, textColor: Color = Style.HeaderTextColor) {
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
color = Style.HeaderTextColor,
|
color = textColor,
|
||||||
fontSize = Style.HeaderFontSize,
|
fontSize = Style.HeaderFontSize,
|
||||||
fontWeight = Style.HeaderFontWeight,
|
fontWeight = Style.HeaderFontWeight,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
|
@ -43,6 +43,10 @@ object DI {
|
||||||
|
|
||||||
fun setRepository(repository: BankingRepository) {
|
fun setRepository(repository: BankingRepository) {
|
||||||
this.bankingRepository = repository
|
this.bankingRepository = repository
|
||||||
|
|
||||||
|
repository.getAppSettings()?.let { // otherwise it's the first app start, BankingService will take care of this case
|
||||||
|
uiState.appSettings.value = it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package net.codinux.banking.ui.config
|
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.client.model.tan.ActionRequiringTan
|
||||||
import net.codinux.banking.ui.model.TransactionsGrouping
|
import net.codinux.banking.ui.model.TransactionsGrouping
|
||||||
|
import net.codinux.banking.ui.model.settings.AppAuthenticationMethod
|
||||||
|
|
||||||
object Internationalization {
|
object Internationalization {
|
||||||
|
|
||||||
|
@ -11,6 +13,8 @@ object Internationalization {
|
||||||
|
|
||||||
const val ErrorTransferMoney = "Überweisung konnte nicht ausgeführt werden"
|
const val ErrorTransferMoney = "Überweisung konnte nicht ausgeführt werden"
|
||||||
|
|
||||||
|
const val ErrorBiometricAuthentication = "Biometrische Authentifizierung fehlgeschlagen"
|
||||||
|
|
||||||
|
|
||||||
fun getTextForActionRequiringTan(action: ActionRequiringTan): String = when (action) {
|
fun getTextForActionRequiringTan(action: ActionRequiringTan): String = when (action) {
|
||||||
ActionRequiringTan.GetAnonymousBankInfo,
|
ActionRequiringTan.GetAnonymousBankInfo,
|
||||||
|
@ -30,4 +34,23 @@ object Internationalization {
|
||||||
TransactionsGrouping.None -> "Nicht gruppieren"
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -11,9 +11,10 @@ fun ApplicationErrorDialog(error: ApplicationError, onDismiss: (() -> Unit)? = n
|
||||||
ErroneousAction.AddAccount -> Internationalization.ErrorAddAccount
|
ErroneousAction.AddAccount -> Internationalization.ErrorAddAccount
|
||||||
ErroneousAction.UpdateAccountTransactions -> Internationalization.ErrorUpdateAccountTransactions
|
ErroneousAction.UpdateAccountTransactions -> Internationalization.ErrorUpdateAccountTransactions
|
||||||
ErroneousAction.TransferMoney -> Internationalization.ErrorTransferMoney
|
ErroneousAction.TransferMoney -> Internationalization.ErrorTransferMoney
|
||||||
|
ErroneousAction.BiometricAuthentication -> Internationalization.ErrorBiometricAuthentication
|
||||||
}
|
}
|
||||||
|
|
||||||
// add exception stacktrace?
|
// add exception stacktrace?
|
||||||
|
|
||||||
ErrorDialog(error.errorMessage, title, onDismiss = onDismiss)
|
ErrorDialog(error.errorMessage, title, error.exception, onDismiss = onDismiss)
|
||||||
}
|
}
|
|
@ -7,23 +7,31 @@ import androidx.compose.material.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
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.composables.text.HeaderText
|
||||||
import net.codinux.banking.ui.config.Colors
|
import net.codinux.banking.ui.config.Colors
|
||||||
import net.codinux.banking.ui.config.Style
|
import net.codinux.banking.ui.extensions.verticalScroll
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ErrorDialog(
|
fun ErrorDialog(
|
||||||
text: String,
|
text: String,
|
||||||
title: String? = null,
|
title: String? = null,
|
||||||
|
exception: Throwable? = null,
|
||||||
confirmButtonText: String = "OK",
|
confirmButtonText: String = "OK",
|
||||||
onDismiss: (() -> Unit)? = null
|
onDismiss: (() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
val effectiveText = if (exception == null) text else {
|
||||||
|
"$text\r\n\r\nFehlermeldung:\r\n${exception.stackTraceToString()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
text = { Text(text) },
|
text = { Text(effectiveText, Modifier.verticalScroll()) },
|
||||||
title = { title?.let {
|
title = { title?.let {
|
||||||
HeaderText(title, Modifier.fillMaxWidth(), TextAlign.Center)
|
HeaderText(title, Modifier.fillMaxWidth(), TextAlign.Center)
|
||||||
} },
|
} },
|
||||||
|
properties = if (exception == null) DialogProperties() else DialogProperties(usePlatformDefaultWidth = false),
|
||||||
onDismissRequest = { onDismiss?.invoke() },
|
onDismissRequest = { onDismiss?.invoke() },
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package net.codinux.banking.ui.forms
|
package net.codinux.banking.ui.forms
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
|
@ -9,18 +10,42 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import net.codinux.banking.ui.config.Colors
|
import net.codinux.banking.ui.config.Colors
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FormListItem(label: String, isSelectable: Boolean = false, isSelected: Boolean = false, selectedIconContentDescription: String? = null) {
|
fun FormListItem(label: String, itemHeight: Dp = 32.dp, onClick: (() -> Unit)? = null) {
|
||||||
|
FormListItemImpl(label, itemHeight = itemHeight, onClick = onClick)
|
||||||
|
}
|
||||||
|
|
||||||
Row(Modifier.fillMaxWidth().height(32.dp).padding(4.dp), verticalAlignment = Alignment.CenterVertically) {
|
@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) {
|
if (isSelectable) {
|
||||||
Column(Modifier.size(24.dp).padding(end = 8.dp)) {
|
Column(Modifier.padding(end = 8.dp).size(24.dp)) {
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
Icon(Icons.Outlined.Check, selectedIconContentDescription ?: "Item ist ausgewählt")
|
Icon(Icons.Outlined.Check, selectedIconContentDescription ?: "Item ist ausgewählt", tint = Colors.FormListItemTextColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,16 @@ import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Composable // try BasicSecureTextField
|
@Composable // try BasicSecureTextField
|
||||||
fun PasswordTextField(password: String = "", label: String = "Passwort", forceHidePassword: Boolean? = null, onEnterPressed: (() -> Unit)? = null, onChange: (String) -> Unit) {
|
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
|
||||||
|
) {
|
||||||
|
|
||||||
var passwordVisible by remember { mutableStateOf(false) }
|
var passwordVisible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
@ -29,7 +38,8 @@ fun PasswordTextField(password: String = "", label: String = "Passwort", forceHi
|
||||||
value = password,
|
value = password,
|
||||||
onValueChange = { onChange(it) },
|
onValueChange = { onChange(it) },
|
||||||
label = { Text(label) },
|
label = { Text(label) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
isError = isError,
|
||||||
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
val visibilityIcon = if (passwordVisible) {
|
val visibilityIcon = if (passwordVisible) {
|
||||||
|
@ -43,7 +53,7 @@ fun PasswordTextField(password: String = "", label: String = "Passwort", forceHi
|
||||||
modifier = Modifier.size(24.dp).clickable { passwordVisible = !passwordVisible }
|
modifier = Modifier.size(24.dp).clickable { passwordVisible = !passwordVisible }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
keyboardOptions = keyboardOptions?.copy(keyboardType = KeyboardType.Password) ?: KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||||
onEnterPressed = onEnterPressed
|
onEnterPressed = onEnterPressed
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -5,5 +5,7 @@ enum class ErroneousAction {
|
||||||
|
|
||||||
UpdateAccountTransactions,
|
UpdateAccountTransactions,
|
||||||
|
|
||||||
TransferMoney
|
TransferMoney,
|
||||||
|
|
||||||
|
BiometricAuthentication
|
||||||
}
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
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 net.codinux.banking.dataaccess.entities.BankAccountEntity
|
||||||
|
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 selectedIncludeInAutomaticAccountsUpdate by remember { mutableStateOf(account.includeInAutomaticAccountsUpdate) }
|
||||||
|
|
||||||
|
var selectedHideAccount by remember { mutableStateOf(account.hideAccount) }
|
||||||
|
|
||||||
|
val hasDataChanged by remember(enteredAccountName) {
|
||||||
|
mutableStateOf(
|
||||||
|
enteredAccountName != account.displayName
|
||||||
|
|| selectedIncludeInAutomaticAccountsUpdate != account.includeInAutomaticAccountsUpdate
|
||||||
|
|| selectedHideAccount != account.hideAccount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
FullscreenViewBase(account.displayName, onClosed = onClosed) {
|
||||||
|
Column(Modifier.fillMaxSize().verticalScroll().padding(8.dp)) {
|
||||||
|
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", 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import net.codinux.banking.dataaccess.entities.BankAccessEntity
|
import net.codinux.banking.dataaccess.entities.BankAccessEntity
|
||||||
|
import net.codinux.banking.ui.config.DI
|
||||||
import net.codinux.banking.ui.extensions.verticalScroll
|
import net.codinux.banking.ui.extensions.verticalScroll
|
||||||
import net.codinux.banking.ui.forms.*
|
import net.codinux.banking.ui.forms.*
|
||||||
|
|
||||||
|
@ -56,7 +57,9 @@ fun BankSettingsScreen(bank: BankAccessEntity, onClosed: () -> Unit) {
|
||||||
|
|
||||||
Column(Modifier.padding(top = 8.dp)) {
|
Column(Modifier.padding(top = 8.dp)) {
|
||||||
bank.accounts.sortedBy { it.displayIndex }.forEach { account ->
|
bank.accounts.sortedBy { it.displayIndex }.forEach { account ->
|
||||||
FormListItem(account.displayName)
|
FormListItem(account.displayName, itemHeight = 42.dp) {
|
||||||
|
DI.uiState.showBankAccountSettingsScreenForAccount.value = account
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,7 +81,7 @@ fun BankSettingsScreen(bank: BankAccessEntity, onClosed: () -> Unit) {
|
||||||
|
|
||||||
Column(Modifier.padding(top = 8.dp)) {
|
Column(Modifier.padding(top = 8.dp)) {
|
||||||
bank.tanMethods.sortedBy { it.identifier }.forEach { tanMethod ->
|
bank.tanMethods.sortedBy { it.identifier }.forEach { tanMethod ->
|
||||||
FormListItem(tanMethod.displayName, true, tanMethod == bank.selectedTanMethod, "TAN Verfahren ist ausgewähltes TAN Verfahren")
|
SelectableFormListItem(tanMethod.displayName, tanMethod == bank.selectedTanMethod, "TAN Verfahren ist ausgewähltes TAN Verfahren")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,7 +92,7 @@ fun BankSettingsScreen(bank: BankAccessEntity, onClosed: () -> Unit) {
|
||||||
|
|
||||||
Column(Modifier.padding(top = 8.dp)) {
|
Column(Modifier.padding(top = 8.dp)) {
|
||||||
bank.tanMedia.sortedBy { it.status }.forEach { tanMedium ->
|
bank.tanMedia.sortedBy { it.status }.forEach { tanMedium ->
|
||||||
FormListItem(tanMedium.displayName, true, tanMedium == bank.selectedTanMedium, "TAN Medium ist ausgewähltes TAN Medium")
|
SelectableFormListItem(tanMedium.displayName, tanMedium == bank.selectedTanMedium, "TAN Medium ist ausgewähltes TAN Medium")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,12 +16,14 @@ import androidx.compose.ui.zIndex
|
||||||
import net.codinux.banking.ui.composables.text.HeaderText
|
import net.codinux.banking.ui.composables.text.HeaderText
|
||||||
import net.codinux.banking.ui.config.Colors
|
import net.codinux.banking.ui.config.Colors
|
||||||
import net.codinux.banking.ui.config.DI
|
import net.codinux.banking.ui.config.DI
|
||||||
|
import net.codinux.banking.ui.config.Style
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FullscreenViewBase(
|
fun FullscreenViewBase(
|
||||||
title: String,
|
title: String,
|
||||||
confirmButtonTitle: String = "OK",
|
confirmButtonTitle: String = "OK",
|
||||||
confirmButtonEnabled: Boolean = true,
|
confirmButtonEnabled: Boolean = true,
|
||||||
|
showButtonBar: Boolean = true,
|
||||||
onClosed: () -> Unit,
|
onClosed: () -> Unit,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
|
@ -32,7 +34,7 @@ fun FullscreenViewBase(
|
||||||
Column(Modifier.fillMaxSize().zIndex(1000f).background(Color.White).padding(8.dp)) {
|
Column(Modifier.fillMaxSize().zIndex(1000f).background(Color.White).padding(8.dp)) {
|
||||||
|
|
||||||
Row(Modifier.fillMaxWidth()) {
|
Row(Modifier.fillMaxWidth()) {
|
||||||
HeaderText(title, Modifier.padding(top = 8.dp, bottom = 16.dp).weight(1f))
|
HeaderText(title, Modifier.padding(top = 8.dp, bottom = 16.dp).weight(1f), textColor = Style.ListItemHeaderTextColor)
|
||||||
|
|
||||||
if (DI.platform.isDesktop) {
|
if (DI.platform.isDesktop) {
|
||||||
TextButton(onClosed, colors = ButtonDefaults.buttonColors(contentColor = Colors.Zinc700, backgroundColor = Color.Transparent)) {
|
TextButton(onClosed, colors = ButtonDefaults.buttonColors(contentColor = Colors.Zinc700, backgroundColor = Color.Transparent)) {
|
||||||
|
@ -45,19 +47,21 @@ fun FullscreenViewBase(
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
if (showButtonBar) {
|
||||||
// TextButton(onClick = onClosed, Modifier.width(Style.DialogButtonWidth)) {
|
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||||
// Text("Abbrechen", color = Colors.CodinuxSecondaryColor)
|
// TextButton(onClick = onClosed, Modifier.width(Style.DialogButtonWidth)) {
|
||||||
// }
|
// Text("Abbrechen", color = Colors.CodinuxSecondaryColor)
|
||||||
|
// }
|
||||||
//
|
//
|
||||||
// Spacer(Modifier.width(8.dp))
|
// Spacer(Modifier.width(8.dp))
|
||||||
|
|
||||||
TextButton(
|
TextButton(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
enabled = confirmButtonEnabled,
|
enabled = confirmButtonEnabled,
|
||||||
onClick = { /* onConfirm?.invoke() ?: */ onClosed() }
|
onClick = { /* onConfirm?.invoke() ?: */ onClosed() }
|
||||||
) {
|
) {
|
||||||
Text(confirmButtonTitle, color = Colors.CodinuxSecondaryColor, textAlign = TextAlign.Center)
|
Text(confirmButtonTitle, color = Colors.CodinuxSecondaryColor, textAlign = TextAlign.Center)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
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,
|
||||||
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,125 @@
|
||||||
|
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.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(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()) {
|
||||||
|
if (currentAuthenticationMethod == AppAuthenticationMethod.None) {
|
||||||
|
Text("Appzugangsschutz 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(imeAction = ImeAction.Next)) { newPassword = it }
|
||||||
|
|
||||||
|
PasswordTextField(confirmedNewPassword, "Password bestätigen", Modifier.padding(top = 16.dp), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done)) { 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -25,7 +25,7 @@ class AccountTransactionsFilterService {
|
||||||
appliedAccountFilter = appliedAccountFilter.filter { it.valueDate.year == year && (month == null || it.valueDate.monthNumber == month) }
|
appliedAccountFilter = appliedAccountFilter.filter { it.valueDate.year == year && (month == null || it.valueDate.monthNumber == month) }
|
||||||
}
|
}
|
||||||
|
|
||||||
val searchTerms = filter.searchTerm.split(SearchTermOrSeparatorSymbol).filter { it.isNotBlank() }
|
val searchTerms = filter.searchTerm.split(SearchTermOrSeparatorSymbol).filter { it.isNotBlank() }.map { it.trim() }
|
||||||
return if (searchTerms.isEmpty()) {
|
return if (searchTerms.isEmpty()) {
|
||||||
appliedAccountFilter
|
appliedAccountFilter
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
package net.codinux.banking.ui.service
|
||||||
|
|
||||||
|
import net.codinux.banking.ui.config.DI
|
||||||
|
import net.codinux.banking.ui.model.AuthenticationResult
|
||||||
|
import net.codinux.banking.ui.model.error.ErroneousAction
|
||||||
|
import net.codinux.log.Log
|
||||||
|
|
||||||
|
expect object AuthenticationService {
|
||||||
|
|
||||||
|
fun hashPassword(password: String): String
|
||||||
|
|
||||||
|
fun checkPassword(password: String, hashedPassword: String): Boolean
|
||||||
|
|
||||||
|
|
||||||
|
val supportsBiometricAuthentication: Boolean
|
||||||
|
|
||||||
|
fun authenticateWithBiometrics(authenticationResult: (AuthenticationResult) -> Unit)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun AuthenticationService.safelyAuthenticateWithBiometrics(authenticationResult: (AuthenticationResult) -> Unit) {
|
||||||
|
try {
|
||||||
|
this.authenticateWithBiometrics(authenticationResult)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.error(e) { "Could not authenticate with Biometrics" }
|
||||||
|
DI.uiState.applicationErrorOccurred(ErroneousAction.BiometricAuthentication, "Beim Login mit Fingerabdruck / Gesichtsscan ist etwas fehlgeschlagen, was nicht fehlschlagen sollte. Die folgende Fehlermeldung wird Ihnen vermutlich nichts sagen. Am besten informieren Sie die Entwickler (die faulen Säcke), z. B. mit einem Screenshot dieser Fehlermeldung, damit diese sich darum kümmern.", e)
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import net.codinux.banking.client.model.tan.EnterTanResult
|
||||||
import net.codinux.banking.client.model.tan.TanChallenge
|
import net.codinux.banking.client.model.tan.TanChallenge
|
||||||
import net.codinux.banking.dataaccess.entities.HoldingEntity
|
import net.codinux.banking.dataaccess.entities.HoldingEntity
|
||||||
import net.codinux.banking.dataaccess.entities.BankAccessEntity
|
import net.codinux.banking.dataaccess.entities.BankAccessEntity
|
||||||
|
import net.codinux.banking.dataaccess.entities.BankAccountEntity
|
||||||
import net.codinux.banking.ui.model.*
|
import net.codinux.banking.ui.model.*
|
||||||
import net.codinux.banking.ui.model.error.ApplicationError
|
import net.codinux.banking.ui.model.error.ApplicationError
|
||||||
import net.codinux.banking.ui.model.error.BankingClientError
|
import net.codinux.banking.ui.model.error.BankingClientError
|
||||||
|
@ -58,8 +59,12 @@ class UiState : ViewModel() {
|
||||||
|
|
||||||
val showBankSettingsScreenForBank = MutableStateFlow<BankAccessEntity?>(null)
|
val showBankSettingsScreenForBank = MutableStateFlow<BankAccessEntity?>(null)
|
||||||
|
|
||||||
|
val showBankAccountSettingsScreenForAccount = MutableStateFlow<BankAccountEntity?>(null)
|
||||||
|
|
||||||
val showExportScreen = MutableStateFlow(false)
|
val showExportScreen = MutableStateFlow(false)
|
||||||
|
|
||||||
|
val showProtectAppSettingsScreen = MutableStateFlow(false)
|
||||||
|
|
||||||
|
|
||||||
val tanChallengeReceived = MutableStateFlow<TanChallengeReceived?>(null)
|
val tanChallengeReceived = MutableStateFlow<TanChallengeReceived?>(null)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
package net.codinux.banking.ui.service
|
||||||
|
|
||||||
|
import kotlinx.datetime.*
|
||||||
|
import net.codinux.banking.client.model.Amount
|
||||||
|
import net.codinux.banking.client.model.DefaultValues
|
||||||
|
import net.codinux.banking.client.model.extensions.EuropeBerlin
|
||||||
|
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
||||||
|
import net.codinux.banking.ui.model.AccountTransactionsFilter
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class AccountTransactionsFilterServiceTest {
|
||||||
|
|
||||||
|
private val underTest = AccountTransactionsFilterService()
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun filterAccounts_TermsSeparatedByComma_WhitespaceGetsFilteredOut() {
|
||||||
|
// before whitespace after comma led to that for " rewe" was search which yielded no results (as opposed to "rewe") -> ensure whitespace gets remove
|
||||||
|
val filter = filter("edeka, rewe")
|
||||||
|
|
||||||
|
val result = underTest.filterAccounts(listOf(transaction("Edeka"), transaction("Rewe")), filter)
|
||||||
|
|
||||||
|
assertEquals(2, result.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun filter(searchTerm: String = "") = AccountTransactionsFilter().apply {
|
||||||
|
this.updateSearchTerm(searchTerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun transaction(reference: String? = null, otherPartyName: String? = null, valueDate: LocalDate = Clock.System.todayIn(TimeZone.EuropeBerlin)) = AccountTransactionViewModel(
|
||||||
|
-1, -1, -1, Amount("0"), DefaultValues.DefaultCurrency, reference, valueDate, otherPartyName
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package net.codinux.banking.ui.service
|
||||||
|
|
||||||
|
import at.favre.lib.crypto.bcrypt.BCrypt
|
||||||
|
import net.codinux.banking.ui.model.AuthenticationResult
|
||||||
|
|
||||||
|
actual object AuthenticationService {
|
||||||
|
|
||||||
|
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 = false
|
||||||
|
|
||||||
|
actual fun authenticateWithBiometrics(authenticationResult: (AuthenticationResult) -> Unit) {
|
||||||
|
authenticationResult(AuthenticationResult(false, "Biometrics is not supported"))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package net.codinux.banking.ui.service
|
||||||
|
|
||||||
|
import net.codinux.banking.ui.model.AuthenticationResult
|
||||||
|
|
||||||
|
actual object AuthenticationService {
|
||||||
|
|
||||||
|
// for iOS see e.g.
|
||||||
|
// https://medium.com/@mohamed.ma872/strengthening-mobile-app-security-pbkdf2-bcrypt-and-scrypt-for-android-and-ios-8089b0edbf76
|
||||||
|
// https://github.com/felipeflorencio/BCryptSwift
|
||||||
|
|
||||||
|
actual fun hashPassword(password: String): String {
|
||||||
|
return password // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun checkPassword(password: String, hashedPassword: String): Boolean {
|
||||||
|
return password == hashedPassword // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
actual val supportsBiometricAuthentication = false // TODO
|
||||||
|
|
||||||
|
actual fun authenticateWithBiometrics(authenticationResult: (AuthenticationResult) -> Unit) {
|
||||||
|
authenticationResult(AuthenticationResult(false, "Biometrics is not implemented yet"))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package net.codinux.banking.ui.service
|
||||||
|
|
||||||
|
import net.codinux.banking.ui.model.AuthenticationResult
|
||||||
|
|
||||||
|
actual object AuthenticationService {
|
||||||
|
|
||||||
|
actual fun hashPassword(password: String): String {
|
||||||
|
return password // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun checkPassword(password: String, hashedPassword: String): Boolean {
|
||||||
|
return password == hashedPassword // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
actual val supportsBiometricAuthentication = false
|
||||||
|
|
||||||
|
actual fun authenticateWithBiometrics(authenticationResult: (AuthenticationResult) -> Unit) {
|
||||||
|
authenticationResult(AuthenticationResult(false, "Biometrics is not supported"))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -7,6 +7,8 @@ banking-client = "0.6.0"
|
||||||
kcsv = "2.2.0"
|
kcsv = "2.2.0"
|
||||||
kotlinx-serializable = "1.7.1"
|
kotlinx-serializable = "1.7.1"
|
||||||
|
|
||||||
|
favre-bcrypt = "0.10.2"
|
||||||
|
|
||||||
klf = "1.6.1"
|
klf = "1.6.1"
|
||||||
logback = "1.5.7"
|
logback = "1.5.7"
|
||||||
|
|
||||||
|
@ -23,6 +25,7 @@ androidx-core-ktx = "1.13.1"
|
||||||
androidx-espresso-core = "3.6.1"
|
androidx-espresso-core = "3.6.1"
|
||||||
androidx-lifecycle = "2.8.0"
|
androidx-lifecycle = "2.8.0"
|
||||||
androidx-material = "1.12.0"
|
androidx-material = "1.12.0"
|
||||||
|
androidx-biometric = "1.1.0"
|
||||||
androidx-test-junit = "1.2.1"
|
androidx-test-junit = "1.2.1"
|
||||||
compose-plugin = "1.6.11"
|
compose-plugin = "1.6.11"
|
||||||
|
|
||||||
|
@ -36,6 +39,8 @@ kcsv = { group = "net.codinux.csv", name = "kcsv", version.ref = "kcsv" }
|
||||||
coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
|
coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
|
||||||
kotlinx-serializable = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serializable" }
|
kotlinx-serializable = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serializable" }
|
||||||
|
|
||||||
|
favre-bcrypt = { group = "at.favre.lib", name = "bcrypt", version.ref = "favre-bcrypt" }
|
||||||
|
|
||||||
klf = { group = "net.codinux.log", name = "klf", version.ref = "klf" }
|
klf = { group = "net.codinux.log", name = "klf", version.ref = "klf" }
|
||||||
logback = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" }
|
logback = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" }
|
||||||
|
|
||||||
|
@ -55,6 +60,7 @@ androidx-constraintlayout = { group = "androidx.constraintlayout", name = "const
|
||||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
|
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
|
||||||
androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
|
androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
|
||||||
androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
||||||
|
androidx-biometric = { group = "androidx.biometric", name = "biometric", version.ref = "androidx-biometric" }
|
||||||
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
||||||
|
|
||||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
||||||
|
|
Loading…
Reference in New Issue