Implemented that entered tan now can directly be set on TanChallenge, therefore no need of callback anymore
This commit is contained in:
parent
54c430af2b
commit
b74b165974
|
@ -10,6 +10,7 @@ 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.
|
||||
|
@ -24,14 +25,10 @@ class FirstFragment : Fragment() {
|
|||
// onDestroyView.
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
|
||||
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?) {
|
||||
|
@ -43,8 +40,14 @@ class FirstFragment : Fragment() {
|
|||
adapter = accountTransactionsAdapter
|
||||
}
|
||||
|
||||
val presenter = Presenter() // TODO: inject
|
||||
|
||||
presenter.enterTanCallback = { tanChallenge ->
|
||||
EnterTanDialog().show(tanChallenge, activity!!)
|
||||
}
|
||||
|
||||
// TODO: set your credentials here
|
||||
Presenter().retrieveAccountData("", "", "", "") { response ->
|
||||
presenter.retrieveAccountData("", "", "", "") { response ->
|
||||
if (response.successful) {
|
||||
accountTransactionsAdapter.items = response.retrievedData.flatMap { it.bookedTransactions }
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import kotlinx.datetime.LocalDate
|
|||
import net.dankito.banking.fints.FinTsClientDeprecated
|
||||
import net.dankito.banking.fints.callback.SimpleFinTsClientCallback
|
||||
import net.dankito.banking.fints.model.AddAccountParameter
|
||||
import net.dankito.banking.fints.model.TanChallenge
|
||||
import net.dankito.banking.fints.response.client.AddAccountResponse
|
||||
import net.dankito.utils.multiplatform.extensions.millisSinceEpochAtSystemDefaultTimeZone
|
||||
import org.slf4j.LoggerFactory
|
||||
|
@ -15,7 +16,7 @@ import java.math.BigDecimal
|
|||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
||||
class Presenter {
|
||||
open class Presenter {
|
||||
|
||||
companion object {
|
||||
val ValueDateFormat = DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
|
@ -23,11 +24,17 @@ class Presenter {
|
|||
private val log = LoggerFactory.getLogger(Presenter::class.java)
|
||||
}
|
||||
|
||||
private val fintsClient = FinTsClientDeprecated(SimpleFinTsClientCallback())
|
||||
private val fintsClient = FinTsClientDeprecated(SimpleFinTsClientCallback { challenge -> enterTan(challenge) })
|
||||
|
||||
open var enterTanCallback: ((TanChallenge) -> Unit)? = null
|
||||
|
||||
open protected fun enterTan(tanChallenge: TanChallenge) {
|
||||
enterTanCallback?.invoke(tanChallenge) ?: run { tanChallenge.userDidNotEnterTan() }
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun retrieveAccountData(bankCode: String, customerId: String, pin: String, finTs3ServerAddress: String, retrievedResult: (AddAccountResponse) -> Unit) {
|
||||
open fun retrieveAccountData(bankCode: String, customerId: String, pin: String, finTs3ServerAddress: String, retrievedResult: (AddAccountResponse) -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val response = fintsClient.addAccountAsync(AddAccountParameter(bankCode, customerId, pin, finTs3ServerAddress))
|
||||
log.info("Retrieved response from ${response.bank.bankName} for ${response.bank.customerName}")
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
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()
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
<?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>
|
|
@ -15,4 +15,12 @@
|
|||
<dimen name="list_item_account_transaction_transaction_text_margin_left_and_right">4dp</dimen>
|
||||
<dimen name="list_item_account_transaction_other_party_name_margin_top_and_bottom">6dp</dimen>
|
||||
|
||||
<dimen name="dialog_enter_tan_padding">4dp</dimen>
|
||||
<dimen name="dialog_enter_tan_margin_before_enter_tan">6dp</dimen>
|
||||
<dimen name="dialog_enter_tan_enter_tan_height">50dp</dimen>
|
||||
<dimen name="dialog_enter_tan_enter_tan_width">150dp</dimen>
|
||||
<dimen name="dialog_enter_tan_enter_tan_margin_left">6dp</dimen>
|
||||
<dimen name="dialog_enter_tan_enter_tan_margin_bottom">8dp</dimen>
|
||||
<dimen name="dialog_enter_tan_buttons_width">120dp</dimen>
|
||||
|
||||
</resources>
|
|
@ -9,4 +9,10 @@
|
|||
|
||||
<string name="hello_first_fragment">Hello first fragment</string>
|
||||
<string name="hello_second_fragment">Hello second fragment. Arg: %1$s</string>
|
||||
|
||||
|
||||
<string name="dialog_enter_tan_tan_description_label">Hint from your bank:</string>
|
||||
<string name="dialog_enter_tan_enter_tan">Enter TAN:</string>
|
||||
<string name="dialog_enter_tan_error_could_not_decode_tan_image">Could not decode flicker code or QR code / PhotoTan. Most likely an internal error:\n%s.</string>
|
||||
|
||||
</resources>
|
|
@ -1,8 +1,6 @@
|
|||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.dankito.banking.fints.FinTsClientDeprecated
|
||||
import net.dankito.banking.fints.model.AccountTransaction
|
||||
import net.dankito.banking.fints.model.AddAccountParameter
|
||||
import net.dankito.banking.fints.model.*
|
||||
import react.RBuilder
|
||||
import react.RComponent
|
||||
import react.Props
|
||||
|
@ -11,10 +9,10 @@ import react.dom.*
|
|||
import styled.styledDiv
|
||||
|
||||
external interface AccountTransactionsViewProps : Props {
|
||||
var client: FinTsClientDeprecated
|
||||
var presenter: Presenter
|
||||
}
|
||||
|
||||
data class AccountTransactionsViewState(val balance: String, val transactions: Collection<AccountTransaction>) : State
|
||||
data class AccountTransactionsViewState(val balance: String, val transactions: Collection<AccountTransaction>, val enterTanChallenge: TanChallenge? = null) : State
|
||||
|
||||
@JsExport
|
||||
class AccountTransactionsView(props: AccountTransactionsViewProps) : RComponent<AccountTransactionsViewProps, AccountTransactionsViewState>(props) {
|
||||
|
@ -23,19 +21,31 @@ class AccountTransactionsView(props: AccountTransactionsViewProps) : RComponent<
|
|||
init {
|
||||
state = AccountTransactionsViewState("", listOf())
|
||||
|
||||
props.presenter.enterTanCallback = { setState(AccountTransactionsViewState(state.balance, state.transactions, it)) }
|
||||
|
||||
// due to CORS your bank's servers can not be requested directly from browser -> set a CORS proxy url in main.kt
|
||||
// TODO: set your credentials here
|
||||
GlobalScope.launch {
|
||||
val response = props.client.addAccountAsync(AddAccountParameter("", "", "", ""))
|
||||
if (response.successful) {
|
||||
val balance = response.retrievedData.sumOf { it.balance?.amount?.string?.replace(',', '.')?.toDoubleOrNull() ?: 0.0 } // i know, double is not an appropriate data type for amounts
|
||||
props.presenter.retrieveAccountData("", "", "", "") { response ->
|
||||
if (response.successful) {
|
||||
val balance = response.retrievedData.sumOf { it.balance?.amount?.string?.replace(',', '.')?.toDoubleOrNull() ?: 0.0 } // i know, double is not an appropriate data type for amounts
|
||||
|
||||
setState(AccountTransactionsViewState(balance.toString() + " " + (response.retrievedData.firstOrNull()?.balance?.currency ?: ""), response.retrievedData.flatMap { it.bookedTransactions }))
|
||||
setState(AccountTransactionsViewState(balance.toString() + " " + (response.retrievedData.firstOrNull()?.balance?.currency ?: ""), response.retrievedData.flatMap { it.bookedTransactions }, state.enterTanChallenge))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun RBuilder.render() {
|
||||
state.enterTanChallenge?.let { challenge ->
|
||||
child(EnterTanView::class) {
|
||||
attrs {
|
||||
presenter = props.presenter
|
||||
tanChallenge = challenge
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
+"Saldo: ${state.balance}"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
import io.ktor.util.encodeBase64
|
||||
import kotlinx.html.InputType
|
||||
import kotlinx.html.js.onChangeFunction
|
||||
import kotlinx.html.style
|
||||
import net.dankito.banking.fints.model.ImageTanChallenge
|
||||
import net.dankito.banking.fints.model.TanChallenge
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
import react.Props
|
||||
import react.RBuilder
|
||||
import react.RComponent
|
||||
import react.State
|
||||
import react.dom.*
|
||||
|
||||
external interface EnterTanViewProps : Props {
|
||||
var presenter: Presenter
|
||||
var tanChallenge: TanChallenge
|
||||
}
|
||||
|
||||
data class EnterTanViewState(val enteredTan: String? = null) : State
|
||||
|
||||
@JsExport
|
||||
class EnterTanView(props: EnterTanViewProps) : RComponent<EnterTanViewProps, EnterTanViewState>(props) {
|
||||
|
||||
override fun RBuilder.render() {
|
||||
p {
|
||||
+"Enter TAN:"
|
||||
}
|
||||
|
||||
if (props.tanChallenge is ImageTanChallenge) {
|
||||
val tanImage = (props.tanChallenge as ImageTanChallenge).image
|
||||
if (tanImage.decodingSuccessful) {
|
||||
val base64Encoded = tanImage.imageBytes.encodeBase64()
|
||||
|
||||
img(src = "data:${tanImage.mimeType};base64, $base64Encoded") { }
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
props.tanChallenge.messageToShowToUser
|
||||
}
|
||||
|
||||
div {
|
||||
span { +"TAN:" }
|
||||
|
||||
input {
|
||||
attrs {
|
||||
type = InputType.text
|
||||
onChangeFunction = { event ->
|
||||
val enteredTan = (event.target as HTMLInputElement).value
|
||||
|
||||
setState(EnterTanViewState(enteredTan))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
span { +"Done" }
|
||||
attrs {
|
||||
onMouseUp = {
|
||||
state.enteredTan?.let {
|
||||
props.tanChallenge.userEnteredTan(it)
|
||||
} ?: run { props.tanChallenge.userDidNotEnterTan() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.dankito.banking.fints.FinTsClientDeprecated
|
||||
import net.dankito.banking.fints.callback.SimpleFinTsClientCallback
|
||||
import net.dankito.banking.fints.model.AddAccountParameter
|
||||
import net.dankito.banking.fints.model.TanChallenge
|
||||
import net.dankito.banking.fints.response.client.AddAccountResponse
|
||||
import net.dankito.banking.fints.webclient.KtorWebClient
|
||||
import net.dankito.banking.fints.webclient.ProxyingWebClient
|
||||
import net.dankito.utils.multiplatform.log.LoggerFactory
|
||||
|
||||
open class Presenter {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(Presenter::class)
|
||||
}
|
||||
|
||||
|
||||
// to circumvent CORS we have to use a CORS proxy like the SampleApplications.CorsProxy Application.kt or
|
||||
// https://github.com/Rob--W/cors-anywhere. Set CORS proxy's URL here
|
||||
protected open val fintsClient = FinTsClientDeprecated(SimpleFinTsClientCallback { challenge -> enterTan(challenge) },
|
||||
ProxyingWebClient("http://localhost:8082/", KtorWebClient()))
|
||||
|
||||
open var enterTanCallback: ((TanChallenge) -> Unit)? = null
|
||||
|
||||
open protected fun enterTan(tanChallenge: TanChallenge) {
|
||||
enterTanCallback?.invoke(tanChallenge) ?: run { tanChallenge.userDidNotEnterTan() }
|
||||
}
|
||||
|
||||
|
||||
open fun retrieveAccountData(bankCode: String, customerId: String, pin: String, finTs3ServerAddress: String, retrievedResult: (AddAccountResponse) -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.Unconfined) {
|
||||
val response = fintsClient.addAccountAsync(AddAccountParameter(bankCode, customerId, pin, finTs3ServerAddress))
|
||||
|
||||
log.info("Retrieved response from ${response.bank.bankName} for ${response.bank.customerName}")
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
retrievedResult(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -11,9 +11,7 @@ fun main() {
|
|||
render(document.getElementById("root")!!) {
|
||||
child(AccountTransactionsView::class) {
|
||||
attrs {
|
||||
// to circumvent CORS we have to use a CORS proxy like the SampleApplications.CorsProxy Application.kt or
|
||||
// https://github.com/Rob--W/cors-anywhere. Set CORS proxy's URL here
|
||||
client = FinTsClientDeprecated(SimpleFinTsClientCallback(), ProxyingWebClient("http://localhost:8082/", KtorWebClient()))
|
||||
presenter = Presenter()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
36266AE427BC7776008F3B00 /* Presenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36266AE327BC7776008F3B00 /* Presenter.swift */; };
|
||||
36266AE727BC7801008F3B00 /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36266AE627BC7801008F3B00 /* ViewExtensions.swift */; };
|
||||
36266AEA27BC85D8008F3B00 /* UrlSessionWebClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36266AE927BC85D8008F3B00 /* UrlSessionWebClient.swift */; };
|
||||
36266AEC27C19146008F3B00 /* EnterTanDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36266AEB27C19146008F3B00 /* EnterTanDialog.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
|
@ -25,6 +26,7 @@
|
|||
36266AE327BC7776008F3B00 /* Presenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Presenter.swift; sourceTree = "<group>"; };
|
||||
36266AE627BC7801008F3B00 /* ViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewExtensions.swift; sourceTree = "<group>"; };
|
||||
36266AE927BC85D8008F3B00 /* UrlSessionWebClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlSessionWebClient.swift; sourceTree = "<group>"; };
|
||||
36266AEB27C19146008F3B00 /* EnterTanDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterTanDialog.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -80,6 +82,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
36266AE627BC7801008F3B00 /* ViewExtensions.swift */,
|
||||
36266AEB27C19146008F3B00 /* EnterTanDialog.swift */,
|
||||
);
|
||||
path = ui;
|
||||
sourceTree = "<group>";
|
||||
|
@ -183,6 +186,7 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
36266AEC27C19146008F3B00 /* EnterTanDialog.swift in Sources */,
|
||||
36266AE427BC7776008F3B00 /* Presenter.swift in Sources */,
|
||||
36266AD727BC6F72008F3B00 /* ContentView.swift in Sources */,
|
||||
36266AEA27BC85D8008F3B00 /* UrlSessionWebClient.swift in Sources */,
|
||||
|
|
|
@ -6,7 +6,7 @@ struct ContentView: View {
|
|||
|
||||
@State var transactions: [AccountTransaction] = []
|
||||
|
||||
private let presenter = Presenter()
|
||||
@EnvironmentObject private var presenter: Presenter
|
||||
|
||||
|
||||
var body: some View {
|
||||
|
|
|
@ -1,13 +1,23 @@
|
|||
import SwiftUI
|
||||
import fints4k
|
||||
|
||||
class Presenter {
|
||||
class Presenter : ObservableObject {
|
||||
|
||||
private let fintsClient = FinTsClientDeprecated(callback: SimpleFinTsClientCallback(), webClient: UrlSessionWebClient())
|
||||
// var enterTanCallback: ((TanChallenge) -> Void)? = nil
|
||||
|
||||
// Swift, you're so stupid! It seems to be impossible to initialize SimpleFinTsClientCallback here so that enterTanCallback gets called if set
|
||||
private var fintsClient = iOSFinTsClient(callback: SimpleFinTsClientCallback(), webClient: UrlSessionWebClient())
|
||||
|
||||
private let formatter = DateFormatter()
|
||||
|
||||
|
||||
func setEnterTanCallback(enterTanCallback: @escaping (TanChallenge) -> Void) {
|
||||
self.fintsClient.callback = SimpleFinTsClientCallback( enterTan: { tanChallenge in
|
||||
enterTanCallback(tanChallenge)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
func retrieveTransactions(_ bankCode: String, _ customerId: String, _ pin: String, _ finTs3ServerAddress: String, _ callback: @escaping (AddAccountResponse) -> Void) {
|
||||
self.fintsClient.addAccountAsync(parameter: AddAccountParameter(bankCode: bankCode, customerId: customerId, pin: pin, finTs3ServerAddress: finTs3ServerAddress), callback: callback)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,41 @@
|
|||
|
||||
import SwiftUI
|
||||
import fints4k
|
||||
|
||||
@main
|
||||
struct fints4k_iOSApp: App {
|
||||
|
||||
@StateObject var presenter = Presenter()
|
||||
|
||||
@State private var isShowingEnterTanDialog = false
|
||||
|
||||
// init with a default value
|
||||
@State private var tanChallenge = TanChallenge(messageToShowToUser: "", challenge: "", tanMethod: TanMethod(displayName: "", securityFunction: .pinTan900, type: .entertan, hhdVersion: .hhd13, maxTanInputLength: 6, allowedTanFormat: .numeric, nameOfTanMediumRequired: false, decoupledParameters: nil), tanMediaIdentifier: nil)
|
||||
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
NavigationView {
|
||||
VStack {
|
||||
NavigationLink(destination: EnterTanDialog(tanChallenge), isActive: $isShowingEnterTanDialog) { EmptyView() }
|
||||
|
||||
ContentView()
|
||||
.onAppear {
|
||||
self.initApp()
|
||||
}
|
||||
.environmentObject(presenter)
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initApp() {
|
||||
self.presenter.setEnterTanCallback { tanChallenge in
|
||||
self.tanChallenge = tanChallenge
|
||||
|
||||
self.isShowingEnterTanDialog = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,10 +3,10 @@ import fints4k
|
|||
|
||||
class UrlSessionWebClient : IWebClient {
|
||||
|
||||
func post(url: String, body: String, contentType: String, userAgent: String, callback: @escaping (WebClientResponse) -> Void) {
|
||||
func post(url: String, body: String, contentType: String, userAgent: String, completionHandler: @escaping (WebClientResponse?, Error?) -> Void) {
|
||||
let request = requestFor(url, "POST", body)
|
||||
|
||||
executeRequestAsync(request, callback)
|
||||
executeRequestAsync(request) { response in completionHandler(response, nil) }
|
||||
}
|
||||
|
||||
func getAsync(_ url: String, callback: @escaping (WebClientResponse) -> Void) {
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
import SwiftUI
|
||||
import fints4k
|
||||
|
||||
|
||||
struct EnterTanDialog: View {
|
||||
|
||||
@Environment(\.presentationMode) var presentation
|
||||
|
||||
|
||||
private var tanChallenge: TanChallenge
|
||||
|
||||
private let imageTanChallenge: ImageTanChallenge?
|
||||
|
||||
private let messageToShowToUser: String
|
||||
|
||||
@State private var enteredTan = ""
|
||||
|
||||
private var senteredTanBinding: Binding<String> {
|
||||
Binding<String>(
|
||||
get: { self.enteredTan },
|
||||
set: {
|
||||
self.enteredTan = $0
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@EnvironmentObject private var presenter: Presenter
|
||||
|
||||
|
||||
init(_ tanChallenge: TanChallenge) {
|
||||
self.tanChallenge = tanChallenge
|
||||
|
||||
self.imageTanChallenge = tanChallenge as? ImageTanChallenge
|
||||
|
||||
self.messageToShowToUser = tanChallenge.messageToShowToUser//.htmlToString // parse in init() calling this method in body { } crashes application
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
imageTanChallenge.map { imageTanChallenge in
|
||||
Image(uiImage: UIImage(data: imageTanChallenge.image.imageBytesAsNSData())!)
|
||||
}
|
||||
|
||||
VStack {
|
||||
HStack {
|
||||
Text("TAN hint from your bank:")
|
||||
Spacer()
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text(messageToShowToUser)
|
||||
.multilineTextAlignment(.leading)
|
||||
.lineLimit(5) // hm, we doesn't it show more then three lines?
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 6)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
|
||||
Section {
|
||||
HStack(alignment: .center) {
|
||||
Text("Enter TAN:")
|
||||
|
||||
Spacer()
|
||||
|
||||
TextField("Enter the TAN here", text: self.$enteredTan)
|
||||
.keyboardType(tanChallenge.tanMethod.isNumericTan ? .numberPad : .default)
|
||||
.autocapitalization(.none)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: { self.enteringTanDone() },
|
||||
label: { Text("OK") })
|
||||
.disabled( !self.isRequiredDataEntered())
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Enter TAN")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
|
||||
private func isRequiredDataEntered() -> Bool {
|
||||
return self.enteredTan.isEmpty == false
|
||||
}
|
||||
|
||||
|
||||
private func enteringTanDone() {
|
||||
if self.isRequiredDataEntered() {
|
||||
self.tanChallenge.userEnteredTan(enteredTan: self.enteredTan)
|
||||
} else {
|
||||
self.tanChallenge.userDidNotEnterTan()
|
||||
}
|
||||
|
||||
// TODO: if a TAN has been entered, check result if user has made a mistake and has to re-enter TAN
|
||||
dismissDialog()
|
||||
}
|
||||
|
||||
private func dismissDialog() {
|
||||
presentation.wrappedValue.dismiss()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
//struct EnterTanDialog_Previews: PreviewProvider {
|
||||
//
|
||||
// static var previews: some View {
|
||||
// EnterTanDialog()
|
||||
// }
|
||||
//
|
||||
//}
|
|
@ -1,5 +1,6 @@
|
|||
package net.dankito.banking.fints
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.datetime.LocalDate
|
||||
import net.dankito.banking.fints.messages.MessageBuilder
|
||||
import net.dankito.banking.fints.messages.MessageBuilderResult
|
||||
|
@ -17,7 +18,6 @@ import net.dankito.banking.fints.tan.FlickerCodeDecoder
|
|||
import net.dankito.banking.fints.tan.TanImageDecoder
|
||||
import net.dankito.banking.fints.util.TanMethodSelector
|
||||
import net.dankito.utils.multiplatform.log.LoggerFactory
|
||||
import net.dankito.utils.multiplatform.ObjectReference
|
||||
import net.dankito.utils.multiplatform.extensions.millisSinceEpochAtEuropeBerlin
|
||||
import net.dankito.utils.multiplatform.extensions.minusDays
|
||||
import net.dankito.utils.multiplatform.extensions.todayAtEuropeBerlin
|
||||
|
@ -376,17 +376,25 @@ open class FinTsJobExecutor(
|
|||
|
||||
protected open suspend fun handleEnteringTanRequired(context: JobContext, tanResponse: TanResponse, response: BankResponse): BankResponse {
|
||||
val bank = context.bank // TODO: copy required data to TanChallenge
|
||||
val tanChallenge = createTanChallenge(tanResponse, bank)
|
||||
|
||||
val userDidCancelEnteringTan = ObjectReference(false)
|
||||
// on all platforms run on Dispatchers.Main, but on iOS skip this (or wrap in withContext(Dispatchers.IO) )
|
||||
// val enteredTanResult = GlobalScope.async {
|
||||
val tanChallenge = createTanChallenge(tanResponse, bank)
|
||||
|
||||
val enteredTanResult = context.callback.enterTan(bank, tanChallenge)
|
||||
userDidCancelEnteringTan.value = true
|
||||
context.callback.enterTan(tanChallenge)
|
||||
|
||||
while (tanChallenge.enterTanResult == null) {
|
||||
delay(250)
|
||||
|
||||
mayRetrieveAutomaticallyIfUserEnteredDecoupledTan(context, tanChallenge, tanResponse)
|
||||
|
||||
// TODO: add a timeout of e.g. 30 min
|
||||
}
|
||||
|
||||
val enteredTanResult = tanChallenge.enterTanResult!!
|
||||
// }
|
||||
|
||||
return handleEnterTanResult(context, enteredTanResult, tanResponse, response)
|
||||
|
||||
// TODO:
|
||||
// mayRetrieveAutomaticallyIfUserEnteredDecoupledTan(context, tanChallenge, tanResponse, userDidCancelEnteringTan)
|
||||
}
|
||||
|
||||
protected open fun createTanChallenge(tanResponse: TanResponse, bank: BankData): TanChallenge {
|
||||
|
@ -409,17 +417,15 @@ open class FinTsJobExecutor(
|
|||
}
|
||||
}
|
||||
|
||||
protected open fun mayRetrieveAutomaticallyIfUserEnteredDecoupledTan(context: JobContext, tanChallenge: TanChallenge, tanResponse: TanResponse,
|
||||
userDidCancelEnteringTan: ObjectReference<Boolean>
|
||||
) {
|
||||
protected open fun mayRetrieveAutomaticallyIfUserEnteredDecoupledTan(context: JobContext, tanChallenge: TanChallenge, tanResponse: TanResponse) {
|
||||
context.bank.selectedTanMethod.decoupledParameters?.let { decoupledTanMethodParameters ->
|
||||
if (tanResponse.tanProcess == TanProcess.AppTan && decoupledTanMethodParameters.periodicStateRequestsAllowed) {
|
||||
automaticallyRetrieveIfUserEnteredDecoupledTan(context, tanChallenge, userDidCancelEnteringTan)
|
||||
automaticallyRetrieveIfUserEnteredDecoupledTan(context, tanChallenge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun automaticallyRetrieveIfUserEnteredDecoupledTan(context: JobContext, tanChallenge: TanChallenge, userDidCancelEnteringTan: ObjectReference<Boolean>) {
|
||||
protected open fun automaticallyRetrieveIfUserEnteredDecoupledTan(context: JobContext, tanChallenge: TanChallenge) {
|
||||
log.info("automaticallyRetrieveIfUserEnteredDecoupledTan() called for $tanChallenge")
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ interface FinTsClientCallback {
|
|||
*/
|
||||
suspend fun askUserForTanMethod(supportedTanMethods: List<TanMethod>, suggestedTanMethod: TanMethod?): TanMethod?
|
||||
|
||||
suspend fun enterTan(bank: BankData, tanChallenge: TanChallenge): EnterTanResult
|
||||
suspend fun enterTan(tanChallenge: TanChallenge)
|
||||
|
||||
/**
|
||||
* This method gets called for chipTan TAN generators when the bank asks the customer to synchronize her/his TAN generator.
|
||||
|
|
|
@ -10,8 +10,8 @@ open class NoOpFinTsClientCallback : FinTsClientCallback {
|
|||
return suggestedTanMethod
|
||||
}
|
||||
|
||||
override suspend fun enterTan(bank: BankData, tanChallenge: TanChallenge): EnterTanResult {
|
||||
return EnterTanResult.userDidNotEnterTan()
|
||||
override suspend fun enterTan(tanChallenge: TanChallenge) {
|
||||
return tanChallenge.userDidNotEnterTan()
|
||||
}
|
||||
|
||||
override suspend fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanGeneratorTanMedium): EnterTanGeneratorAtcResult {
|
||||
|
|
|
@ -5,14 +5,14 @@ import net.dankito.banking.fints.model.*
|
|||
|
||||
|
||||
open class SimpleFinTsClientCallback(
|
||||
protected open val enterTan: ((bank: BankData, tanChallenge: TanChallenge) -> EnterTanResult)? = null,
|
||||
protected open val enterTan: ((tanChallenge: TanChallenge) -> Unit)? = null,
|
||||
protected open val enterTanGeneratorAtc: ((bank: BankData, tanMedium: TanGeneratorTanMedium) -> EnterTanGeneratorAtcResult)? = null,
|
||||
protected open val askUserForTanMethod: ((supportedTanMethods: List<TanMethod>, suggestedTanMethod: TanMethod?) -> TanMethod?)? = null
|
||||
) : FinTsClientCallback {
|
||||
|
||||
constructor() : this(null) // Swift does not support default parameter values -> create constructor overloads
|
||||
|
||||
constructor(enterTan: ((bank: BankData, tanChallenge: TanChallenge) -> EnterTanResult)?) : this(enterTan, null)
|
||||
constructor(enterTan: ((tanChallenge: TanChallenge) -> Unit)?) : this(enterTan, null)
|
||||
|
||||
|
||||
override suspend fun askUserForTanMethod(supportedTanMethods: List<TanMethod>, suggestedTanMethod: TanMethod?): TanMethod? {
|
||||
|
@ -20,8 +20,8 @@ open class SimpleFinTsClientCallback(
|
|||
return askUserForTanMethod?.invoke(supportedTanMethods, suggestedTanMethod) ?: suggestedTanMethod
|
||||
}
|
||||
|
||||
override suspend fun enterTan(bank: BankData, tanChallenge: TanChallenge): EnterTanResult {
|
||||
return enterTan?.invoke(bank, tanChallenge) ?: EnterTanResult.userDidNotEnterTan()
|
||||
override suspend fun enterTan(tanChallenge: TanChallenge) {
|
||||
enterTan?.invoke(tanChallenge) ?: run { tanChallenge.userDidNotEnterTan() }
|
||||
}
|
||||
|
||||
override suspend fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanGeneratorTanMedium): EnterTanGeneratorAtcResult {
|
||||
|
|
|
@ -4,33 +4,13 @@ import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanMe
|
|||
import net.dankito.banking.fints.response.client.FinTsClientResponse
|
||||
|
||||
|
||||
open class EnterTanResult protected constructor(
|
||||
open class EnterTanResult(
|
||||
val enteredTan: String?,
|
||||
val changeTanMethodTo: TanMethod? = null,
|
||||
val changeTanMediumTo: TanMedium? = null,
|
||||
val changeTanMediumResultCallback: ((FinTsClientResponse) -> Unit)? = null
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
fun userEnteredTan(enteredTan: String): EnterTanResult {
|
||||
return EnterTanResult(enteredTan.replace(" ", ""))
|
||||
}
|
||||
|
||||
fun userDidNotEnterTan(): EnterTanResult {
|
||||
return EnterTanResult(null)
|
||||
}
|
||||
|
||||
fun userAsksToChangeTanMethod(changeTanMethodTo: TanMethod): EnterTanResult {
|
||||
return EnterTanResult(null, changeTanMethodTo)
|
||||
}
|
||||
|
||||
fun userAsksToChangeTanMedium(changeTanMediumTo: TanMedium, changeTanMediumResultCallback: ((FinTsClientResponse) -> Unit)?): EnterTanResult {
|
||||
return EnterTanResult(null, null, changeTanMediumTo, changeTanMediumResultCallback)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
if (changeTanMethodTo != null) {
|
||||
return "User asks to change TAN method to $changeTanMethodTo"
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
package net.dankito.banking.fints.model
|
||||
|
||||
import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanMedium
|
||||
import net.dankito.banking.fints.response.client.FinTsClientResponse
|
||||
|
||||
|
||||
open class TanChallenge(
|
||||
val messageToShowToUser: String,
|
||||
|
@ -8,6 +11,30 @@ open class TanChallenge(
|
|||
val tanMediaIdentifier: String?
|
||||
) {
|
||||
|
||||
var enterTanResult: EnterTanResult? = null
|
||||
private set
|
||||
|
||||
open val isEnteringTanDone: Boolean
|
||||
get() = enterTanResult != null
|
||||
|
||||
|
||||
fun userEnteredTan(enteredTan: String) {
|
||||
this.enterTanResult = EnterTanResult(enteredTan.replace(" ", ""))
|
||||
}
|
||||
|
||||
fun userDidNotEnterTan() {
|
||||
this.enterTanResult = EnterTanResult(null)
|
||||
}
|
||||
|
||||
fun userAsksToChangeTanMethod(changeTanMethodTo: TanMethod) {
|
||||
this.enterTanResult = EnterTanResult(null, changeTanMethodTo)
|
||||
}
|
||||
|
||||
fun userAsksToChangeTanMedium(changeTanMediumTo: TanMedium, changeTanMediumResultCallback: ((FinTsClientResponse) -> Unit)?) {
|
||||
this.enterTanResult = EnterTanResult(null, null, changeTanMediumTo, changeTanMediumResultCallback)
|
||||
}
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
return "$tanMethod (medium: $tanMediaIdentifier): $messageToShowToUser"
|
||||
}
|
||||
|
|
|
@ -20,6 +20,10 @@ open class TanMethod(
|
|||
|
||||
|
||||
|
||||
open val isNumericTan: Boolean
|
||||
get() = allowedTanFormat == AllowedTanFormat.Numeric
|
||||
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is TanMethod) return false
|
||||
|
|
|
@ -20,10 +20,6 @@ interface IWebClient {
|
|||
}
|
||||
|
||||
|
||||
suspend fun post(url: String, body: String): WebClientResponse { // some platforms don't support default parameters
|
||||
return post(url, body)
|
||||
}
|
||||
|
||||
suspend fun post(url: String, body: String, contentType: String = "application/octet-stream", userAgent: String = DefaultUserAgent): WebClientResponse
|
||||
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package net.dankito.banking.fints.extensions
|
||||
|
||||
import net.dankito.banking.fints.tan.TanImage
|
||||
import net.dankito.utils.multiplatform.extensions.toNSData
|
||||
import kotlinx.cinterop.*
|
||||
import platform.Foundation.*
|
||||
|
||||
|
||||
fun TanImage.imageBytesAsNSData(): NSData {
|
||||
return imageBytes.toNSData()
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package net.dankito.banking.fints
|
||||
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import net.dankito.banking.fints.callback.FinTsClientCallback
|
||||
import net.dankito.banking.fints.model.AddAccountParameter
|
||||
import net.dankito.banking.fints.response.client.AddAccountResponse
|
||||
import net.dankito.banking.fints.webclient.IWebClient
|
||||
|
||||
open class iOSFinTsClient(
|
||||
callback: FinTsClientCallback,
|
||||
webClient: IWebClient
|
||||
) {
|
||||
|
||||
protected open val fintsClient = FinTsClientDeprecated(callback, FinTsJobExecutor(RequestExecutor(webClient = webClient)))
|
||||
|
||||
open var callback: FinTsClientCallback
|
||||
get() = fintsClient.callback
|
||||
set(value) {
|
||||
fintsClient.callback = value
|
||||
}
|
||||
|
||||
|
||||
open fun addAccountAsync(parameter: AddAccountParameter, callback: (AddAccountResponse) -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.Main) { // do not block UI thread as with runBlocking { } but stay on UI thread as passing mutable state between threads currently doesn't work in Kotlin/Native
|
||||
callback(fintsClient.addAccountAsync(parameter))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -4,6 +4,7 @@ import net.dankito.banking.fints.FinTsClientDeprecated
|
|||
import net.dankito.banking.fints.callback.SimpleFinTsClientCallback
|
||||
import net.dankito.banking.fints.model.AddAccountParameter
|
||||
import net.dankito.banking.fints.model.RetrievedAccountData
|
||||
import net.dankito.banking.fints.model.TanChallenge
|
||||
import net.dankito.banking.fints.response.client.AddAccountResponse
|
||||
import net.dankito.utils.multiplatform.extensions.*
|
||||
import platform.posix.exit
|
||||
|
@ -22,7 +23,7 @@ class Application {
|
|||
|
||||
fun retrieveAccountData(bankCode: String, customerId: String, pin: String, finTs3ServerAddress: String) {
|
||||
runBlocking {
|
||||
val client = FinTsClientDeprecated(SimpleFinTsClientCallback())
|
||||
val client = FinTsClientDeprecated(SimpleFinTsClientCallback { tanChallenge -> enterTan(tanChallenge) })
|
||||
|
||||
val response = client.addAccountAsync(AddAccountParameter(bankCode, customerId, pin, finTs3ServerAddress))
|
||||
|
||||
|
@ -32,6 +33,23 @@ class Application {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private fun enterTan(tanChallenge: TanChallenge) {
|
||||
println("A TAN is required:")
|
||||
println(tanChallenge.messageToShowToUser)
|
||||
println()
|
||||
|
||||
print("TAN: ")
|
||||
val enteredTan = readLine()
|
||||
|
||||
if (enteredTan.isNullOrBlank()) {
|
||||
tanChallenge.userDidNotEnterTan()
|
||||
} else {
|
||||
tanChallenge.userEnteredTan(enteredTan)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun displayRetrievedAccountData(response: AddAccountResponse) {
|
||||
if (response.retrievedData.isEmpty()) {
|
||||
println()
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package net.dankito.utils.multiplatform.extensions
|
||||
|
||||
import kotlinx.cinterop.*
|
||||
import platform.Foundation.*
|
||||
|
||||
|
||||
fun ByteArray.toNSData(): NSData = NSMutableData().apply {
|
||||
if (isEmpty()) return@apply
|
||||
this@toNSData.usePinned {
|
||||
appendBytes(it.addressOf(0), size.convert())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue