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 net.codinux.banking.ui.dialogs.ApplicationErrorDialog
import net.codinux.banking.ui.dialogs.BankingClientErrorDialog
import net.codinux.banking.ui.dialogs.EnterTanDialog
import net.codinux.banking.ui.state.UiState
@Composable
fun StateHandler(uiState: UiState) {
val applicationError by uiState.applicationErrorOccurred.collectAsState()
val bankingClientError by uiState.bankingClientErrorOccurred.collectAsState()
val tanChallengeReceived by uiState.tanChallengeReceived.collectAsState()
applicationError?.let { 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
import net.codinux.banking.client.model.tan.ActionRequiringTan
object Internationalization {
const val ErrorAddAccount = "Konto konnte nicht hinzugefügt 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()
Dialog(onDismissRequest = onDismiss) {
RoundedCornersCard {
Column(Modifier.background(Color.White).padding(8.dp)) {
BaseDialog(
title = "Bank Konto hinzufügen",
confirmButtonTitle = "Hinzufügen",
confirmButtonEnabled = isRequiredDataEntered && isAddingAccount == false,
showProgressIndicatorOnConfirmButton = isAddingAccount,
onDismiss = onDismiss,
onConfirm = {
selectedBank?.let {
isAddingAccount = true
Row(Modifier.fillMaxWidth()) {
Text(
"Bank Konto hinzufügen",
color = Style.HeaderTextColor,
fontSize = Style.HeaderFontSize,
fontWeight = Style.HeaderFontWeight,
modifier = Modifier.padding(top = 8.dp, bottom = 16.dp).weight(1f)
)
coroutineScope.launch { // TODO: launch on Dispatchers.IO where it is available
val successful = DI.bankingService.addAccount(selectedBank!!, loginName, password)
TextButton(onDismiss, colors = ButtonDefaults.buttonColors(contentColor = Colors.Zinc700, backgroundColor = Color.Transparent)) {
Icon(Icons.Filled.Close, contentDescription = "Close dialog", Modifier.size(32.dp))
}
}
withContext(Dispatchers.Main) {
isAddingAccount = false
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") },
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")
if (successful) {
onDismiss()
}
}
}
}
}
) {
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 kotlinx.datetime.LocalDate
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.Amount
import net.codinux.banking.client.model.response.GetAccountDataResponse
import net.codinux.banking.client.model.response.Response
import net.codinux.banking.client.model.response.ResponseType
import net.codinux.banking.client.model.options.GetAccountDataOptions
import net.codinux.banking.client.model.options.RetrieveTransactions
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.FinTsClientOptions
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.BankingClientError
import net.codinux.banking.ui.model.error.ErroneousAction
import net.codinux.banking.ui.state.UiState
import net.codinux.csv.reader.CsvReader
import net.codinux.log.Log
import net.codinux.log.logger
import org.jetbrains.compose.resources.ExperimentalResourceApi
@ -27,6 +28,10 @@ class BankingService(
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()
@ -41,12 +46,7 @@ class BankingService(
suspend fun addAccount(bank: BankInfo, loginName: String, password: String): Boolean {
try {
val config = FinTsClientConfiguration(FinTsClientOptions(true))
val client = FinTs4kBankingClientForCustomer(bank.bankCode, loginName, password, config, SimpleBankingClientCallback { tanChallenge, callback ->
// TODO: show EnterTanDialog
})
val response = client.getAccountDataAsync()
val response = client.getAccountDataAsync(GetAccountDataRequest(bank.bankCode, loginName, password, GetAccountDataOptions(retrieveTransactions = RetrieveTransactions.All)))
if (response.type == ResponseType.Success && response.data != null) {
handleSuccessfulGetAccountDataResponse(response.data!!)

View File

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

View File

@ -69,6 +69,11 @@
resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273"
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":
version "1.1.2"
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"
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:
version "1.3.8"
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"
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:
version "4.0.7"
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"
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:
version "1.3.1"
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"
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:
version "1.0.2"
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:
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:
version "5.1.4"
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"
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:
version "1.3.1"
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"
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:
version "8.18.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"