Added EnterTanDialog

This commit is contained in:
dankito 2024-08-27 01:33:38 +02:00
parent b6b88d31a1
commit 7d39f94271
11 changed files with 435 additions and 95 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -5,12 +5,14 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import net.codinux.banking.ui.dialogs.ApplicationErrorDialog import net.codinux.banking.ui.dialogs.ApplicationErrorDialog
import net.codinux.banking.ui.dialogs.BankingClientErrorDialog import net.codinux.banking.ui.dialogs.BankingClientErrorDialog
import net.codinux.banking.ui.dialogs.EnterTanDialog
import net.codinux.banking.ui.state.UiState import net.codinux.banking.ui.state.UiState
@Composable @Composable
fun StateHandler(uiState: UiState) { fun StateHandler(uiState: UiState) {
val applicationError by uiState.applicationErrorOccurred.collectAsState() val applicationError by uiState.applicationErrorOccurred.collectAsState()
val bankingClientError by uiState.bankingClientErrorOccurred.collectAsState() val bankingClientError by uiState.bankingClientErrorOccurred.collectAsState()
val tanChallengeReceived by uiState.tanChallengeReceived.collectAsState()
applicationError?.let { error -> applicationError?.let { error ->
ApplicationErrorDialog(error) { ApplicationErrorDialog(error) {
@ -24,4 +26,10 @@ fun StateHandler(uiState: UiState) {
} }
} }
tanChallengeReceived?.let { tanChallengeReceived ->
EnterTanDialog(tanChallengeReceived) {
uiState.tanChallengeReceived.value = null
}
}
} }

View File

@ -1,9 +1,22 @@
package net.codinux.banking.ui.config package net.codinux.banking.ui.config
import net.codinux.banking.client.model.tan.ActionRequiringTan
object Internationalization { object Internationalization {
const val ErrorAddAccount = "Konto konnte nicht hinzugefügt werden" const val ErrorAddAccount = "Konto konnte nicht hinzugefügt werden"
const val ErrorUpdateAccountTransactions = "Umsätze konnten nicht aktualisiert werden" const val ErrorUpdateAccountTransactions = "Umsätze konnten nicht aktualisiert werden"
fun getTextForActionRequiringTan(action: ActionRequiringTan): String = when (action) {
ActionRequiringTan.GetAnonymousBankInfo,
ActionRequiringTan.GetAccountInfo,
ActionRequiringTan.GetTanMedia
-> "zum Einloggen"
ActionRequiringTan.GetTransactions -> "um Kontoumsätze abzuholen"
ActionRequiringTan.TransferMoney -> "um Geld zu überweisen"
ActionRequiringTan.ChangeTanMedium -> "um das TAN Medium zu ändern"
}
} }

View File

@ -40,97 +40,65 @@ fun AddAccountDialog(
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
Dialog(onDismissRequest = onDismiss) { BaseDialog(
RoundedCornersCard { title = "Bank Konto hinzufügen",
Column(Modifier.background(Color.White).padding(8.dp)) { confirmButtonTitle = "Hinzufügen",
confirmButtonEnabled = isRequiredDataEntered && isAddingAccount == false,
showProgressIndicatorOnConfirmButton = isAddingAccount,
onDismiss = onDismiss,
onConfirm = {
selectedBank?.let {
isAddingAccount = true
Row(Modifier.fillMaxWidth()) { coroutineScope.launch { // TODO: launch on Dispatchers.IO where it is available
Text( val successful = DI.bankingService.addAccount(selectedBank!!, loginName, password)
"Bank Konto hinzufügen",
color = Style.HeaderTextColor,
fontSize = Style.HeaderFontSize,
fontWeight = Style.HeaderFontWeight,
modifier = Modifier.padding(top = 8.dp, bottom = 16.dp).weight(1f)
)
TextButton(onDismiss, colors = ButtonDefaults.buttonColors(contentColor = Colors.Zinc700, backgroundColor = Color.Transparent)) { withContext(Dispatchers.Main) {
Icon(Icons.Filled.Close, contentDescription = "Close dialog", Modifier.size(32.dp)) isAddingAccount = false
}
}
AutocompleteTextField( if (successful) {
onValueChange = { selectedBank = it }, onDismiss()
label = { Text("Bank (Suche mit Name, Bankleitzahl oder Ort)") },
getItemTitle = { bank -> bank.name },
fetchSuggestions = { query -> bankingService.findBanks(query) }
) { bank ->
Text(bank.name)
Row(Modifier.fillMaxWidth().padding(top = 8.dp)) {
Text(bank.bankCode)
Text("${bank.postalCode} ${bank.city}", Modifier.weight(1f).padding(start = 8.dp), color = Color.Gray)
}
}
Spacer(modifier = Modifier.height(24.dp))
Text("Online-Banking Zugangsdaten", color = Colors.CodinuxSecondaryColor)
Spacer(modifier = Modifier.height(12.dp))
OutlinedTextField(
value = loginName,
onValueChange = { loginName = it },
label = { Text("Login Name") },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(12.dp))
PasswordTextField(password) { password = it }
Spacer(modifier = Modifier.height(16.dp))
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
TextButton(onClick = onDismiss, Modifier.width(Style.DialogButtonWidth)) {
Text("Abbrechen")
}
Spacer(Modifier.width(8.dp))
TextButton(
modifier = Modifier.width(Style.DialogButtonWidth),
enabled = isRequiredDataEntered && isAddingAccount == false,
onClick = {
selectedBank?.let {
isAddingAccount = true
coroutineScope.launch { // TODO: launch on Dispatchers.IO where it is available
val successful = DI.bankingService.addAccount(selectedBank!!, loginName, password)
withContext(Dispatchers.Main) {
isAddingAccount = false
if (successful) {
onDismiss()
}
}
}
}
}
) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (isAddingAccount) {
CircularProgressIndicator(Modifier.padding(end = 6.dp))
}
Text("Hinzufügen")
} }
} }
} }
} }
} }
) {
Column {
AutocompleteTextField(
onValueChange = { selectedBank = it },
label = { Text("Bank (Suche mit Name, Bankleitzahl oder Ort)") },
getItemTitle = { bank -> bank.name },
fetchSuggestions = { query -> bankingService.findBanks(query) }
) { bank ->
Text(bank.name)
Row(Modifier.fillMaxWidth().padding(top = 8.dp)) {
Text(bank.bankCode)
Text("${bank.postalCode} ${bank.city}", Modifier.weight(1f).padding(start = 8.dp), color = Color.Gray)
}
}
Spacer(modifier = Modifier.height(24.dp))
Text("Online-Banking Zugangsdaten", color = Colors.CodinuxSecondaryColor)
Spacer(modifier = Modifier.height(12.dp))
OutlinedTextField(
value = loginName,
onValueChange = { loginName = it },
label = { Text("Login Name") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(12.dp))
PasswordTextField(password) { password = it }
Spacer(modifier = Modifier.height(16.dp))
}
} }
} }

