Compare commits
No commits in common. "5e7d8804992f671c4d7232970874739bfe9fd96b" and "345f84c0b201f6538db52ca10414e6723473ab20" have entirely different histories.
5e7d880499
...
345f84c0b2
|
@ -1,12 +1,9 @@
|
||||||
.idea/
|
.idea/
|
||||||
*.iml
|
*.iml
|
||||||
|
|
||||||
build/
|
|
||||||
**/build
|
**/build
|
||||||
out/
|
|
||||||
**/out
|
**/out
|
||||||
/captures
|
**/target
|
||||||
**/kotlin-js-store
|
|
||||||
|
|
||||||
.gradle
|
.gradle
|
||||||
.kotlin
|
.kotlin
|
||||||
|
@ -18,17 +15,3 @@ out/
|
||||||
|
|
||||||
/data/
|
/data/
|
||||||
**/*.log
|
**/*.log
|
||||||
|
|
||||||
docs/received_messages
|
|
||||||
|
|
||||||
keys.gradle
|
|
||||||
|
|
||||||
|
|
||||||
# Xcode
|
|
||||||
xcuserdata/
|
|
||||||
*.xcworkspace
|
|
||||||
|
|
||||||
*.ipa
|
|
||||||
*.dSYM.zip
|
|
||||||
*.dSYM
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "gradle/scripts"]
|
||||||
|
path = gradle/scripts
|
||||||
|
url = git@github.com:dankito/GradleScripts.git
|
|
@ -0,0 +1,108 @@
|
||||||
|
@file:OptIn(ExperimentalWasmDsl::class)
|
||||||
|
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
kotlin("multiplatform")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(8)
|
||||||
|
|
||||||
|
jvm {
|
||||||
|
withJava()
|
||||||
|
|
||||||
|
testRuns["test"].executionTask.configure {
|
||||||
|
useJUnitPlatform()
|
||||||
|
|
||||||
|
testLogging {
|
||||||
|
showExceptions = true
|
||||||
|
showStandardStreams = true
|
||||||
|
events("passed", "skipped", "failed")
|
||||||
|
// exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
js {
|
||||||
|
moduleName = "banking-client"
|
||||||
|
binaries.executable()
|
||||||
|
|
||||||
|
browser {
|
||||||
|
testTask {
|
||||||
|
useKarma {
|
||||||
|
useChromeHeadless()
|
||||||
|
useFirefoxHeadless()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nodejs {
|
||||||
|
testTask {
|
||||||
|
useMocha {
|
||||||
|
timeout = "20s" // Mocha times out after 2 s, which is too short for bufferExceeded() test
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wasmJs()
|
||||||
|
|
||||||
|
|
||||||
|
linuxX64()
|
||||||
|
mingwX64()
|
||||||
|
|
||||||
|
iosArm64()
|
||||||
|
iosSimulatorArm64()
|
||||||
|
macosX64()
|
||||||
|
macosArm64()
|
||||||
|
watchosArm64()
|
||||||
|
watchosSimulatorArm64()
|
||||||
|
tvosArm64()
|
||||||
|
tvosSimulatorArm64()
|
||||||
|
|
||||||
|
applyDefaultHierarchyTemplate()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
val coroutinesVersion: String by project
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
api(project(":BankingClientModel"))
|
||||||
|
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
commonTest {
|
||||||
|
dependencies {
|
||||||
|
implementation(kotlin("test"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jvmMain {
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jvmTest { }
|
||||||
|
|
||||||
|
jsMain {
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jsTest { }
|
||||||
|
|
||||||
|
nativeMain { }
|
||||||
|
nativeTest { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ext["customArtifactId"] = "banking-client"
|
||||||
|
|
||||||
|
apply(from = "../gradle/scripts/publish-codinux.gradle.kts")
|
|
@ -0,0 +1,14 @@
|
||||||
|
package net.codinux.banking.client
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.request.GetAccountDataRequest
|
||||||
|
import net.codinux.banking.client.model.response.GetAccountDataResponse
|
||||||
|
import net.codinux.banking.client.model.response.Response
|
||||||
|
|
||||||
|
interface BankingClient {
|
||||||
|
|
||||||
|
suspend fun getAccountDataAsync(bankCode: String, loginName: String, password: String) =
|
||||||
|
getAccountDataAsync(GetAccountDataRequest(bankCode, loginName, password))
|
||||||
|
|
||||||
|
suspend fun getAccountDataAsync(request: GetAccountDataRequest): Response<GetAccountDataResponse>
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package net.codinux.banking.client
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.tan.EnterTanResult
|
||||||
|
import net.codinux.banking.client.model.tan.TanChallenge
|
||||||
|
|
||||||
|
interface BankingClientCallback {
|
||||||
|
|
||||||
|
fun enterTan(tanChallenge: TanChallenge, callback: (EnterTanResult) -> Unit)
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package net.codinux.banking.client
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.options.GetAccountDataOptions
|
||||||
|
import net.codinux.banking.client.model.response.GetAccountDataResponse
|
||||||
|
import net.codinux.banking.client.model.response.Response
|
||||||
|
|
||||||
|
interface BankingClientForCustomer {
|
||||||
|
|
||||||
|
// for languages not supporting default parameters (Java, Swift, JS, ...)
|
||||||
|
suspend fun getAccountDataAsync() = getAccountDataAsync(GetAccountDataOptions())
|
||||||
|
|
||||||
|
suspend fun getAccountDataAsync(options: GetAccountDataOptions): Response<GetAccountDataResponse>
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package net.codinux.banking.client
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.AccountCredentials
|
||||||
|
import net.codinux.banking.client.model.options.GetAccountDataOptions
|
||||||
|
import net.codinux.banking.client.model.request.GetAccountDataRequest
|
||||||
|
|
||||||
|
abstract class BankingClientForCustomerBase(
|
||||||
|
protected val credentials: AccountCredentials,
|
||||||
|
protected val client: BankingClient
|
||||||
|
) : BankingClientForCustomer {
|
||||||
|
|
||||||
|
override suspend fun getAccountDataAsync(options: GetAccountDataOptions) =
|
||||||
|
client.getAccountDataAsync(GetAccountDataRequest(credentials, options))
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package net.codinux.banking.client
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.request.GetAccountDataRequest
|
||||||
|
import net.codinux.banking.client.model.response.GetAccountDataResponse
|
||||||
|
import net.codinux.banking.client.model.response.Response
|
||||||
|
|
||||||
|
interface BlockingBankingClient {
|
||||||
|
|
||||||
|
suspend fun getAccountData(bankCode: String, loginName: String, password: String) =
|
||||||
|
getAccountData(GetAccountDataRequest(bankCode, loginName, password))
|
||||||
|
|
||||||
|
fun getAccountData(request: GetAccountDataRequest): Response<GetAccountDataResponse>
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package net.codinux.banking.client
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.options.GetAccountDataOptions
|
||||||
|
import net.codinux.banking.client.model.response.GetAccountDataResponse
|
||||||
|
import net.codinux.banking.client.model.response.Response
|
||||||
|
|
||||||
|
interface BlockingBankingClientForCustomer {
|
||||||
|
|
||||||
|
// for languages not supporting default parameters (Java, Swift, JS, ...)
|
||||||
|
fun getAccountData() = getAccountData(GetAccountDataOptions())
|
||||||
|
|
||||||
|
fun getAccountData(options: GetAccountDataOptions): Response<GetAccountDataResponse>
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package net.codinux.banking.client
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.AccountCredentials
|
||||||
|
import net.codinux.banking.client.model.options.GetAccountDataOptions
|
||||||
|
import net.codinux.banking.client.model.request.GetAccountDataRequest
|
||||||
|
|
||||||
|
abstract class BlockingBankingClientForCustomerBase(
|
||||||
|
protected val credentials: AccountCredentials,
|
||||||
|
protected val client: BlockingBankingClient
|
||||||
|
) : BlockingBankingClientForCustomer {
|
||||||
|
|
||||||
|
override fun getAccountData(options: GetAccountDataOptions) =
|
||||||
|
client.getAccountData(GetAccountDataRequest(credentials, options))
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package net.codinux.banking.client
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.tan.EnterTanResult
|
||||||
|
import net.codinux.banking.client.model.tan.TanChallenge
|
||||||
|
|
||||||
|
open class SimpleBankingClientCallback(
|
||||||
|
protected val enterTan: ((tanChallenge: TanChallenge, callback: (EnterTanResult) -> Unit) -> Unit)? = null
|
||||||
|
) : BankingClientCallback {
|
||||||
|
|
||||||
|
override fun enterTan(tanChallenge: TanChallenge, callback: (EnterTanResult) -> Unit) {
|
||||||
|
if (enterTan != null) {
|
||||||
|
enterTan.invoke(tanChallenge, callback)
|
||||||
|
} else {
|
||||||
|
callback(EnterTanResult(null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package net.codinux.banking.client
|
||||||
|
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import net.codinux.banking.client.model.options.GetAccountDataOptions
|
||||||
|
import net.codinux.banking.client.model.request.GetAccountDataRequest
|
||||||
|
|
||||||
|
fun BankingClient.getAccountData(bankCode: String, loginName: String, password: String) = runBlocking {
|
||||||
|
this@getAccountData.getAccountDataAsync(bankCode, loginName, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BankingClient.getAccountData(request: GetAccountDataRequest) = runBlocking {
|
||||||
|
this@getAccountData.getAccountDataAsync(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BankingClientForCustomer.getAccountData() = runBlocking {
|
||||||
|
this@getAccountData.getAccountDataAsync()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BankingClientForCustomer.getAccountData(options: GetAccountDataOptions) = runBlocking {
|
||||||
|
this@getAccountData.getAccountDataAsync(options)
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package net.codinux.banking.client
|
||||||
|
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import net.codinux.banking.client.model.options.GetAccountDataOptions
|
||||||
|
import net.codinux.banking.client.model.request.GetAccountDataRequest
|
||||||
|
|
||||||
|
fun BankingClient.getAccountData(bankCode: String, loginName: String, password: String) = runBlocking {
|
||||||
|
this@getAccountData.getAccountDataAsync(bankCode, loginName, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BankingClient.getAccountData(request: GetAccountDataRequest) = runBlocking {
|
||||||
|
this@getAccountData.getAccountDataAsync(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BankingClientForCustomer.getAccountData() = runBlocking {
|
||||||
|
this@getAccountData.getAccountDataAsync()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BankingClientForCustomer.getAccountData(options: GetAccountDataOptions) = runBlocking {
|
||||||
|
this@getAccountData.getAccountDataAsync(options)
|
||||||
|
}
|
|
@ -0,0 +1,122 @@
|
||||||
|
@file:OptIn(ExperimentalWasmDsl::class)
|
||||||
|
|
||||||
|
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
kotlin("multiplatform")
|
||||||
|
kotlin("plugin.noarg")
|
||||||
|
// kotlin("plugin.serialization")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(8)
|
||||||
|
|
||||||
|
@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")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
jvm {
|
||||||
|
withJava()
|
||||||
|
|
||||||
|
testRuns["test"].executionTask.configure {
|
||||||
|
useJUnitPlatform()
|
||||||
|
|
||||||
|
testLogging {
|
||||||
|
showExceptions = true
|
||||||
|
showStandardStreams = true
|
||||||
|
events("passed", "skipped", "failed")
|
||||||
|
// exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
js {
|
||||||
|
moduleName = "banking-client-model"
|
||||||
|
binaries.executable()
|
||||||
|
|
||||||
|
browser {
|
||||||
|
testTask {
|
||||||
|
useKarma {
|
||||||
|
useChromeHeadless()
|
||||||
|
useFirefoxHeadless()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nodejs {
|
||||||
|
testTask {
|
||||||
|
useMocha {
|
||||||
|
timeout = "20s" // Mocha times out after 2 s, which is too short for bufferExceeded() test
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wasmJs()
|
||||||
|
|
||||||
|
|
||||||
|
linuxX64()
|
||||||
|
mingwX64()
|
||||||
|
|
||||||
|
iosArm64()
|
||||||
|
iosSimulatorArm64()
|
||||||
|
macosX64()
|
||||||
|
macosArm64()
|
||||||
|
watchosArm64()
|
||||||
|
watchosSimulatorArm64()
|
||||||
|
tvosArm64()
|
||||||
|
tvosSimulatorArm64()
|
||||||
|
|
||||||
|
applyDefaultHierarchyTemplate()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
val kotlinxDateTimeVersion: String by project
|
||||||
|
val jsJodaTimeZoneVersion: String by project
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
api("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDateTimeVersion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
commonTest {
|
||||||
|
dependencies {
|
||||||
|
implementation(kotlin("test"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jvmMain {
|
||||||
|
dependencies {
|
||||||
|
compileOnly("com.fasterxml.jackson.core:jackson-annotations:2.15.0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jvmTest { }
|
||||||
|
|
||||||
|
jsMain {
|
||||||
|
dependencies {
|
||||||
|
api(npm("@js-joda/timezone", jsJodaTimeZoneVersion))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jsTest { }
|
||||||
|
|
||||||
|
nativeMain { }
|
||||||
|
nativeTest { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
noArg {
|
||||||
|
annotation("net.codinux.accounting.common.config.NoArgConstructor")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ext["customArtifactId"] = "banking-client-model"
|
||||||
|
|
||||||
|
apply(from = "../gradle/scripts/publish-codinux.gradle.kts")
|
|
@ -0,0 +1,12 @@
|
||||||
|
package net.codinux.banking.client.model
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
|
||||||
|
@NoArgConstructor
|
||||||
|
open class AccountCredentials(
|
||||||
|
val bankCode: String,
|
||||||
|
val loginName: String,
|
||||||
|
val password: String
|
||||||
|
) {
|
||||||
|
override fun toString() = "$bankCode $loginName"
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
package net.codinux.banking.client.model
|
||||||
|
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
|
||||||
|
@NoArgConstructor
|
||||||
|
open class AccountTransaction(
|
||||||
|
val amount: Amount = Amount.Zero,
|
||||||
|
val currency: String,
|
||||||
|
val reference: String, // Alternative: purpose (or Remittance information)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transaction date (Buchungstag) - der Tag, an dem ein Zahlungsvorgang in das System einer Bank eingegangen ist.
|
||||||
|
* Das bedeutet aber nicht automatisch, dass das Geld schon verfügbar ist. Dafür ist die Wertstellung entscheidend.
|
||||||
|
*/
|
||||||
|
val bookingDate: LocalDate,
|
||||||
|
/**
|
||||||
|
* Effective date (Wertstellung / Valutadatum) - der Tag an dem das Geld verfügbar ist. An diesem Tag wird die
|
||||||
|
* Kontobewegung wirksam.
|
||||||
|
*
|
||||||
|
* Buchung und Wertstellung erfolgen häufig am gleichen Tag, das muss aber nicht immer der Fall sein.
|
||||||
|
*/
|
||||||
|
val valueDate: LocalDate,
|
||||||
|
|
||||||
|
val otherPartyName: String? = null, // Alternatives: Parties involved, Transaction parties.single names: Beneficiary, Payee respectively Payer, Debtor
|
||||||
|
val otherPartyBankCode: String? = null,
|
||||||
|
val otherPartyAccountId: String? = null,
|
||||||
|
|
||||||
|
val bookingText: String? = null,
|
||||||
|
val information: String? = null,
|
||||||
|
|
||||||
|
val statementNumber: Int? = null,
|
||||||
|
val sequenceNumber: Int? = null,
|
||||||
|
|
||||||
|
val openingBalance: Amount? = null,
|
||||||
|
val closingBalance: Amount? = null,
|
||||||
|
|
||||||
|
val endToEndReference: String? = null,
|
||||||
|
val customerReference: String? = null,
|
||||||
|
val mandateReference: String? = null,
|
||||||
|
val creditorIdentifier: String? = null,
|
||||||
|
val originatorsIdentificationCode: String? = null,
|
||||||
|
val compensationAmount: String? = null,
|
||||||
|
val originalAmount: String? = null,
|
||||||
|
val sepaReference: String? = null,
|
||||||
|
val deviantOriginator: String? = null,
|
||||||
|
val deviantRecipient: String? = null,
|
||||||
|
val referenceWithNoSpecialType: String? = null,
|
||||||
|
val primaNotaNumber: String? = null,
|
||||||
|
val textKeySupplement: String? = null,
|
||||||
|
|
||||||
|
val currencyType: String? = null,
|
||||||
|
val bookingKey: String? = null,
|
||||||
|
val referenceForTheAccountOwner: String? = null,
|
||||||
|
val referenceOfTheAccountServicingInstitution: String? = null,
|
||||||
|
val supplementaryDetails: String? = null,
|
||||||
|
|
||||||
|
val transactionReferenceNumber: String? = null,
|
||||||
|
val relatedReferenceNumber: String? = null,
|
||||||
|
|
||||||
|
var userSetDisplayName: String? = null,
|
||||||
|
var notes: String? = null,
|
||||||
|
) {
|
||||||
|
override fun toString() = "${valueDate.dayOfMonth}.${valueDate.monthNumber}.${valueDate.year} ${amount.toString().padStart(4, ' ')} ${if (currency == "EUR") "€" else currency} ${otherPartyName ?: ""} - $reference"
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package net.codinux.banking.client.model
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
import kotlin.jvm.JvmInline
|
||||||
|
|
||||||
|
@JvmInline
|
||||||
|
@NoArgConstructor
|
||||||
|
value class Amount(val amount: String = "0") {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val Zero = Amount("0")
|
||||||
|
|
||||||
|
fun fromString(amount: String): Amount = Amount(amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun toString() = amount
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package net.codinux.banking.client.model
|
||||||
|
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
|
||||||
|
@NoArgConstructor
|
||||||
|
open class BankAccount(
|
||||||
|
val identifier: String,
|
||||||
|
var accountHolderName: String,
|
||||||
|
val type: BankAccountType = BankAccountType.Other,
|
||||||
|
val iban: String? = null,
|
||||||
|
val subAccountNumber: String? = null,
|
||||||
|
val productName: String? = null,
|
||||||
|
val currency: String = "EUR",
|
||||||
|
var accountLimit: String? = null,
|
||||||
|
|
||||||
|
val isAccountTypeSupportedByApplication: Boolean = true,
|
||||||
|
val features: Set<BankAccountFeatures> = emptySet(),
|
||||||
|
|
||||||
|
// var balance: BigDecimal = BigDecimal.ZERO,
|
||||||
|
var balance: Amount = Amount.Zero, // TODO: add a BigDecimal library
|
||||||
|
var retrievedTransactionsFrom: LocalDate? = null,
|
||||||
|
var retrievedTransactionsTo: LocalDate? = null,
|
||||||
|
|
||||||
|
var haveAllTransactionsBeenRetrieved: Boolean = false,
|
||||||
|
val countDaysForWhichTransactionsAreKept: Int? = null,
|
||||||
|
|
||||||
|
val bookedTransactions: MutableList<AccountTransaction> = mutableListOf(),
|
||||||
|
val unbookedTransactions: MutableList<UnbookedAccountTransaction> = mutableListOf(),
|
||||||
|
|
||||||
|
var userSetDisplayName: String? = null,
|
||||||
|
var displayIndex: Int = 0,
|
||||||
|
|
||||||
|
var hideAccount: Boolean = false,
|
||||||
|
var includeInAutomaticAccountsUpdate: Boolean = true
|
||||||
|
) {
|
||||||
|
override fun toString() = "$type $identifier $productName (IBAN: $iban)"
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package net.codinux.banking.client.model
|
||||||
|
|
||||||
|
enum class BankAccountFeatures {
|
||||||
|
RetrieveTransactions,
|
||||||
|
RetrieveBalance,
|
||||||
|
TransferMoney,
|
||||||
|
InstantPayment
|
||||||
|
}
|
|
@ -1,8 +1,6 @@
|
||||||
package net.dankito.banking.client.model
|
package net.codinux.banking.client.model
|
||||||
|
|
||||||
|
|
||||||
enum class BankAccountType {
|
enum class BankAccountType {
|
||||||
|
|
||||||
CheckingAccount,
|
CheckingAccount,
|
||||||
|
|
||||||
SavingsAccount,
|
SavingsAccount,
|
||||||
|
@ -22,5 +20,4 @@ enum class BankAccountType {
|
||||||
InsuranceContract,
|
InsuranceContract,
|
||||||
|
|
||||||
Other
|
Other
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package net.codinux.banking.client.model
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains only the basic info of a [BankAccount], just enough that a client application can display it to the user
|
||||||
|
* and the user knows exactly which [BankAccount] is meant / referred.
|
||||||
|
*/
|
||||||
|
@NoArgConstructor
|
||||||
|
open class BankAccountViewInfo(
|
||||||
|
val identifier: String,
|
||||||
|
val subAccountNumber: String? = null,
|
||||||
|
val type: BankAccountType = BankAccountType.Other,
|
||||||
|
val iban: String? = null,
|
||||||
|
val productName: String? = null,
|
||||||
|
) {
|
||||||
|
override fun toString() = "$type $productName $identifier"
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package net.codinux.banking.client.model
|
||||||
|
|
||||||
|
enum class BankingGroup {
|
||||||
|
Sparkasse,
|
||||||
|
DKB,
|
||||||
|
OldenburgischeLandesbank,
|
||||||
|
|
||||||
|
VolksUndRaiffeisenbanken,
|
||||||
|
Sparda,
|
||||||
|
PSD,
|
||||||
|
GLS,
|
||||||
|
SonstigeGenossenschaftsbank,
|
||||||
|
|
||||||
|
DeutscheBank,
|
||||||
|
Postbank,
|
||||||
|
|
||||||
|
Commerzbank,
|
||||||
|
Comdirect,
|
||||||
|
|
||||||
|
Unicredit,
|
||||||
|
Targobank,
|
||||||
|
ING,
|
||||||
|
Santander,
|
||||||
|
Norisbank,
|
||||||
|
Degussa,
|
||||||
|
Oberbank,
|
||||||
|
Bundesbank,
|
||||||
|
KfW,
|
||||||
|
N26,
|
||||||
|
Consors
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package net.codinux.banking.client.model
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
|
||||||
|
@NoArgConstructor
|
||||||
|
open class CustomerAccount(
|
||||||
|
val bankCode: String,
|
||||||
|
var loginName: String,
|
||||||
|
/**
|
||||||
|
* User may decides to not save password .
|
||||||
|
*/
|
||||||
|
var password: String?,
|
||||||
|
|
||||||
|
val bankName: String,
|
||||||
|
val bic: String,
|
||||||
|
|
||||||
|
val customerName: String,
|
||||||
|
val userId: String = loginName,
|
||||||
|
|
||||||
|
val accounts: List<BankAccount> = emptyList(),
|
||||||
|
|
||||||
|
var bankingGroup: BankingGroup? = null,
|
||||||
|
var iconUrl: String? = null,
|
||||||
|
) {
|
||||||
|
|
||||||
|
var wrongCredentialsEntered: Boolean = false
|
||||||
|
|
||||||
|
var userSetDisplayName: String? = null
|
||||||
|
var displayIndex: Int = 0
|
||||||
|
|
||||||
|
|
||||||
|
override fun toString() = "$bankName $loginName, ${accounts.size} accounts"
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package net.codinux.banking.client.model
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains only the basic info of a [CustomerAccount], just enough that a client application can display it to the user
|
||||||
|
* and the user knows exactly which [CustomerAccount] is meant / referred.
|
||||||
|
*/
|
||||||
|
@NoArgConstructor
|
||||||
|
open class CustomerAccountViewInfo(
|
||||||
|
val bankCode: String,
|
||||||
|
var loginName: String,
|
||||||
|
val bankName: String
|
||||||
|
) {
|
||||||
|
override fun toString() = "$bankCode $bankName $loginName"
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package net.codinux.banking.client.model
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
|
||||||
|
@NoArgConstructor
|
||||||
|
open class UnbookedAccountTransaction {
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package net.codinux.banking.client.model.config
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation to be able to apply Jackson's @com.fasterxml.jackson.annotation.JsonIgnore in common module
|
||||||
|
*/
|
||||||
|
// match the target and retention settings of Jackson's JsonIgnore annotation
|
||||||
|
@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR, AnnotationTarget.FIELD, AnnotationTarget.PROPERTY_GETTER)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
expect annotation class JsonIgnore()
|
|
@ -0,0 +1,6 @@
|
||||||
|
package net.codinux.banking.client.model.config
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marker interface for Kotlin No-arg plugin so that No-arg plugin adds no-arg constructors to classes marked with this annotation.
|
||||||
|
*/
|
||||||
|
annotation class NoArgConstructor
|
|
@ -0,0 +1,17 @@
|
||||||
|
package net.codinux.banking.client.model.options
|
||||||
|
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
|
||||||
|
@NoArgConstructor
|
||||||
|
open class GetAccountDataOptions(
|
||||||
|
val retrieveBalance: Boolean = true,
|
||||||
|
val retrieveTransactions: RetrieveTransactions = RetrieveTransactions.OfLast90Days,
|
||||||
|
val retrieveTransactionsFrom: LocalDate? = null,
|
||||||
|
val retrieveTransactionsTo: LocalDate? = null,
|
||||||
|
val abortIfTanIsRequired: Boolean = false
|
||||||
|
) {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "retrieveBalance=$retrieveBalance, retrieveTransactions=$retrieveTransactions, abortIfTanIsRequired=$abortIfTanIsRequired"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package net.codinux.banking.client.model.options
|
||||||
|
|
||||||
|
enum class RetrieveTransactions {
|
||||||
|
No,
|
||||||
|
|
||||||
|
All,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some banks support that according to PSD2 account transactions of last 90 days may be retrieved without
|
||||||
|
* a TAN (= no strong customer authorization needed). So try this options if you don't want to enter a TAN.
|
||||||
|
*/
|
||||||
|
OfLast90Days,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves account transactions in the boundaries of [GetAccountDataOptions.retrieveTransactionsFrom] to [GetAccountDataOptions.retrieveTransactionsTo].
|
||||||
|
*/
|
||||||
|
AccordingToRetrieveFromAndTo
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package net.codinux.banking.client.model.request
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.tan.EnterTanResult
|
||||||
|
|
||||||
|
class EnterTanResultDto(
|
||||||
|
val tanRequestId: String,
|
||||||
|
enteredTan: String?
|
||||||
|
) : EnterTanResult(enteredTan) {
|
||||||
|
override fun toString() = "$tanRequestId, entered Tan: $enteredTan"
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package net.codinux.banking.client.model.request
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.AccountCredentials
|
||||||
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
import net.codinux.banking.client.model.options.GetAccountDataOptions
|
||||||
|
|
||||||
|
@NoArgConstructor
|
||||||
|
open class GetAccountDataRequest(bankCode: String, loginName: String, password: String, val options: GetAccountDataOptions? = null)
|
||||||
|
: AccountCredentials(bankCode, loginName, password) {
|
||||||
|
|
||||||
|
constructor(credentials: AccountCredentials, options: GetAccountDataOptions? = null)
|
||||||
|
: this(credentials.bankCode, credentials.loginName, credentials.password, options)
|
||||||
|
|
||||||
|
override fun toString() = "${super.toString()}: $options"
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package net.codinux.banking.client.model.response
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
|
||||||
|
@NoArgConstructor
|
||||||
|
open class Error(
|
||||||
|
val type: ErrorType,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A banking client internal error like an error occurred during response parsing.
|
||||||
|
*/
|
||||||
|
val internalError: String? = null,
|
||||||
|
/**
|
||||||
|
* Error messages as received from bank
|
||||||
|
*/
|
||||||
|
val errorMessagesFromBank: List<String> = emptyList(),
|
||||||
|
) {
|
||||||
|
override fun toString() = "$type: ${internalError ?: errorMessagesFromBank.joinToString()}"
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package net.codinux.banking.client.model.response
|
||||||
|
|
||||||
|
enum class ErrorType {
|
||||||
|
BankDoesNotSupportFinTs3,
|
||||||
|
|
||||||
|
NetworkError,
|
||||||
|
|
||||||
|
InternalError,
|
||||||
|
|
||||||
|
BankReturnedError,
|
||||||
|
|
||||||
|
WrongCredentials,
|
||||||
|
|
||||||
|
AccountLocked,
|
||||||
|
|
||||||
|
JobNotSupported,
|
||||||
|
|
||||||
|
UserCancelledAction,
|
||||||
|
|
||||||
|
TanRequiredButShouldAbortIfRequiresTan,
|
||||||
|
|
||||||
|
TanRequestIdNotFound,
|
||||||
|
|
||||||
|
NoneOfTheAccountsSupportsRetrievingData,
|
||||||
|
|
||||||
|
DidNotRetrieveAllAccountData,
|
||||||
|
|
||||||
|
CanNotDetermineBicForIban,
|
||||||
|
|
||||||
|
NoAccountSupportsMoneyTransfer,
|
||||||
|
|
||||||
|
MoreThanOneAccountSupportsMoneyTransfer,
|
||||||
|
|
||||||
|
UnknownError
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package net.codinux.banking.client.model.response
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.AccountTransaction
|
||||||
|
import net.codinux.banking.client.model.CustomerAccount
|
||||||
|
import net.codinux.banking.client.model.config.JsonIgnore
|
||||||
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
|
||||||
|
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
|
||||||
|
@NoArgConstructor
|
||||||
|
open class GetAccountDataResponse(
|
||||||
|
val customer: CustomerAccount
|
||||||
|
) {
|
||||||
|
|
||||||
|
@get:JsonIgnore
|
||||||
|
val bookedTransactions: List<AccountTransaction>
|
||||||
|
get() = customer.accounts.flatMap { it.bookedTransactions }.sortedByDescending { it.valueDate }
|
||||||
|
|
||||||
|
|
||||||
|
override fun toString() = customer.toString()
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package net.codinux.banking.client.model.response
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
|
||||||
|
// TODO: may differentiate between ClientResponse, which is either Success or Error, and RestResponse, which can be Success, Error and TanRequired
|
||||||
|
@NoArgConstructor
|
||||||
|
open class Response<T> protected constructor(
|
||||||
|
val type: ResponseType,
|
||||||
|
val data: T? = null,
|
||||||
|
val error: Error? = null,
|
||||||
|
val tanRequired: TanRequired? = null
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun <T> success(data: T): Response<T> =
|
||||||
|
Response(ResponseType.Success, data)
|
||||||
|
|
||||||
|
fun <T> error(errorType: ErrorType, internalError: String? = null, errorMessagesFromBank: List<String> = emptyList()): Response<T> =
|
||||||
|
Response(ResponseType.Error, null, Error(errorType, internalError, errorMessagesFromBank))
|
||||||
|
|
||||||
|
fun <T> tanRequired(tanRequired: TanRequired): Response<T> =
|
||||||
|
Response(ResponseType.TanRequired, null, null, tanRequired)
|
||||||
|
|
||||||
|
fun <T> bankReturnedError(errorMessagesFromBank: List<String>): Response<T> =
|
||||||
|
Response.error(ErrorType.BankReturnedError, null, errorMessagesFromBank)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun toString() = when (type) {
|
||||||
|
ResponseType.Success -> "Success: $data"
|
||||||
|
ResponseType.Error -> "Error: $error"
|
||||||
|
ResponseType.TanRequired -> "TanRequired: $tanRequired"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package net.codinux.banking.client.model.response
|
||||||
|
|
||||||
|
enum class ResponseType {
|
||||||
|
Success,
|
||||||
|
|
||||||
|
Error,
|
||||||
|
|
||||||
|
TanRequired
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package net.codinux.banking.client.model.response
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
import net.codinux.banking.client.model.tan.TanChallenge
|
||||||
|
|
||||||
|
@NoArgConstructor
|
||||||
|
open class TanRequired (
|
||||||
|
val tanRequestId: String,
|
||||||
|
val tanChallenge: TanChallenge
|
||||||
|
) {
|
||||||
|
override fun toString() = "$tanChallenge"
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package net.dankito.banking.fints.model
|
package net.codinux.banking.client.model.tan
|
||||||
|
|
||||||
enum class ActionRequiringTan {
|
enum class ActionRequiringTan {
|
||||||
GetAnonymousBankInfo,
|
GetAnonymousBankInfo,
|
|
@ -0,0 +1,9 @@
|
||||||
|
package net.codinux.banking.client.model.tan
|
||||||
|
|
||||||
|
enum class AllowedTanFormat {
|
||||||
|
Numeric,
|
||||||
|
|
||||||
|
Alphanumeric,
|
||||||
|
|
||||||
|
TanIsEnteredOnOtherDevice
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package net.codinux.banking.client.model.tan
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
|
||||||
|
@NoArgConstructor
|
||||||
|
open class EnterTanResult(
|
||||||
|
val enteredTan: String?,
|
||||||
|
// val changeTanMethodTo: TanMethod? = null,
|
||||||
|
// val changeTanMediumTo: TanMedium? = null,
|
||||||
|
// val changeTanMediumResultCallback: ((BankingClientResponse) -> Unit)? = null
|
||||||
|
)
|
|
@ -1,12 +1,17 @@
|
||||||
package net.dankito.banking.fints.tan
|
package net.codinux.banking.client.model.tan
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.config.JsonIgnore
|
||||||
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
|
||||||
|
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
|
||||||
|
@NoArgConstructor
|
||||||
open class FlickerCode(
|
open class FlickerCode(
|
||||||
val challengeHHD_UC: String,
|
val challengeHHD_UC: String,
|
||||||
val parsedDataSet: String,
|
val parsedDataSet: String,
|
||||||
val decodingError: Exception? = null
|
val decodingError: String? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@get:JsonIgnore
|
||||||
val decodingSuccessful: Boolean
|
val decodingSuccessful: Boolean
|
||||||
get() = decodingError == null
|
get() = decodingError == null
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package net.codinux.banking.client.model.tan
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
|
||||||
|
@NoArgConstructor
|
||||||
|
open class MobilePhoneTanMedium(
|
||||||
|
val phoneNumber: String?
|
||||||
|
) {
|
||||||
|
override fun toString() = phoneNumber ?: "No phone number"
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package net.codinux.banking.client.model.tan
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.BankAccountViewInfo
|
||||||
|
import net.codinux.banking.client.model.CustomerAccountViewInfo
|
||||||
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
|
||||||
|
@NoArgConstructor
|
||||||
|
open class TanChallenge(
|
||||||
|
val type: TanChallengeType,
|
||||||
|
val forAction: ActionRequiringTan,
|
||||||
|
val messageToShowToUser: String,
|
||||||
|
val tanMethod: TanMethod,
|
||||||
|
val tanImage: TanImage? = null,
|
||||||
|
val flickerCode: FlickerCode? = null,
|
||||||
|
val customer: CustomerAccountViewInfo,
|
||||||
|
val account: BankAccountViewInfo? = null
|
||||||
|
// TODO: add availableTanMethods, selectedTanMedium, availableTanMedia
|
||||||
|
) {
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "$tanMethod $forAction: $messageToShowToUser" + when (type) {
|
||||||
|
TanChallengeType.EnterTan -> ""
|
||||||
|
TanChallengeType.Image -> ", Image: $tanImage"
|
||||||
|
TanChallengeType.Flickercode -> ", FlickerCode: $flickerCode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package net.codinux.banking.client.model.tan
|
||||||
|
|
||||||
|
enum class TanChallengeType {
|
||||||
|
Image,
|
||||||
|
|
||||||
|
Flickercode,
|
||||||
|
|
||||||
|
EnterTan
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package net.codinux.banking.client.model.tan
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
|
||||||
|
@NoArgConstructor
|
||||||
|
open class TanGeneratorTanMedium(
|
||||||
|
val cardNumber: String
|
||||||
|
) {
|
||||||
|
override fun toString() = cardNumber
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package net.codinux.banking.client.model.tan
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.config.JsonIgnore
|
||||||
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
|
||||||
|
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
|
||||||
|
@NoArgConstructor
|
||||||
|
open class TanImage(
|
||||||
|
val mimeType: String,
|
||||||
|
val imageBytesBase64: String,
|
||||||
|
val decodingError: String? = null
|
||||||
|
) {
|
||||||
|
|
||||||
|
@get:JsonIgnore
|
||||||
|
val decodingSuccessful: Boolean
|
||||||
|
get() = decodingError == null
|
||||||
|
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
if (decodingSuccessful == false) {
|
||||||
|
return "Decoding error: $decodingError"
|
||||||
|
}
|
||||||
|
|
||||||
|
return mimeType
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package net.codinux.banking.client.model.tan
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
|
||||||
|
@NoArgConstructor
|
||||||
|
open class TanMedium(
|
||||||
|
val type: TanMediumType,
|
||||||
|
val displayName: String,
|
||||||
|
val status: TanMediumStatus,
|
||||||
|
/**
|
||||||
|
* Only set if [type] is [TanMediumType.TanGenerator].
|
||||||
|
*/
|
||||||
|
val tanGenerator: TanGeneratorTanMedium? = null,
|
||||||
|
/**
|
||||||
|
* Only set if [type] is [TanMediumType.MobilePhone].
|
||||||
|
*/
|
||||||
|
val mobilePhone: MobilePhoneTanMedium? = null
|
||||||
|
) {
|
||||||
|
override fun toString() = "$displayName $status"
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package net.codinux.banking.client.model.tan
|
||||||
|
|
||||||
|
enum class TanMediumStatus {
|
||||||
|
Used,
|
||||||
|
|
||||||
|
Available
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package net.codinux.banking.client.model.tan
|
||||||
|
|
||||||
|
enum class TanMediumType {
|
||||||
|
/**
|
||||||
|
* All other TAN media, like AppTan.
|
||||||
|
*/
|
||||||
|
Generic,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If I'm not wrong MobilePhone is only used for SmsTan.
|
||||||
|
*/
|
||||||
|
MobilePhone,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mostly used for chipTan.
|
||||||
|
*/
|
||||||
|
TanGenerator
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package net.codinux.banking.client.model.tan
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||||
|
|
||||||
|
@NoArgConstructor
|
||||||
|
open class TanMethod(
|
||||||
|
val displayName: String,
|
||||||
|
val type: TanMethodType,
|
||||||
|
val identifier: String,
|
||||||
|
val maxTanInputLength: Int? = null,
|
||||||
|
val allowedTanFormat: AllowedTanFormat = AllowedTanFormat.Alphanumeric
|
||||||
|
) {
|
||||||
|
override fun toString() = "$displayName ($type, ${identifier})"
|
||||||
|
}
|
|
@ -1,8 +1,6 @@
|
||||||
package net.dankito.banking.fints.model
|
package net.codinux.banking.client.model.tan
|
||||||
|
|
||||||
|
|
||||||
enum class TanMethodType {
|
enum class TanMethodType {
|
||||||
|
|
||||||
EnterTan,
|
EnterTan,
|
||||||
|
|
||||||
ChipTanManuell,
|
ChipTanManuell,
|
||||||
|
@ -22,5 +20,4 @@ enum class TanMethodType {
|
||||||
photoTan,
|
photoTan,
|
||||||
|
|
||||||
QrCode
|
QrCode
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package net.codinux.banking.client.model
|
||||||
|
|
||||||
|
@JsModule("@js-joda/timezone")
|
||||||
|
@JsNonModule
|
||||||
|
external object JsJodaTimeZoneModule
|
||||||
|
|
||||||
|
private val jsJodaTz = JsJodaTimeZoneModule
|
|
@ -0,0 +1,3 @@
|
||||||
|
package net.codinux.banking.client.model.config
|
||||||
|
|
||||||
|
actual annotation class JsonIgnore
|
|
@ -0,0 +1,5 @@
|
||||||
|
package net.codinux.banking.client.model.config
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
|
|
||||||
|
actual typealias JsonIgnore = JsonIgnore
|
|
@ -0,0 +1,3 @@
|
||||||
|
package net.codinux.banking.client.model.config
|
||||||
|
|
||||||
|
actual annotation class JsonIgnore
|
|
@ -0,0 +1,3 @@
|
||||||
|
package net.codinux.banking.client.model.config
|
||||||
|
|
||||||
|
actual annotation class JsonIgnore
|
|
@ -0,0 +1,139 @@
|
||||||
|
@file:OptIn(ExperimentalWasmDsl::class)
|
||||||
|
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
kotlin("multiplatform")
|
||||||
|
|
||||||
|
id("maven-publish")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(8)
|
||||||
|
|
||||||
|
jvm {
|
||||||
|
withJava()
|
||||||
|
|
||||||
|
testRuns["test"].executionTask.configure {
|
||||||
|
useJUnitPlatform()
|
||||||
|
|
||||||
|
testLogging {
|
||||||
|
showExceptions = true
|
||||||
|
showStandardStreams = true
|
||||||
|
events("passed", "skipped", "failed")
|
||||||
|
// exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
js {
|
||||||
|
moduleName = "fints4k-banking-client"
|
||||||
|
binaries.executable()
|
||||||
|
|
||||||
|
browser {
|
||||||
|
testTask {
|
||||||
|
useKarma {
|
||||||
|
useChromeHeadless()
|
||||||
|
useFirefoxHeadless()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nodejs {
|
||||||
|
testTask {
|
||||||
|
useMocha {
|
||||||
|
timeout = "20s" // Mocha times out after 2 s, which is too short for bufferExceeded() test
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wasmJs()
|
||||||
|
|
||||||
|
|
||||||
|
linuxX64()
|
||||||
|
mingwX64()
|
||||||
|
|
||||||
|
iosArm64()
|
||||||
|
iosSimulatorArm64()
|
||||||
|
macosX64()
|
||||||
|
macosArm64()
|
||||||
|
watchosArm64()
|
||||||
|
watchosSimulatorArm64()
|
||||||
|
tvosArm64()
|
||||||
|
tvosSimulatorArm64()
|
||||||
|
|
||||||
|
applyDefaultHierarchyTemplate()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
val coroutinesVersion: String by project
|
||||||
|
val kotlinxDateTimeVersion: String by project
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
api(project(":BankingClient"))
|
||||||
|
|
||||||
|
api("net.codinux.banking:fints4k:1.0.0-Alpha-11")
|
||||||
|
|
||||||
|
api("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDateTimeVersion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
commonTest {
|
||||||
|
dependencies {
|
||||||
|
implementation(kotlin("test"))
|
||||||
|
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jvmMain {
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jvmTest {
|
||||||
|
dependencies {
|
||||||
|
implementation(kotlin("test"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jsMain {
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jsTest { }
|
||||||
|
|
||||||
|
nativeMain { }
|
||||||
|
nativeTest { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//ext["customArtifactId"] = "fints4k-banking-client"
|
||||||
|
//
|
||||||
|
//apply(from = "../gradle/scripts/publish-codinux.gradle.kts")
|
||||||
|
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
repositories {
|
||||||
|
maven {
|
||||||
|
name = "codinux"
|
||||||
|
url = uri("https://maven.dankito.net/api/packages/codinux/maven")
|
||||||
|
|
||||||
|
credentials(PasswordCredentials::class.java) {
|
||||||
|
username = project.property("codinuxRegistryWriterUsername") as String
|
||||||
|
password = project.property("codinuxRegistryWriterPassword") as String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package net.codinux.banking.client.fints4k
|
||||||
|
|
||||||
|
import net.codinux.banking.client.BankingClientCallback
|
||||||
|
import net.dankito.banking.fints.callback.FinTsClientCallback
|
||||||
|
import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium
|
||||||
|
import net.dankito.banking.fints.model.BankData
|
||||||
|
import net.dankito.banking.fints.model.EnterTanGeneratorAtcResult
|
||||||
|
import net.dankito.banking.fints.model.TanMethod
|
||||||
|
|
||||||
|
open class BridgeFintTsToBankingClientCallback(
|
||||||
|
protected val bankingClientCallback: BankingClientCallback,
|
||||||
|
protected val mapper: FinTs4kMapper
|
||||||
|
) : FinTsClientCallback {
|
||||||
|
|
||||||
|
override suspend fun askUserForTanMethod(supportedTanMethods: List<TanMethod>, suggestedTanMethod: TanMethod?): TanMethod? {
|
||||||
|
return suggestedTanMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun enterTan(tanChallenge: net.dankito.banking.fints.model.TanChallenge) {
|
||||||
|
bankingClientCallback.enterTan(mapper.mapTanChallenge(tanChallenge)) { enterTanResult ->
|
||||||
|
if (enterTanResult.enteredTan != null) {
|
||||||
|
tanChallenge.userEnteredTan(enterTanResult.enteredTan!!)
|
||||||
|
} else {
|
||||||
|
tanChallenge.userDidNotEnterTan()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanGeneratorTanMedium): EnterTanGeneratorAtcResult {
|
||||||
|
return EnterTanGeneratorAtcResult.userDidNotEnterAtc()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package net.codinux.banking.client.fints4k
|
||||||
|
|
||||||
|
import net.codinux.banking.client.BankingClient
|
||||||
|
import net.codinux.banking.client.BankingClientCallback
|
||||||
|
import net.codinux.banking.client.model.options.GetAccountDataOptions
|
||||||
|
import net.codinux.banking.client.model.request.GetAccountDataRequest
|
||||||
|
import net.codinux.banking.client.model.response.GetAccountDataResponse
|
||||||
|
import net.codinux.banking.client.model.response.Response
|
||||||
|
import net.dankito.banking.fints.FinTsClient
|
||||||
|
|
||||||
|
open class FinTs4kBankingClient(
|
||||||
|
callback: BankingClientCallback
|
||||||
|
) : BankingClient {
|
||||||
|
|
||||||
|
private val mapper = FinTs4kMapper()
|
||||||
|
|
||||||
|
private val client = FinTsClient(BridgeFintTsToBankingClientCallback(callback, mapper))
|
||||||
|
|
||||||
|
|
||||||
|
override suspend fun getAccountDataAsync(request: GetAccountDataRequest): Response<GetAccountDataResponse> {
|
||||||
|
val response = client.getAccountDataAsync(mapper.mapToGetAccountDataParameter(request, request.options ?: GetAccountDataOptions()))
|
||||||
|
|
||||||
|
return mapper.map(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package net.codinux.banking.client.fints4k
|
||||||
|
|
||||||
|
import net.codinux.banking.client.BankingClientCallback
|
||||||
|
import net.codinux.banking.client.BankingClientForCustomerBase
|
||||||
|
import net.codinux.banking.client.model.AccountCredentials
|
||||||
|
|
||||||
|
open class FinTs4kBankingClientForCustomer(credentials: AccountCredentials, callback: BankingClientCallback)
|
||||||
|
: BankingClientForCustomerBase(credentials, FinTs4kBankingClient(callback)) {
|
||||||
|
|
||||||
|
constructor(bankCode: String, loginName: String, password: String, callback: BankingClientCallback)
|
||||||
|
: this(AccountCredentials(bankCode, loginName, password), callback)
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,164 @@
|
||||||
|
package net.codinux.banking.client.fints4k
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.*
|
||||||
|
import net.codinux.banking.client.model.AccountTransaction
|
||||||
|
import net.codinux.banking.client.model.Amount
|
||||||
|
import net.codinux.banking.client.model.tan.*
|
||||||
|
import net.codinux.banking.client.model.options.GetAccountDataOptions
|
||||||
|
import net.codinux.banking.client.model.response.*
|
||||||
|
import net.codinux.banking.client.model.tan.ActionRequiringTan
|
||||||
|
import net.codinux.banking.client.model.tan.TanChallenge
|
||||||
|
import net.codinux.banking.client.model.tan.TanImage
|
||||||
|
import net.codinux.banking.client.model.tan.TanMethod
|
||||||
|
import net.codinux.banking.client.model.tan.TanMethodType
|
||||||
|
import net.dankito.banking.client.model.BankAccountIdentifierImpl
|
||||||
|
import net.dankito.banking.client.model.parameter.GetAccountDataParameter
|
||||||
|
import net.dankito.banking.client.model.parameter.RetrieveTransactions
|
||||||
|
import net.dankito.banking.client.model.response.ErrorCode
|
||||||
|
import net.dankito.banking.fints.mapper.FinTsModelMapper
|
||||||
|
import net.dankito.banking.fints.model.*
|
||||||
|
import kotlin.io.encoding.Base64
|
||||||
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
|
open class FinTs4kMapper {
|
||||||
|
|
||||||
|
private val fintsModelMapper = FinTsModelMapper()
|
||||||
|
|
||||||
|
|
||||||
|
fun mapToGetAccountDataParameter(credentials: AccountCredentials, options: GetAccountDataOptions) = GetAccountDataParameter(
|
||||||
|
credentials.bankCode, credentials.loginName, credentials.password,
|
||||||
|
options.accounts.map { BankAccountIdentifierImpl(it.identifier, it.subAccountNumber, it.iban) },
|
||||||
|
options.retrieveBalance,
|
||||||
|
RetrieveTransactions.valueOf(options.retrieveTransactions.name), options.retrieveTransactionsFrom, options.retrieveTransactionsTo,
|
||||||
|
abortIfTanIsRequired = options.abortIfTanIsRequired
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
fun map(response: net.dankito.banking.client.model.response.GetAccountDataResponse): Response<GetAccountDataResponse> {
|
||||||
|
return if (response.successful && response.customerAccount != null) {
|
||||||
|
Response.success(GetAccountDataResponse(mapCustomer(response.customerAccount!!)))
|
||||||
|
} else {
|
||||||
|
mapError(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun mapToCustomerAccountViewInfo(bank: BankData): CustomerAccountViewInfo = CustomerAccountViewInfo(
|
||||||
|
bank.bankCode, bank.customerId, bank.bankName
|
||||||
|
)
|
||||||
|
|
||||||
|
fun mapToBankAccountViewInfo(account: AccountData): BankAccountViewInfo = BankAccountViewInfo(
|
||||||
|
account.accountIdentifier, account.subAccountAttribute,
|
||||||
|
mapAccountType(fintsModelMapper.map(account.accountType)),
|
||||||
|
account.iban, account.productName
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
private fun mapCustomer(customer: net.dankito.banking.client.model.CustomerAccount): CustomerAccount = CustomerAccount(
|
||||||
|
customer.bankCode, customer.loginName, customer.password,
|
||||||
|
customer.bankName, customer.bic, customer.customerName, customer.userId,
|
||||||
|
customer.accounts.map { mapAccount(it) }
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
private fun mapAccount(account: net.dankito.banking.client.model.BankAccount): BankAccount = BankAccount(
|
||||||
|
account.identifier, account.accountHolderName, mapAccountType(account.type), account.iban, account.subAccountNumber,
|
||||||
|
account.productName, account.currency, account.accountLimit, account.isAccountTypeSupportedByApplication,
|
||||||
|
mapFeatures(account),
|
||||||
|
mapAmount(account.balance), account.retrievedTransactionsFrom, account.retrievedTransactionsTo,
|
||||||
|
// TODO: map haveAllTransactionsBeenRetrieved
|
||||||
|
countDaysForWhichTransactionsAreKept = account.countDaysForWhichTransactionsAreKept,
|
||||||
|
bookedTransactions = account.bookedTransactions.map { mapTransaction(it) }.toMutableList()
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun mapAccountType(type: net.dankito.banking.client.model.BankAccountType): BankAccountType =
|
||||||
|
BankAccountType.valueOf(type.name)
|
||||||
|
|
||||||
|
private fun mapFeatures(account: net.dankito.banking.client.model.BankAccount): Set<BankAccountFeatures> = buildSet {
|
||||||
|
if (account.supportsRetrievingBalance) {
|
||||||
|
add(BankAccountFeatures.RetrieveBalance)
|
||||||
|
}
|
||||||
|
if (account.supportsRetrievingTransactions) {
|
||||||
|
add(BankAccountFeatures.RetrieveTransactions)
|
||||||
|
}
|
||||||
|
if (account.supportsTransferringMoney) {
|
||||||
|
add(BankAccountFeatures.TransferMoney)
|
||||||
|
}
|
||||||
|
if (account.supportsInstantPayment) {
|
||||||
|
add(BankAccountFeatures.InstantPayment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun mapTransaction(transaction: net.dankito.banking.client.model.AccountTransaction): AccountTransaction = AccountTransaction(
|
||||||
|
mapAmount(transaction.amount), transaction.amount.currency.code, transaction.unparsedReference,
|
||||||
|
transaction.bookingDate, transaction.valueDate,
|
||||||
|
transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId,
|
||||||
|
transaction.bookingText, null,
|
||||||
|
transaction.statementNumber, transaction.sequenceNumber,
|
||||||
|
mapNullableAmount(transaction.openingBalance), mapNullableAmount(transaction.closingBalance),
|
||||||
|
// TODO: map other properties
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun mapNullableAmount(amount: Money?) = amount?.let { mapAmount(it) }
|
||||||
|
|
||||||
|
private fun mapAmount(amount: Money) = Amount.fromString(amount.amount.string.replace(',', '.'))
|
||||||
|
|
||||||
|
|
||||||
|
fun mapTanChallenge(challenge: net.dankito.banking.fints.model.TanChallenge): TanChallenge {
|
||||||
|
val type = mapTanChallengeType(challenge)
|
||||||
|
val action = mapActionRequiringTan(challenge.forAction)
|
||||||
|
val tanMethod = mapTanMethod(challenge.tanMethod)
|
||||||
|
val customer = mapToCustomerAccountViewInfo(challenge.bank)
|
||||||
|
val account = challenge.account?.let { mapToBankAccountViewInfo(it) }
|
||||||
|
|
||||||
|
val tanImage = if (challenge is ImageTanChallenge) mapTanImage(challenge.image) else null
|
||||||
|
val flickerCode = if (challenge is FlickerCodeTanChallenge) mapFlickerCode(challenge.flickerCode) else null
|
||||||
|
|
||||||
|
return TanChallenge(type, action, challenge.messageToShowToUser, tanMethod, tanImage, flickerCode, customer, account)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapTanChallengeType(challenge: net.dankito.banking.fints.model.TanChallenge): TanChallengeType = when {
|
||||||
|
challenge is ImageTanChallenge -> TanChallengeType.Image
|
||||||
|
challenge is FlickerCodeTanChallenge -> TanChallengeType.Flickercode
|
||||||
|
else -> TanChallengeType.EnterTan
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapActionRequiringTan(action: net.dankito.banking.fints.model.ActionRequiringTan): ActionRequiringTan =
|
||||||
|
ActionRequiringTan.valueOf(action.name)
|
||||||
|
|
||||||
|
private fun mapTanMethod(method: net.dankito.banking.fints.model.TanMethod): TanMethod = TanMethod(
|
||||||
|
method.displayName, mapTanMethodType(method.type), method.securityFunction.code, method.maxTanInputLength, mapAllowedTanFormat(method.allowedTanFormat)
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun mapTanMethodType(type: net.dankito.banking.fints.model.TanMethodType): TanMethodType =
|
||||||
|
TanMethodType.valueOf(type.name)
|
||||||
|
|
||||||
|
private fun mapAllowedTanFormat(allowedTanFormat: net.dankito.banking.fints.messages.datenelemente.implementierte.tan.AllowedTanFormat?): AllowedTanFormat =
|
||||||
|
allowedTanFormat?.let { AllowedTanFormat.valueOf(it.name) } ?: AllowedTanFormat.Alphanumeric
|
||||||
|
|
||||||
|
private fun mapTanImage(image: net.dankito.banking.fints.tan.TanImage): TanImage =
|
||||||
|
TanImage(image.mimeType, mapToBase64(image.imageBytes), mapException(image.decodingError))
|
||||||
|
|
||||||
|
@OptIn(ExperimentalEncodingApi::class)
|
||||||
|
private fun mapToBase64(bytes: ByteArray): String {
|
||||||
|
return Base64.Default.encode(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapFlickerCode(flickerCode: net.dankito.banking.fints.tan.FlickerCode): FlickerCode =
|
||||||
|
FlickerCode(flickerCode.challengeHHD_UC, flickerCode.parsedDataSet, mapException(flickerCode.decodingError))
|
||||||
|
|
||||||
|
|
||||||
|
private fun <T> mapError(response: net.dankito.banking.client.model.response.GetAccountDataResponse): Response<T> {
|
||||||
|
return if (response.error != null) {
|
||||||
|
Response.error(ErrorType.valueOf(response.error!!.name), if (response.error == ErrorCode.BankReturnedError) null else response.errorMessage,
|
||||||
|
if (response.error == ErrorCode.BankReturnedError && response.errorMessage !== null) listOf(response.errorMessage!!) else emptyList())
|
||||||
|
} else {
|
||||||
|
Response.error(ErrorType.UnknownError, response.errorMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapException(exception: Exception?): String? =
|
||||||
|
exception?.stackTraceToString()
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package net.codinux.banking.client.fints4k
|
||||||
|
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import net.codinux.banking.client.SimpleBankingClientCallback
|
||||||
|
import net.codinux.banking.client.model.response.ResponseType
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNotNull
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class FinTs4kBankingClientTest {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
// set your credentials here:
|
||||||
|
private const val bankCode = ""
|
||||||
|
private const val loginName = ""
|
||||||
|
private const val password = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private val underTest = FinTs4kBankingClientForCustomer(bankCode, loginName, password, SimpleBankingClientCallback { customer, tanChallenge ->
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getAccountDataAsync() = runTest {
|
||||||
|
val result = underTest.getAccountDataAsync()
|
||||||
|
|
||||||
|
assertEquals(ResponseType.Success, result.type)
|
||||||
|
assertNotNull(result.data)
|
||||||
|
assertTrue(result.data!!.bookedTransactions.isNotEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
16
LICENSE.md
|
@ -1,16 +0,0 @@
|
||||||
This program is offered under a commercial and under the AGPL license.
|
|
||||||
For commercial licensing, contact us at sales@dankito.net. For AGPL licensing, see below.
|
|
||||||
|
|
||||||
AGPL licensing:
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
128
README.md
|
@ -1,97 +1,93 @@
|
||||||
# fints4k
|
# Banking Client
|
||||||
|
|
||||||
fints4k is an implementation of the FinTS 3.0 online banking protocol used by most German banks.
|
Library to abstract over different banking client implementations like [fints4k](https://git.dankito.net/codinux/fints4k).
|
||||||
|
|
||||||
It's fast, easy extendable and running on multiple platforms: JVM, Android, (iOS, JavaScript, Windows, MacOS, Linux).
|
It's primary purpose is to abstract away the different implementation details and to create a common model that can be
|
||||||
|
used in all projects directly or indirectly referencing it - Web Service, Middleware, Native Apps, HTML Apps - so that
|
||||||
However it's not a full implementation of FinTS standard but implements all common use cases:
|
not each project has the implement to model again.
|
||||||
|
|
||||||
## Features
|
|
||||||
- Retrieving account information, balances and turnovers (Kontoumsätze und -saldo).
|
|
||||||
- Transfer money and real-time transfers (SEPA Überweisungen und Echtzeitüberweisung).
|
|
||||||
- Supports TAN methods chipTAN manual, Flickercode, QrCode and Photo (Matrix code), pushTAN, smsTAN and appTAN.
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
Not uploaded to Maven Central yet, will do this the next few days!
|
|
||||||
|
|
||||||
Gradle:
|
### Gradle:
|
||||||
|
|
||||||
```
|
```
|
||||||
dependencies {
|
plugins {
|
||||||
compile 'net.dankito.banking:fints4k:0.1.0'
|
kotlin("jvm") version "2.0.10" // or kotlin("multiplatform"), depending on your requirements
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
Maven:
|
|
||||||
```
|
repositories {
|
||||||
<dependency>
|
mavenCentral()
|
||||||
<groupId>net.dankito.banking</groupId>
|
maven {
|
||||||
<artifactId>fints4k</artifactId>
|
setUrl("https://maven.dankito.net/api/packages/codinux/maven")
|
||||||
<version>0.1.0</version>
|
}
|
||||||
</dependency>
|
}
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("net.codinux.banking.client:fints4k-banking-client:0.5.0")
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
See e.g. [JavaShowcase](fints4k/src/test/java/net/dankito/banking/fints/JavaShowcase.java) or [FinTsClientTest](fints4k/src/test/kotlin/net/dankito/banking/fints/FinTsClientTest.kt).
|
### Get AccountData
|
||||||
|
|
||||||
```java
|
Retrieves data like accounts, balance and booked transactions (Konten, Saldo und Kontoumsätze).
|
||||||
// Set your bank code (Bankleitzahl) here.
|
|
||||||
// BankInfo contains e.g. a bank's FinTS server address, country code and BIC (needed for money transfer)
|
|
||||||
List<BankInfo> foundBanks = new InMemoryBankFinder().findBankByNameBankCodeOrCity("<bank code, bank name or city>");
|
|
||||||
|
|
||||||
if (foundBanks.isEmpty() == false) { // please also check if bank supports FinTS 3.0
|
Basically:
|
||||||
BankData bank = new BankDataMapper().mapFromBankInfo(foundBanks.get(0));
|
|
||||||
|
|
||||||
// set your customer data (customerId = username you use to log in; pin = online banking pin / password)
|
```kotlin
|
||||||
CustomerData customer = new CustomerData("<customer_id>", "<pin>");
|
class ShowUsage {
|
||||||
|
|
||||||
FinTsClientCallback callback = new SimpleFinTsClientCallback(); // see advanced showcase for configuring callback
|
private val bankCode = "" // Bankleitzahl deiner Bank
|
||||||
|
|
||||||
FinTsClient finTsClient = new FinTsClient(callback, new Java8Base64Service());
|
private val loginName = "" // Online-Banking Login Name mit dem du dich beim Online-Banking deiner Bank anmeldest
|
||||||
|
|
||||||
AddAccountResponse addAccountResponse = finTsClient.addAccount(bank, customer);
|
private val password = "" // Online-Banking Password mit dem du dich beim Online-Banking deiner Bank anmeldest
|
||||||
|
|
||||||
if (addAccountResponse.isSuccessful()) {
|
|
||||||
System.out.println("Successfully added account for " + bank.getBankCode() + " " + customer.getCustomerId());
|
|
||||||
|
|
||||||
if (addAccountResponse.getBookedTransactions().isEmpty() == false) {
|
fun getAccountData() {
|
||||||
System.out.println("Account transactions of last 90 days:");
|
val client = FinTs4kBankingClientForCustomer(bankCode, loginName, password, SimpleBankingClientCallback())
|
||||||
showGetTransactionsResponse(addAccountResponse);
|
|
||||||
|
val response = client.getAccountData()
|
||||||
|
|
||||||
|
response.data?.let { data ->
|
||||||
|
val customer = data.customer
|
||||||
|
println("Kunde: ${customer.customerName} ${customer.accounts.size} Konten @ ${customer.bic} ${customer.bankName}")
|
||||||
|
|
||||||
|
println()
|
||||||
|
println("Konten:")
|
||||||
|
customer.accounts.sortedBy { it.type }.forEach { account ->
|
||||||
|
println("${account.identifier} ${account.productName} ${account.balance} ${account.currency}")
|
||||||
|
}
|
||||||
|
|
||||||
|
println()
|
||||||
|
println("Umsätze:")
|
||||||
|
data.bookedTransactions.forEach { transaction ->
|
||||||
|
println("${transaction.valueDate} ${transaction.amount} ${transaction.currency} ${transaction.otherPartyName ?: ""} - ${transaction.reference}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
System.out.println("Could not add account for " + bank.getBankCode() + " " + customer.getCustomerId() + ":");
|
|
||||||
showResponseError(addAccountResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
// see advanced show case what else you can do with this library, e.g. retrieving all account transactions and transferring money
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Logging
|
This fetches the booked account transactions of the last 90 days. In most cases no TAN is required for this.
|
||||||
|
|
||||||
fints4k uses slf4j as logging facade.
|
In case there is, add TAN handling in Client Callback:
|
||||||
|
|
||||||
So you can use any logger that supports slf4j, like Logback and log4j, to configure and get fints4k's log output.
|
```kotlin
|
||||||
|
val client = FinTs4kBankingClientForCustomer(bankCode, loginName, password, SimpleBankingClientCallback { tanChallenge, callback ->
|
||||||
|
val tan: String? = null // if a TAN is required, add a UI or ...
|
||||||
## Sample applications
|
callback.invoke(EnterTanResult(tan)) // ... set a break point here, get TAN e.g. from your TAN app, set tan variable in debugger view and resume debugger
|
||||||
|
})
|
||||||
### WebApp
|
|
||||||
|
|
||||||
Directly requesting bank servers is forbidden in browsers due to CORS.
|
|
||||||
|
|
||||||
In order to use fints4k directly in browser you need a CORS proxy like the one from CorsProxy
|
|
||||||
[Application.kt](SampleApplications/CorsProxy/src/main/kotlin/net/codinux/web/cors/Application.kt) or https://github.com/Rob--W/cors-anywhere.
|
|
||||||
|
|
||||||
Set CORS proxy's URL in WebApp [main.kt](SampleApplications/WebApp/src/main/kotlin/main.kt).
|
|
||||||
|
|
||||||
Start sample WebApp then with
|
|
||||||
```shell
|
|
||||||
./gradlew WebApp:run --continuous
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
Add some error handling by checking `response.error`:
|
||||||
|
|
||||||
Not free for commercial applications. More details to follow or [contact](mailto:sales@codinux.net) us.
|
```kotlin
|
||||||
|
response.error?.let{ error ->
|
||||||
|
println("Could not fetch account data: ${error.internalError ?: error.errorMessagesFromBank.joinToString()}")
|
||||||
|
}
|
||||||
|
```
|
|
@ -0,0 +1,16 @@
|
||||||
|
plugins {
|
||||||
|
kotlin("jvm")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven {
|
||||||
|
setUrl("https://maven.dankito.net/api/packages/codinux/maven")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("net.codinux.banking.client:fints4k-banking-client:0.5.0")
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package net.codinux.banking.client.fints4k.example
|
||||||
|
|
||||||
|
import net.codinux.banking.client.SimpleBankingClientCallback
|
||||||
|
import net.codinux.banking.client.fints4k.FinTs4kBankingClientForCustomer
|
||||||
|
import net.codinux.banking.client.getAccountData
|
||||||
|
import net.codinux.banking.client.model.tan.EnterTanResult
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
ShowUsage().getAccountData()
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShowUsage {
|
||||||
|
|
||||||
|
private val bankCode = "" // Bankleitzahl deiner Bank
|
||||||
|
|
||||||
|
private val loginName = "" // Online-Banking Login Name mit dem du dich beim Online-Banking deiner Bank anmeldest
|
||||||
|
|
||||||
|
private val password = "" // Online-Banking Password mit dem du dich beim Online-Banking deiner Bank anmeldest
|
||||||
|
|
||||||
|
|
||||||
|
fun getAccountData() {
|
||||||
|
val client = FinTs4kBankingClientForCustomer(bankCode, loginName, password, SimpleBankingClientCallback { tanChallenge, callback ->
|
||||||
|
val tan: String? = null // if a TAN is required, add a UI or ...
|
||||||
|
callback.invoke(EnterTanResult(tan)) // ... set a break point here, get TAN e.g. from your TAN app, set tan variable in debugger view and resume debugger
|
||||||
|
})
|
||||||
|
|
||||||
|
val response = client.getAccountData()
|
||||||
|
|
||||||
|
response.error?.let{ error ->
|
||||||
|
println("Could not fetch account data: ${error.internalError ?: error.errorMessagesFromBank.joinToString()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
response.data?.let { data ->
|
||||||
|
val customer = data.customer
|
||||||
|
println("Kunde: ${customer.customerName} ${customer.accounts.size} Konten @ ${customer.bic} ${customer.bankName}")
|
||||||
|
|
||||||
|
println()
|
||||||
|
println("Konten:")
|
||||||
|
customer.accounts.sortedBy { it.type }.forEach { account ->
|
||||||
|
println("${account.identifier} ${account.productName} ${account.balance} ${account.currency}")
|
||||||
|
}
|
||||||
|
|
||||||
|
println()
|
||||||
|
println("Umsätze:")
|
||||||
|
data.bookedTransactions.forEach { transaction ->
|
||||||
|
println("${transaction.valueDate} ${transaction.amount} ${transaction.currency} ${transaction.otherPartyName ?: ""} - ${transaction.reference}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,51 +0,0 @@
|
||||||
plugins {
|
|
||||||
id 'com.android.application'
|
|
||||||
id 'kotlin-android'
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdk 31
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "net.codinux.banking.fints4k.android"
|
|
||||||
minSdk 21
|
|
||||||
targetSdk 31
|
|
||||||
versionCode 1
|
|
||||||
versionName "1.0"
|
|
||||||
|
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = '1.8'
|
|
||||||
}
|
|
||||||
buildFeatures {
|
|
||||||
viewBinding true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation project(":fints4k")
|
|
||||||
|
|
||||||
implementation "net.dankito.utils:android-utils:1.1.2"
|
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
|
||||||
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.0.0'
|
|
||||||
implementation 'com.google.android.material:material:1.0.0'
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.0.0-rc02'
|
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.0.0-rc02'
|
|
||||||
|
|
||||||
implementation "org.slf4j:slf4j-android:1.7.32"
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
# Add project specific ProGuard rules here.
|
|
||||||
# You can control the set of applied configuration files using the
|
|
||||||
# proguardFiles setting in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
|
||||||
# debugging stack traces.
|
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
|
||||||
# hide the original source file name.
|
|
||||||
#-renamesourcefileattribute SourceFile
|
|
|
@ -1,28 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="net.codinux.banking.fints4k.android">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:allowBackup="true"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:supportsRtl="true"
|
|
||||||
android:theme="@style/Theme.Fints4kProject">
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:exported="true"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:theme="@style/Theme.Fints4kProject.NoActionBar">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
|
|
@ -1,61 +0,0 @@
|
||||||
package net.codinux.banking.fints4k.android
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.ContextThemeWrapper
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import net.codinux.banking.fints4k.android.adapter.AccountTransactionsListRecyclerAdapter
|
|
||||||
import net.codinux.banking.fints4k.android.databinding.FragmentFirstBinding
|
|
||||||
import net.codinux.banking.fints4k.android.dialogs.EnterTanDialog
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple [Fragment] subclass as the default destination in the navigation.
|
|
||||||
*/
|
|
||||||
class FirstFragment : Fragment() {
|
|
||||||
|
|
||||||
private var _binding: FragmentFirstBinding? = null
|
|
||||||
|
|
||||||
private val accountTransactionsAdapter = AccountTransactionsListRecyclerAdapter()
|
|
||||||
|
|
||||||
// This property is only valid between onCreateView and
|
|
||||||
// onDestroyView.
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
|
||||||
_binding = FragmentFirstBinding.inflate(inflater, container, false)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
binding.rcyvwAccountTransactions.apply {
|
|
||||||
layoutManager = LinearLayoutManager(this@FirstFragment.context, LinearLayoutManager.VERTICAL, false)
|
|
||||||
addItemDecoration(DividerItemDecoration(ContextThemeWrapper(this@FirstFragment.context, R.style.Theme_Fints4kProject), (layoutManager as LinearLayoutManager).orientation))
|
|
||||||
adapter = accountTransactionsAdapter
|
|
||||||
}
|
|
||||||
|
|
||||||
val presenter = Presenter() // TODO: inject
|
|
||||||
|
|
||||||
presenter.enterTanCallback = { tanChallenge ->
|
|
||||||
EnterTanDialog().show(tanChallenge, activity!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: set your credentials here
|
|
||||||
presenter.retrieveAccountData("", "", "") { response ->
|
|
||||||
response.customerAccount?.let { customer ->
|
|
||||||
accountTransactionsAdapter.items = customer.accounts.flatMap { it.bookedTransactions }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
package net.codinux.banking.fints4k.android
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.navigation.findNavController
|
|
||||||
import androidx.navigation.ui.AppBarConfiguration
|
|
||||||
import androidx.navigation.ui.navigateUp
|
|
||||||
import androidx.navigation.ui.setupActionBarWithNavController
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import net.codinux.banking.fints4k.android.databinding.ActivityMainBinding
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
private lateinit var appBarConfiguration: AppBarConfiguration
|
|
||||||
private lateinit var binding: ActivityMainBinding
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
setSupportActionBar(binding.toolbar)
|
|
||||||
|
|
||||||
val navController = findNavController(R.id.nav_host_fragment_content_main)
|
|
||||||
appBarConfiguration = AppBarConfiguration(navController.graph)
|
|
||||||
setupActionBarWithNavController(navController, appBarConfiguration)
|
|
||||||
|
|
||||||
binding.fab.setOnClickListener { view ->
|
|
||||||
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
|
|
||||||
.setAction("Action", null).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
||||||
// Inflate the menu; this adds items to the action bar if it is present.
|
|
||||||
menuInflater.inflate(R.menu.menu_main, menu)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
// Handle action bar item clicks here. The action bar will
|
|
||||||
// automatically handle clicks on the Home/Up button, so long
|
|
||||||
// as you specify a parent activity in AndroidManifest.xml.
|
|
||||||
return when (item.itemId) {
|
|
||||||
R.id.action_settings -> true
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSupportNavigateUp(): Boolean {
|
|
||||||
val navController = findNavController(R.id.nav_host_fragment_content_main)
|
|
||||||
return navController.navigateUp(appBarConfiguration)
|
|
||||||
|| super.onSupportNavigateUp()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
package net.codinux.banking.fints4k.android
|
|
||||||
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import kotlinx.datetime.LocalDate
|
|
||||||
import net.dankito.banking.client.model.parameter.GetAccountDataParameter
|
|
||||||
import net.dankito.banking.client.model.response.GetAccountDataResponse
|
|
||||||
import net.dankito.banking.fints.FinTsClient
|
|
||||||
import net.dankito.banking.fints.callback.SimpleFinTsClientCallback
|
|
||||||
import net.dankito.banking.fints.model.TanChallenge
|
|
||||||
import net.dankito.utils.multiplatform.extensions.millisSinceEpochAtSystemDefaultTimeZone
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.math.BigDecimal
|
|
||||||
import java.text.DateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
open class Presenter {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val ValueDateFormat = DateFormat.getDateInstance(DateFormat.SHORT)
|
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(Presenter::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val fintsClient = FinTsClient(SimpleFinTsClientCallback { challenge -> enterTan(challenge) })
|
|
||||||
|
|
||||||
open var enterTanCallback: ((TanChallenge) -> Unit)? = null
|
|
||||||
|
|
||||||
open protected fun enterTan(tanChallenge: TanChallenge) {
|
|
||||||
enterTanCallback?.invoke(tanChallenge) ?: run { tanChallenge.userDidNotEnterTan() }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
open fun retrieveAccountData(bankCode: String, loginName: String, password: String, retrievedResult: (GetAccountDataResponse) -> Unit) {
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
|
||||||
val response = fintsClient.getAccountDataAsync(GetAccountDataParameter(bankCode, loginName, password))
|
|
||||||
log.info("Retrieved response from ${response.customerAccount?.bankName} for ${response.customerAccount?.customerName}")
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
retrievedResult(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun formatDate(date: LocalDate): String {
|
|
||||||
try {
|
|
||||||
return ValueDateFormat.format(Date(date.millisSinceEpochAtSystemDefaultTimeZone))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.error("Could not format date $date", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
return date.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun formatAmount(amount: BigDecimal): String {
|
|
||||||
return String.format("%.02f", amount)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
package net.codinux.banking.fints4k.android
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
import net.codinux.banking.fints4k.android.databinding.FragmentSecondBinding
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple [Fragment] subclass as the second destination in the navigation.
|
|
||||||
*/
|
|
||||||
class SecondFragment : Fragment() {
|
|
||||||
|
|
||||||
private var _binding: FragmentSecondBinding? = null
|
|
||||||
|
|
||||||
// This property is only valid between onCreateView and
|
|
||||||
// onDestroyView.
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
|
|
||||||
_binding = FragmentSecondBinding.inflate(inflater, container, false)
|
|
||||||
return binding.root
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
binding.buttonSecond.setOnClickListener {
|
|
||||||
findNavController().navigate(R.id.action_SecondFragment_to_FirstFragment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
package net.codinux.banking.fints4k.android.adapter
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import net.codinux.banking.fints4k.android.Presenter
|
|
||||||
import net.codinux.banking.fints4k.android.R
|
|
||||||
import net.codinux.banking.fints4k.android.adapter.viewholder.AccountTransactionsViewHolder
|
|
||||||
import net.dankito.banking.client.model.AccountTransaction
|
|
||||||
import net.dankito.banking.fints.util.toBigDecimal
|
|
||||||
import net.dankito.utils.android.extensions.setTextColorToColorResource
|
|
||||||
import net.dankito.utils.android.ui.adapter.ListRecyclerAdapter
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.math.BigDecimal
|
|
||||||
|
|
||||||
class AccountTransactionsListRecyclerAdapter : ListRecyclerAdapter<AccountTransaction, AccountTransactionsViewHolder>() {
|
|
||||||
|
|
||||||
private val presenter = Presenter() // TOOD: inject
|
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(AccountTransactionsListRecyclerAdapter::class.java)
|
|
||||||
|
|
||||||
|
|
||||||
override fun getListItemLayoutId() = R.layout.list_item_account_transaction
|
|
||||||
|
|
||||||
override fun createViewHolder(itemView: View): AccountTransactionsViewHolder {
|
|
||||||
return AccountTransactionsViewHolder(itemView)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun bindItemToView(viewHolder: AccountTransactionsViewHolder, item: AccountTransaction) {
|
|
||||||
try {
|
|
||||||
viewHolder.txtvwBookingText.text = item.bookingText ?: ""
|
|
||||||
|
|
||||||
viewHolder.txtvwOtherPartyName.visibility = if (item.showOtherPartyName) View.VISIBLE else View.GONE
|
|
||||||
viewHolder.txtvwOtherPartyName.text = item.otherPartyName ?: ""
|
|
||||||
|
|
||||||
viewHolder.txtvwReference.text = item.reference
|
|
||||||
|
|
||||||
viewHolder.txtvwDate.text = presenter.formatDate(item.valueDate)
|
|
||||||
|
|
||||||
val amount = item.amount.toBigDecimal()
|
|
||||||
viewHolder.txtvwAmount.text = presenter.formatAmount(amount)
|
|
||||||
viewHolder.txtvwAmount.setTextColorToColorResource(if (amount >= BigDecimal.ZERO) R.color.positiveAmount else R.color.negativeAmount)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.error("Could not display account transaction $item", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
package net.codinux.banking.fints4k.android.adapter.viewholder
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import net.codinux.banking.fints4k.android.R
|
|
||||||
|
|
||||||
class AccountTransactionsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
|
||||||
|
|
||||||
val txtvwBookingText: TextView = itemView.findViewById(R.id.txtvwBookingText)
|
|
||||||
|
|
||||||
val txtvwOtherPartyName: TextView = itemView.findViewById(R.id.txtvwOtherPartyName)
|
|
||||||
|
|
||||||
val txtvwReference: TextView = itemView.findViewById(R.id.txtvwReference)
|
|
||||||
|
|
||||||
val txtvwAmount: TextView = itemView.findViewById(R.id.txtvwAmount)
|
|
||||||
|
|
||||||
val txtvwDate: TextView = itemView.findViewById(R.id.txtvwDate)
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,140 +0,0 @@
|
||||||
package net.codinux.banking.fints4k.android.dialogs
|
|
||||||
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.Handler
|
|
||||||
import android.text.InputFilter
|
|
||||||
import android.text.InputType
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.*
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import net.codinux.banking.fints4k.android.Presenter
|
|
||||||
import net.codinux.banking.fints4k.android.R
|
|
||||||
import net.dankito.banking.fints.model.FlickerCodeTanChallenge
|
|
||||||
import net.dankito.banking.fints.model.ImageTanChallenge
|
|
||||||
import net.dankito.banking.fints.model.TanChallenge
|
|
||||||
import net.dankito.utils.android.extensions.getSpannedFromHtml
|
|
||||||
import net.dankito.utils.android.extensions.show
|
|
||||||
|
|
||||||
|
|
||||||
open class EnterTanDialog : DialogFragment() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val DialogTag = "EnterTanDialog"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected lateinit var tanChallenge: TanChallenge
|
|
||||||
|
|
||||||
|
|
||||||
open fun show(tanChallenge: TanChallenge, activity: FragmentActivity) {
|
|
||||||
this.tanChallenge = tanChallenge
|
|
||||||
|
|
||||||
setStyle(STYLE_NORMAL, R.style.FullscreenDialogWithStatusBar)
|
|
||||||
|
|
||||||
show(activity.supportFragmentManager, DialogTag)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
|
||||||
val rootView = inflater.inflate(R.layout.dialog_enter_tan, container, false)
|
|
||||||
|
|
||||||
setupUI(rootView)
|
|
||||||
|
|
||||||
return rootView
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun setupUI(rootView: View) {
|
|
||||||
setupTanView(rootView)
|
|
||||||
|
|
||||||
setupEnteringTan(rootView)
|
|
||||||
|
|
||||||
rootView.findViewById<TextView>(R.id.txtvwMessageToShowToUser).text = tanChallenge.messageToShowToUser.getSpannedFromHtml()
|
|
||||||
|
|
||||||
rootView.findViewById<Button>(R.id.btnCancel).setOnClickListener { enteringTanDone(null) }
|
|
||||||
|
|
||||||
rootView.findViewById<Button>(R.id.btnEnteringTanDone).setOnClickListener {
|
|
||||||
enteringTanDone(rootView.findViewById<EditText>(R.id.edtxtEnteredTan).text.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun setupTanView(rootView: View) {
|
|
||||||
if (tanChallenge is FlickerCodeTanChallenge) {
|
|
||||||
// setupFlickerCodeTanView(rootView)
|
|
||||||
}
|
|
||||||
else if (tanChallenge is ImageTanChallenge) {
|
|
||||||
setupImageTanView(rootView)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun setupEnteringTan(rootView: View) {
|
|
||||||
val edtxtEnteredTan = rootView.findViewById<EditText>(R.id.edtxtEnteredTan)
|
|
||||||
|
|
||||||
if (tanChallenge.tanMethod.isNumericTan) {
|
|
||||||
edtxtEnteredTan.inputType = InputType.TYPE_CLASS_NUMBER
|
|
||||||
}
|
|
||||||
|
|
||||||
tanChallenge.tanMethod.maxTanInputLength?.let { maxInputLength ->
|
|
||||||
edtxtEnteredTan.filters = arrayOf<InputFilter>(InputFilter.LengthFilter(maxInputLength))
|
|
||||||
}
|
|
||||||
|
|
||||||
edtxtEnteredTan.setOnKeyListener { _, keyCode, _ ->
|
|
||||||
if (keyCode == KeyEvent.KEYCODE_ENTER) {
|
|
||||||
enteringTanDone(edtxtEnteredTan.text.toString())
|
|
||||||
return@setOnKeyListener true
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun setupImageTanView(rootView: View) {
|
|
||||||
val tanImageView = rootView.findViewById<ImageView>(R.id.tanImageView)
|
|
||||||
tanImageView.show()
|
|
||||||
|
|
||||||
val decodedImage = (tanChallenge as ImageTanChallenge).image
|
|
||||||
if (decodedImage.decodingSuccessful) {
|
|
||||||
val bitmap = BitmapFactory.decodeByteArray(decodedImage.imageBytes, 0, decodedImage.imageBytes.size)
|
|
||||||
tanImageView.setImageBitmap(bitmap)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
showDecodingTanChallengeFailedErrorDelayed(decodedImage.decodingError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method gets called right on start up before dialog is shown -> Alert would get displayed before dialog and
|
|
||||||
* therefore covered by dialog -> delay displaying alert.
|
|
||||||
*/
|
|
||||||
protected open fun showDecodingTanChallengeFailedErrorDelayed(error: Exception?) {
|
|
||||||
val handler = Handler()
|
|
||||||
|
|
||||||
handler.postDelayed({ showDecodingTanChallengeFailedError(error) }, 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun showDecodingTanChallengeFailedError(error: Exception?) {
|
|
||||||
activity?.let { context ->
|
|
||||||
AlertDialog.Builder(context)
|
|
||||||
.setMessage(context.getString(R.string.dialog_enter_tan_error_could_not_decode_tan_image, error?.localizedMessage))
|
|
||||||
.setPositiveButton(android.R.string.ok) { dialog, _ -> dialog.dismiss() }
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected open fun enteringTanDone(enteredTan: String?) {
|
|
||||||
if (enteredTan != null) {
|
|
||||||
tanChallenge.userEnteredTan(enteredTan)
|
|
||||||
} else {
|
|
||||||
tanChallenge.userDidNotEnterTan()
|
|
||||||
}
|
|
||||||
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path
|
|
||||||
android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
|
||||||
<aapt:attr name="android:fillColor">
|
|
||||||
<gradient
|
|
||||||
android:startY="49.59793"
|
|
||||||
android:startX="42.9492"
|
|
||||||
android:endY="92.4963"
|
|
||||||
android:endX="85.84757"
|
|
||||||
android:type="linear">
|
|
||||||
<item
|
|
||||||
android:color="#44000000"
|
|
||||||
android:offset="0.0"/>
|
|
||||||
<item
|
|
||||||
android:color="#00000000"
|
|
||||||
android:offset="1.0"/>
|
|
||||||
</gradient>
|
|
||||||
</aapt:attr>
|
|
||||||
</path>
|
|
||||||
<path
|
|
||||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:strokeColor="#00000000"/>
|
|
||||||
</vector>
|
|
|
@ -1,74 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:height="108dp"
|
|
||||||
android:width="108dp"
|
|
||||||
android:viewportHeight="108"
|
|
||||||
android:viewportWidth="108">
|
|
||||||
<path android:fillColor="#3DDC84"
|
|
||||||
android:pathData="M0,0h108v108h-108z"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
</vector>
|
|
|
@ -1,35 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".MainActivity">
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:theme="@style/Theme.Fints4kProject.AppBarOverlay">
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.Toolbar
|
|
||||||
android:id="@+id/toolbar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="?attr/actionBarSize"
|
|
||||||
android:background="?attr/colorPrimary"
|
|
||||||
app:popupTheme="@style/Theme.Fints4kProject.PopupOverlay"/>
|
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
|
||||||
|
|
||||||
<include layout="@layout/content_main"/>
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/fab"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom|end"
|
|
||||||
android:layout_marginEnd="@dimen/fab_margin"
|
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
app:srcCompat="@android:drawable/ic_dialog_email"/>
|
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
|
@ -1,21 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:padding="@dimen/activity_content_padding"
|
|
||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
|
||||||
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/nav_host_fragment_content_main"
|
|
||||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:defaultNavHost="true"
|
|
||||||
app:navGraph="@navigation/nav_graph"/>
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
|
@ -1,96 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:padding="@dimen/dialog_enter_tan_padding"
|
|
||||||
android:isScrollContainer="true"
|
|
||||||
>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
>
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/tanImageView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="350dp"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:layout_marginBottom="@dimen/dialog_enter_tan_margin_before_enter_tan"
|
|
||||||
android:visibility="gone"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/dialog_enter_tan_tan_description_label"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/txtvwMessageToShowToUser"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
</TextView>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="@dimen/dialog_enter_tan_enter_tan_height"
|
|
||||||
android:layout_marginBottom="@dimen/dialog_enter_tan_enter_tan_margin_bottom"
|
|
||||||
>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="center_vertical"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:text="@string/dialog_enter_tan_enter_tan"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/edtxtEnteredTan"
|
|
||||||
android:layout_width="@dimen/dialog_enter_tan_enter_tan_width"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_marginLeft="@dimen/dialog_enter_tan_enter_tan_margin_left"
|
|
||||||
android:layout_marginStart="@dimen/dialog_enter_tan_enter_tan_margin_left"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
|
|
||||||
<RelativeLayout
|
|
||||||
android:id="@+id/lytButtonBar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btnEnteringTanDone"
|
|
||||||
android:layout_width="@dimen/dialog_enter_tan_buttons_width"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentRight="true"
|
|
||||||
android:layout_alignParentEnd="true"
|
|
||||||
style="?android:attr/buttonBarButtonStyle"
|
|
||||||
android:text="@android:string/ok"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btnCancel"
|
|
||||||
android:layout_width="@dimen/dialog_enter_tan_buttons_width"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_toLeftOf="@+id/btnEnteringTanDone"
|
|
||||||
android:layout_toStartOf="@+id/btnEnteringTanDone"
|
|
||||||
style="?android:attr/buttonBarButtonStyle"
|
|
||||||
android:text="@android:string/cancel"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</ScrollView>
|
|
|
@ -1,19 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<RelativeLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".FirstFragment">
|
|
||||||
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView android:id="@+id/rcyvwAccountTransactions"
|
|
||||||
android:layout_width="match_parent" android:layout_height="match_parent"
|
|
||||||
android:layout_alignParentTop="true"
|
|
||||||
android:layout_alignParentBottom="true"
|
|
||||||
>
|
|
||||||
|
|
||||||
</androidx.recyclerview.widget.RecyclerView>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
|
@ -1,30 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".SecondFragment">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/textview_second"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/button_second"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/button_second"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/previous"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/textview_second"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
/>
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
|
@ -1,89 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="@dimen/list_item_account_transaction_height"
|
|
||||||
android:padding="@dimen/list_item_account_transaction_padding"
|
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_alignParentTop="true"
|
|
||||||
android:layout_alignParentLeft="true"
|
|
||||||
android:layout_alignParentStart="true"
|
|
||||||
android:layout_alignParentBottom="true"
|
|
||||||
android:layout_toLeftOf="@+id/lytAmountAndDate"
|
|
||||||
android:layout_toStartOf="@+id/lytAmountAndDate"
|
|
||||||
android:layout_marginLeft="@dimen/list_item_account_transaction_transaction_text_margin_left_and_right"
|
|
||||||
android:layout_marginStart="@dimen/list_item_account_transaction_transaction_text_margin_left_and_right"
|
|
||||||
android:layout_marginRight="@dimen/list_item_account_transaction_transaction_text_margin_left_and_right"
|
|
||||||
android:layout_marginEnd="@dimen/list_item_account_transaction_transaction_text_margin_left_and_right"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/txtvwBookingText"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/txtvwOtherPartyName"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/list_item_account_transaction_other_party_name_margin_top_and_bottom"
|
|
||||||
android:layout_marginBottom="@dimen/list_item_account_transaction_other_party_name_margin_top_and_bottom"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/txtvwReference"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/lytAmountAndDate"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="@dimen/list_item_account_transaction_amount_width"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_alignParentTop="true"
|
|
||||||
android:layout_alignParentRight="true"
|
|
||||||
android:layout_alignParentEnd="true"
|
|
||||||
android:layout_alignParentBottom="true"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
>
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/imgvwBankIcon"
|
|
||||||
android:layout_width="@dimen/list_item_account_transaction_bank_icon_width_and_height"
|
|
||||||
android:layout_height="@dimen/list_item_account_transaction_bank_icon_width_and_height"
|
|
||||||
android:layout_marginTop="@dimen/list_item_account_transaction_bank_icon_margin_top"
|
|
||||||
android:layout_marginBottom="@dimen/list_item_account_transaction_bank_icon_margin_bottom"
|
|
||||||
android:layout_gravity="end"
|
|
||||||
android:visibility="gone"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/txtvwAmount"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="end"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/txtvwDate"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/list_item_account_transaction_date_margin_top"
|
|
||||||
android:layout_marginBottom="@dimen/list_item_account_transaction_date_margin_bottom"
|
|
||||||
android:gravity="end"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
|
@ -1,9 +0,0 @@
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
tools:context="net.codinux.banking.fints4k.android.MainActivity">
|
|
||||||
<item android:id="@+id/action_settings"
|
|
||||||
android:title="@string/action_settings"
|
|
||||||
android:orderInCategory="100"
|
|
||||||
app:showAsAction="never"/>
|
|
||||||
</menu>
|
|
|
@ -1,5 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
|
@ -1,5 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 982 B |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 7.6 KiB |
|
@ -1,28 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/nav_graph"
|
|
||||||
app:startDestination="@id/FirstFragment">
|
|
||||||
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/FirstFragment"
|
|
||||||
android:name="net.codinux.banking.fints4k.android.FirstFragment"
|
|
||||||
android:label="@string/first_fragment_label"
|
|
||||||
tools:layout="@layout/fragment_first">
|
|
||||||
|
|
||||||
<action
|
|
||||||
android:id="@+id/action_FirstFragment_to_SecondFragment"
|
|
||||||
app:destination="@id/SecondFragment"/>
|
|
||||||
</fragment>
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/SecondFragment"
|
|
||||||
android:name="net.codinux.banking.fints4k.android.SecondFragment"
|
|
||||||
android:label="@string/second_fragment_label"
|
|
||||||
tools:layout="@layout/fragment_second">
|
|
||||||
|
|
||||||
<action
|
|
||||||
android:id="@+id/action_SecondFragment_to_FirstFragment"
|
|
||||||
app:destination="@id/FirstFragment"/>
|
|
||||||
</fragment>
|
|
||||||
</navigation>
|
|
|
@ -1,3 +0,0 @@
|
||||||
<resources>
|
|
||||||
<dimen name="fab_margin">48dp</dimen>
|
|
||||||
</resources>
|
|