Compare commits
No commits in common. "5e7d8804992f671c4d7232970874739bfe9fd96b" and "345f84c0b201f6538db52ca10414e6723473ab20" have entirely different histories.
5e7d880499
...
345f84c0b2
|
@ -1,12 +1,9 @@
|
|||
.idea/
|
||||
*.iml
|
||||
|
||||
build/
|
||||
**/build
|
||||
out/
|
||||
**/out
|
||||
/captures
|
||||
**/kotlin-js-store
|
||||
**/target
|
||||
|
||||
.gradle
|
||||
.kotlin
|
||||
|
@ -18,17 +15,3 @@ out/
|
|||
|
||||
/data/
|
||||
**/*.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 {
|
||||
|
||||
CheckingAccount,
|
||||
|
||||
SavingsAccount,
|
||||
|
@ -22,5 +20,4 @@ enum class BankAccountType {
|
|||
InsuranceContract,
|
||||
|
||||
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 {
|
||||
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(
|
||||
val challengeHHD_UC: String,
|
||||
val parsedDataSet: String,
|
||||
val decodingError: Exception? = null
|
||||
val decodingError: String? = null
|
||||
) {
|
||||
|
||||
@get:JsonIgnore
|
||||
val decodingSuccessful: Boolean
|
||||
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 {
|
||||
|
||||
EnterTan,
|
||||
|
||||
ChipTanManuell,
|
||||
|
@ -22,5 +20,4 @@ enum class TanMethodType {
|
|||
photoTan,
|
||||
|
||||
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).
|
||||
|
||||
However it's not a full implementation of FinTS standard but implements all common use cases:
|
||||
|
||||
## 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.
|
||||
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
|
||||
not each project has the implement to model again.
|
||||
|
||||
## Setup
|
||||
Not uploaded to Maven Central yet, will do this the next few days!
|
||||
|
||||
Gradle:
|
||||
### Gradle:
|
||||
|
||||
```
|
||||
dependencies {
|
||||
compile 'net.dankito.banking:fints4k:0.1.0'
|
||||
plugins {
|
||||
kotlin("jvm") version "2.0.10" // or kotlin("multiplatform"), depending on your requirements
|
||||
}
|
||||
```
|
||||
|
||||
Maven:
|
||||
```
|
||||
<dependency>
|
||||
<groupId>net.dankito.banking</groupId>
|
||||
<artifactId>fints4k</artifactId>
|
||||
<version>0.1.0</version>
|
||||
</dependency>
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
setUrl("https://maven.dankito.net/api/packages/codinux/maven")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
dependencies {
|
||||
implementation("net.codinux.banking.client:fints4k-banking-client:0.5.0")
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 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
|
||||
// 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>");
|
||||
Retrieves data like accounts, balance and booked transactions (Konten, Saldo und Kontoumsätze).
|
||||
|
||||
if (foundBanks.isEmpty() == false) { // please also check if bank supports FinTS 3.0
|
||||
BankData bank = new BankDataMapper().mapFromBankInfo(foundBanks.get(0));
|
||||
Basically:
|
||||
|
||||
// set your customer data (customerId = username you use to log in; pin = online banking pin / password)
|
||||
CustomerData customer = new CustomerData("<customer_id>", "<pin>");
|
||||
```kotlin
|
||||
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) {
|
||||
System.out.println("Account transactions of last 90 days:");
|
||||
showGetTransactionsResponse(addAccountResponse);
|
||||
fun getAccountData() {
|
||||
val client = FinTs4kBankingClientForCustomer(bankCode, loginName, password, SimpleBankingClientCallback())
|
||||
|
||||
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.
|
||||
|
||||
|
||||
## Sample applications
|
||||
|
||||
### 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
|
||||
```kotlin
|
||||
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
|
||||
})
|
||||
```
|
||||
|
||||
## 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>
|