View File

@ -0,0 +1,76 @@
package net.codinux.banking.ui.dialogs
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.Style
import net.codinux.banking.ui.forms.*
@Composable
fun BaseDialog(
title: String,
confirmButtonTitle: String = "OK",
confirmButtonEnabled: Boolean = true,
showProgressIndicatorOnConfirmButton: Boolean = false,
onDismiss: () -> Unit,
onConfirm: (() -> Unit)? = null,
properties: DialogProperties = DialogProperties(),
content: @Composable () -> Unit
) {
Dialog(onDismissRequest = onDismiss, properties) {
RoundedCornersCard {
Column(Modifier.background(Color.White).padding(8.dp)) {
Row(Modifier.fillMaxWidth()) {
Text(
title,
color = Style.HeaderTextColor,
fontSize = Style.HeaderFontSize,
fontWeight = Style.HeaderFontWeight,
modifier = Modifier.padding(top = 8.dp, bottom = 16.dp).weight(1f)
)
TextButton(onDismiss, colors = ButtonDefaults.buttonColors(contentColor = Colors.Zinc700, backgroundColor = Color.Transparent)) {
Icon(Icons.Filled.Close, contentDescription = "Close dialog", Modifier.size(32.dp))
}
}
content()
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
TextButton(onClick = onDismiss, Modifier.width(Style.DialogButtonWidth)) {
Text("Abbrechen", color = Colors.CodinuxSecondaryColor)
}
Spacer(Modifier.width(8.dp))
TextButton(
modifier = Modifier.width(Style.DialogButtonWidth),
enabled = confirmButtonEnabled,
onClick = { onConfirm?.invoke() ?: onDismiss() }
) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (showProgressIndicatorOnConfirmButton) {
CircularProgressIndicator(Modifier.padding(end = 6.dp))
}
Text(confirmButtonTitle, color = Colors.CodinuxSecondaryColor)
}
}
}
}
}
}
}

View File

@ -0,0 +1,214 @@
package net.codinux.banking.ui.dialogs
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toComposeImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import bankmeister.composeapp.generated.resources.Res
import net.codinux.banking.client.model.CustomerAccountViewInfo
import net.codinux.banking.client.model.tan.*
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.config.Internationalization
import net.codinux.banking.ui.forms.OutlinedTextField
import net.codinux.banking.ui.model.TanChallengeReceived
import net.codinux.log.Log
import org.jetbrains.compose.resources.imageResource
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.jetbrains.skia.Image
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import bankmeister.composeapp.generated.resources.zoom_in
import bankmeister.composeapp.generated.resources.zoom_out
@OptIn(ExperimentalEncodingApi::class, ExperimentalMaterialApi::class)
@Composable
fun EnterTanDialog(tanChallengeReceived: TanChallengeReceived, onDismiss: () -> Unit) {
val challenge = tanChallengeReceived.tanChallenge
var showTanMethodsDropDownMenu by remember { mutableStateOf(false) }
var showTanMediaDropDownMenu by remember { mutableStateOf(false) }
var tanImageHeight by remember { mutableStateOf(250) }
val minTanImageHeight = 100
val maxTanImageHeight = 500
val textFieldFocus = remember { FocusRequester() }
var enteredTan by remember { mutableStateOf("") }
BaseDialog(
title = "TAN Eingabe",
confirmButtonEnabled = enteredTan.length > 2,
onConfirm = {
tanChallengeReceived.callback(EnterTanResult(enteredTan))
onDismiss()
},
onDismiss = {
tanChallengeReceived.callback(EnterTanResult(null))
onDismiss()
}
) {
Column(Modifier.fillMaxWidth()) {
Column(Modifier.fillMaxWidth()) {
Row {
Text("${challenge.customer.bankName}, Nutzer ${challenge.customer.loginName}${challenge.account?.let { ", Konto ${it.productName ?: it.identifier}" } ?: ""}")
}
Text(
"TAN benötigt ${Internationalization.getTextForActionRequiringTan(challenge.forAction)}",
Modifier.padding(top = 6.dp)
)
}
Row(Modifier.padding(top = 16.dp)) {
ExposedDropdownMenuBox(showTanMethodsDropDownMenu, { isExpanded -> showTanMethodsDropDownMenu = isExpanded }, Modifier.fillMaxWidth()) {
OutlinedTextField(
value = challenge.selectedTanMethod.displayName,
onValueChange = { Log.info { "TanMethod value changed: $it" }},
modifier = Modifier.fillMaxWidth(),
label = { Text("TAN Verfahren") },
readOnly = true,
trailingIcon = { Icon(Icons.Filled.ArrowDropDown, "Alle TAN Medien anzeigen") }
)
ExposedDropdownMenu(showTanMethodsDropDownMenu, { showTanMethodsDropDownMenu = false }) {
challenge.availableTanMethods.sortedBy { it.identifier }.forEach { tanMethod ->
DropdownMenuItem(
onClick = {
showTanMethodsDropDownMenu = false
Log.info { "User selected TanMethod $tanMethod" }
// TODO: change TanMethod
}
) {
Text(tanMethod.displayName)
}
}
}
}
}
if (challenge.availableTanMedia.isNotEmpty()) {
Row(Modifier.padding(top = 16.dp)) {
ExposedDropdownMenuBox(showTanMediaDropDownMenu, { isExpanded -> showTanMediaDropDownMenu = isExpanded }, Modifier.fillMaxWidth()) {
OutlinedTextField(
value = challenge.selectedTanMedium?.let { getTanMediumDisplayName(it) } ?: "<Keines ausgewählt>",
onValueChange = { Log.info { "TanMedia value changed: $it" }},
modifier = Modifier.fillMaxWidth(),
label = { Text("TAN Medium") },
readOnly = true,
trailingIcon = { Icon(Icons.Filled.ArrowDropDown, "Alle TAN Verfahren anzeigen") }
)
ExposedDropdownMenu(showTanMediaDropDownMenu, { showTanMediaDropDownMenu = false }) {
challenge.availableTanMedia.sortedBy { it.status }.forEach { tanMedium ->
DropdownMenuItem(
onClick = {
showTanMediaDropDownMenu = false
Log.info { "User selected TanMedium $tanMedium" }
// TODO: change TanMethod
}
) {
Text(getTanMediumDisplayName(tanMedium))
}
}
}
}
}
}
if (challenge.tanImage != null || challenge.flickerCode != null) {
Column(Modifier.fillMaxWidth().padding(top = 12.dp)) {
if (challenge.flickerCode != null) {
Text("Es tut uns Leid, für die TAN müsste ein Flickercode angezeigt werden, was wir noch nicht implementiert haben.")
Text("Bitte wählen Sie ein anderes TAN Verfahren, z. B. manuelle TAN Eingabe wie chipTAN manuell.", Modifier.padding(top = 6.dp))
}
challenge.tanImage?.let { tanImage ->
if (tanImage.decodingSuccessful) {
val byteArray = Base64.decode(tanImage.imageBytesBase64)
val bitmap = Image.makeFromEncoded(byteArray)
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
Text("Größe")
TextButton({ tanImageHeight -= 25}, enabled = tanImageHeight > minTanImageHeight, colors = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent)) {
Icon(imageResource(Res.drawable.zoom_out), contentDescription = "Bild mit enkodierter TAN verkleiner", Modifier.size(32.dp).padding(horizontal = 6.dp))
}
TextButton({ tanImageHeight += 25}, enabled = tanImageHeight < maxTanImageHeight, colors = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent)) {
Icon(imageResource(Res.drawable.zoom_in), contentDescription = "Bild mit enkodierter TAN vergrößern", Modifier.size(32.dp))
}
}
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
Image(bitmap.toComposeImageBitmap(), "Bild mit enkodierter TAN", Modifier.height(tanImageHeight.dp), contentScale = ContentScale.FillHeight)
}
}
}
}
}
Column(Modifier.padding(top = 16.dp)) {
Text("Hinweis Ihrer Bank:", Modifier.padding(bottom = 6.dp))
Text(challenge.messageToShowToUser)
}
Column(Modifier.fillMaxWidth().padding(top = 16.dp)) {
OutlinedTextField(
value = enteredTan,
onValueChange = { enteredTan = it },
label = { Text("TAN eingeben") },
modifier = Modifier.fillMaxWidth().focusRequester(textFieldFocus),
keyboardOptions = KeyboardOptions(
autoCorrect = false,
keyboardType = if (challenge.selectedTanMethod.allowedTanFormat == AllowedTanFormat.Numeric) KeyboardType.Number else KeyboardType.Text
)
)
}
}
}
LaunchedEffect(textFieldFocus) {
textFieldFocus.requestFocus()
}
}
fun getTanMediumDisplayName(tanMedium: net.codinux.banking.client.model.tan.TanMedium): String {
tanMedium.tanGenerator?.let { tanGenerator ->
return "${tanMedium.mediumName} ${tanGenerator.cardNumber}"
}
tanMedium.mobilePhone?.let { mobilePhone ->
return "${tanMedium.mediumName} ${mobilePhone.concealedPhoneNumber ?: mobilePhone.phoneNumber}"
}
return tanMedium.mediumName ?: ""
}
@Preview
@Composable
fun EnterTanDialogPreview() {
val tanMethod = TanMethod("photoTan", TanMethodType.photoTan, "910", 6, AllowedTanFormat.Numeric)
val tanImage = TanImage("image/png", "")
val customer = CustomerAccountViewInfo("10010010", "Ihr krasser Login Name", "Phantasie Bank")
val tanChallenge = TanChallenge(TanChallengeType.Image, ActionRequiringTan.GetAccountInfo, "Geben Sie die TAN ein", tanMethod.identifier, listOf(tanMethod), null, emptyList(), tanImage, null, customer)
EnterTanDialog(TanChallengeReceived(tanChallenge, { })) { }
}

View File

@ -0,0 +1,11 @@
package net.codinux.banking.ui.model
import net.codinux.banking.client.model.tan.EnterTanResult
import net.codinux.banking.client.model.tan.TanChallenge
data class TanChallengeReceived(
val tanChallenge: TanChallenge,
val callback: (EnterTanResult) -> Unit
) {
override fun toString() = "$tanChallenge"
}

View File

@ -3,21 +3,22 @@ package net.codinux.banking.ui.service
import bankmeister.composeapp.generated.resources.Res import bankmeister.composeapp.generated.resources.Res
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
import net.codinux.banking.client.SimpleBankingClientCallback import net.codinux.banking.client.SimpleBankingClientCallback
import net.codinux.banking.client.fints4k.FinTs4kBankingClientForCustomer import net.codinux.banking.client.fints4k.FinTs4kBankingClient
import net.codinux.banking.client.model.AccountTransaction import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.client.model.Amount import net.codinux.banking.client.model.Amount
import net.codinux.banking.client.model.response.GetAccountDataResponse import net.codinux.banking.client.model.options.GetAccountDataOptions
import net.codinux.banking.client.model.response.Response import net.codinux.banking.client.model.options.RetrieveTransactions
import net.codinux.banking.client.model.response.ResponseType import net.codinux.banking.client.model.request.GetAccountDataRequest
import net.codinux.banking.client.model.response.*
import net.codinux.banking.fints.config.FinTsClientConfiguration import net.codinux.banking.fints.config.FinTsClientConfiguration
import net.codinux.banking.fints.config.FinTsClientOptions import net.codinux.banking.fints.config.FinTsClientOptions
import net.codinux.banking.ui.model.BankInfo import net.codinux.banking.ui.model.BankInfo
import net.codinux.banking.ui.model.TanChallengeReceived
import net.codinux.banking.ui.model.error.BankingClientAction import net.codinux.banking.ui.model.error.BankingClientAction
import net.codinux.banking.ui.model.error.BankingClientError import net.codinux.banking.ui.model.error.BankingClientError
import net.codinux.banking.ui.model.error.ErroneousAction import net.codinux.banking.ui.model.error.ErroneousAction
import net.codinux.banking.ui.state.UiState import net.codinux.banking.ui.state.UiState
import net.codinux.csv.reader.CsvReader import net.codinux.csv.reader.CsvReader
import net.codinux.log.Log
import net.codinux.log.logger import net.codinux.log.logger
import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.ExperimentalResourceApi
@ -27,6 +28,10 @@ class BankingService(
private val bankFinder: BankFinder private val bankFinder: BankFinder
) { ) {
private val client = FinTs4kBankingClient(FinTsClientConfiguration(FinTsClientOptions(true)), SimpleBankingClientCallback { tanChallenge, callback ->
uiState.tanChallengeReceived.value = TanChallengeReceived(tanChallenge, callback)
})
private val log by logger() private val log by logger()
@ -41,12 +46,7 @@ class BankingService(
suspend fun addAccount(bank: BankInfo, loginName: String, password: String): Boolean { suspend fun addAccount(bank: BankInfo, loginName: String, password: String): Boolean {
try { try {
val config = FinTsClientConfiguration(FinTsClientOptions(true)) val response = client.getAccountDataAsync(GetAccountDataRequest(bank.bankCode, loginName, password, GetAccountDataOptions(retrieveTransactions = RetrieveTransactions.All)))
val client = FinTs4kBankingClientForCustomer(bank.bankCode, loginName, password, config, SimpleBankingClientCallback { tanChallenge, callback ->
// TODO: show EnterTanDialog
})
val response = client.getAccountDataAsync()
if (response.type == ResponseType.Success && response.data != null) { if (response.type == ResponseType.Success && response.data != null) {
handleSuccessfulGetAccountDataResponse(response.data!!) handleSuccessfulGetAccountDataResponse(response.data!!)

View File

@ -3,6 +3,7 @@ package net.codinux.banking.ui.state
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import net.codinux.banking.client.model.AccountTransaction import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.ui.model.TanChallengeReceived
import net.codinux.banking.ui.model.error.ApplicationError import net.codinux.banking.ui.model.error.ApplicationError
import net.codinux.banking.ui.model.error.BankingClientError import net.codinux.banking.ui.model.error.BankingClientError
import net.codinux.banking.ui.model.error.ErroneousAction import net.codinux.banking.ui.model.error.ErroneousAction
@ -15,6 +16,8 @@ class UiState : ViewModel() {
val bankingClientErrorOccurred = MutableStateFlow<BankingClientError?>(null) val bankingClientErrorOccurred = MutableStateFlow<BankingClientError?>(null)
val tanChallengeReceived = MutableStateFlow<TanChallengeReceived?>(null)
fun applicationErrorOccurred(erroneousAction: ErroneousAction, exception: Throwable, errorMessage: String? = null) { fun applicationErrorOccurred(erroneousAction: ErroneousAction, exception: Throwable, errorMessage: String? = null) {
val message = errorMessage val message = errorMessage

View File

@ -69,6 +69,11 @@
resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273" resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273"
integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg== integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg==
"@js-joda/timezone@2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@js-joda/timezone/-/timezone-2.3.0.tgz#72878f6dc8afef20c29906e5d8d946f91618a2c3"
integrity sha512-DHXdNs0SydSqC5f0oRJPpTcNfnpRojgBqMCFupQFv6WgeZAjU3DBx+A7JtaGPP3dHrP2Odi2N8Vf+uAm/8ynCQ==
"@jsonjoy.com/base64@^1.1.1": "@jsonjoy.com/base64@^1.1.1":
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz#cf8ea9dcb849b81c95f14fc0aaa151c6b54d2578" resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz#cf8ea9dcb849b81c95f14fc0aaa151c6b54d2578"
@ -422,6 +427,13 @@
resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
abort-controller@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
dependencies:
event-target-shim "^5.0.0"
accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8:
version "1.3.8" version "1.3.8"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
@ -1082,6 +1094,11 @@ etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
event-target-shim@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
eventemitter3@^4.0.0: eventemitter3@^4.0.0:
version "4.0.7" version "4.0.7"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
@ -1991,6 +2008,13 @@ neo-async@^2.6.2:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
node-fetch@2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
dependencies:
whatwg-url "^5.0.0"
node-forge@^1: node-forge@^1:
version "1.3.1" version "1.3.1"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3"
@ -2734,6 +2758,11 @@ toidentifier@1.0.1:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
tree-dump@^1.0.1: tree-dump@^1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.0.2.tgz#c460d5921caeb197bde71d0e9a7b479848c5b8ac" resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.0.2.tgz#c460d5921caeb197bde71d0e9a7b479848c5b8ac"
@ -2832,6 +2861,11 @@ wbuf@^1.1.0, wbuf@^1.7.3:
dependencies: dependencies:
minimalistic-assert "^1.0.0" minimalistic-assert "^1.0.0"
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
webpack-cli@5.1.4: webpack-cli@5.1.4:
version "5.1.4" version "5.1.4"
resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.4.tgz#c8e046ba7eaae4911d7e71e2b25b776fcc35759b" resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.4.tgz#c8e046ba7eaae4911d7e71e2b25b776fcc35759b"
@ -2964,6 +2998,14 @@ websocket-extensions@>=0.1.1:
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
which@^1.2.1: which@^1.2.1:
version "1.3.1" version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
@ -3011,6 +3053,11 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
ws@8.5.0:
version "8.5.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f"
integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==
ws@^8.16.0: ws@^8.16.0:
version "8.18.0" version "8.18.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"