diff --git a/app/src/main/java/com/tangem/tap/common/compose/Button.kt b/app/src/main/java/com/tangem/tap/common/compose/Button.kt index 992939febe..20f72a0a89 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/Button.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/Button.kt @@ -2,7 +2,9 @@ package com.tangem.tap.common.compose import androidx.compose.foundation.layout.* import androidx.compose.material.* +import androidx.compose.material.ripple.LocalRippleTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -83,6 +85,18 @@ fun PasteButton( } } +/** + * Used for disable ripple if button is enable = false + */ +@Composable +fun ToggledRippleTheme( + isEnabled: Boolean, + content: @Composable () -> Unit, +) { + val theme = LocalRippleTheme provides if (isEnabled) LocalRippleTheme.current else NoRippleTheme() + CompositionLocalProvider(theme) { content() } +} + @Preview @Composable fun ButtonTest() { diff --git a/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt b/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt index aa0e7e71d0..d498d577f8 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.tangem.domain.DomainDialog -import com.tangem.domain.DomainStateDialog import com.tangem.domain.redux.domainStore import com.tangem.domain.redux.global.DomainGlobalAction import com.tangem.domain.redux.global.DomainGlobalState @@ -28,7 +27,7 @@ import org.rekotlin.StoreSubscriber @Composable fun ComposeDialogManager() { - val dialogSate = remember { mutableStateOf(null) } + val dialogSate = remember { mutableStateOf(null) } val subscriber = remember { object : StoreSubscriber { override fun newState(state: DomainGlobalState) { @@ -52,7 +51,7 @@ fun ComposeDialogManager() { } @Composable -private fun ShowTheDialog(dialogState: MutableState) { +private fun ShowTheDialog(dialogState: MutableState) { if (dialogState.value == null) return val context = LocalContext.current diff --git a/app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt b/app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt index b7eb414062..1cc41acbf1 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt @@ -5,8 +5,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import com.tangem.blockchain.common.Blockchain import com.tangem.common.extensions.VoidCallback @@ -35,7 +33,7 @@ fun OutlinedSpinner( rSelectedItem.value = selectedItem.value } - val onItemSelectedInternal: (T) -> Unit = { + val onDropDownItemSelectedInternal: (T) -> Unit = { rSelectedItem.value = it rIsExpanded.value = false onItemSelected(it) @@ -49,23 +47,23 @@ fun OutlinedSpinner( expanded = rIsExpanded.value, onExpandedChange = { rIsExpanded.value = !rIsExpanded.value }, ) { - ProvideTextStyle(value = TextStyle(color = Color.Blue)) { - OutlinedTextField( - modifier = modifier, - readOnly = true, - enabled = isEnabled, - value = textFieldConverter(rSelectedItem.value), - onValueChange = {}, - label = { Text(label) }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = rIsExpanded.value) }, - ) - } + OutlinedTextField( + modifier = modifier, + readOnly = true, + enabled = isEnabled, + value = textFieldConverter(rSelectedItem.value), + onValueChange = {}, + label = { Text(label) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = rIsExpanded.value) }, + ) + + if (!isEnabled) return@ExposedDropdownMenuBox ExposedDropdownMenu( expanded = rIsExpanded.value, onDismissRequest = onDismissRequest, ) { itemList.forEach { item -> - DropdownMenuItem(onClick = { onItemSelectedInternal(item) }) { + DropdownMenuItem(onClick = { onDropDownItemSelectedInternal(item) }) { when (dropdownItemView) { null -> Text(textFieldConverter(item)) else -> dropdownItemView(item) diff --git a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt index d0819b0817..0d7d4f44e9 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -105,7 +106,13 @@ private fun OutlinedProgressTextField( onValueChange = ::updateFieldValueAndEmmit, keyboardOptions = keyboardOptions, label = { Text(label) }, - placeholder = { Text(placeholder) }, + placeholder = { + Text( + text = placeholder, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, trailingIcon = trailingIcon, singleLine = true, enabled = isEnabled, diff --git a/app/src/main/java/com/tangem/tap/common/compose/Theme.kt b/app/src/main/java/com/tangem/tap/common/compose/Theme.kt new file mode 100644 index 0000000000..f0afe076ba --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/Theme.kt @@ -0,0 +1,17 @@ +package com.tangem.tap.common.compose + +import androidx.compose.material.ripple.RippleAlpha +import androidx.compose.material.ripple.RippleTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +/** +[REDACTED_AUTHOR] + */ +class NoRippleTheme : RippleTheme { + @Composable + override fun defaultColor() = Color.Unspecified + + @Composable + override fun rippleAlpha(): RippleAlpha = RippleAlpha(0.0f, 0.0f, 0.0f, 0.0f) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index 86d8393672..414edde2e5 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -26,6 +26,8 @@ import com.tangem.tap.features.wallet.ui.WalletDetailsFragment import com.tangem.tap.features.wallet.ui.WalletFragment import com.tangem.wallet.R +private class Navigation + fun FragmentActivity.openFragment( screen: AppScreen, addToBackstack: Boolean, diff --git a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt index b76e63b0e2..0adf835be5 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt @@ -37,10 +37,10 @@ suspend fun WalletManager.safeUpdate(): Result = try { val amountToCreateAccount = blockchain.amountToCreateAccount(wallet.getFirstToken()) if (exception is BlockchainSdkError.AccountNotFound && amountToCreateAccount != null) { - Result.Failure(TapError.WalletManagerUpdate.NoAccountError(amountToCreateAccount.toString())) + Result.Failure(TapError.WalletManager.NoAccountError(amountToCreateAccount.toString())) } else { val message = exception.localizedMessage ?: "An error has occurred. Try later" - Result.Failure(TapError.WalletManagerUpdate.InternalError(message)) + Result.Failure(TapError.WalletManager.InternalError(message)) } } } diff --git a/app/src/main/java/com/tangem/tap/common/redux/NotificationsMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/NotificationsMiddleware.kt index 2ace551205..a32545d675 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/NotificationsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/NotificationsMiddleware.kt @@ -4,12 +4,14 @@ import android.content.Context import android.widget.Toast import androidx.coordinatorlayout.widget.CoordinatorLayout import com.google.android.material.snackbar.Snackbar +import com.tangem.tap.common.extensions.getColor import com.tangem.tap.domain.ArgError import com.tangem.tap.domain.MultiMessageError import com.tangem.tap.domain.TapError import com.tangem.tap.domain.assembleErrors import com.tangem.tap.notificationsHandler import com.tangem.wallet.BuildConfig +import com.tangem.wallet.R import org.rekotlin.Action import org.rekotlin.Middleware import java.lang.ref.WeakReference @@ -29,7 +31,17 @@ class NotificationsHandler(coordinatorLayout: CoordinatorLayout) { fun showNotification(message: String) { baseLayout.get()?.let { layout -> Snackbar.make(layout, message, Snackbar.LENGTH_LONG) - .also { snackbar -> snackbar.show() } + .also { snackbar -> snackbar.show() } + } + } + + fun showDebugNotification(message: String) { + baseLayout.get()?.let { layout -> + Snackbar.make(layout, message, Snackbar.LENGTH_LONG) + .also { snackbar -> + snackbar.setBackgroundTint(layout.getColor(R.color.warning_warning)) + snackbar.show() + } } } @@ -51,6 +63,12 @@ class NotificationsHandler(coordinatorLayout: CoordinatorLayout) { val message = builder(errorList.map { getMessageString(context, it.first, it.second) }) showNotification(message) } + + fun showDebugErrorNotification(message: Int, args: List? = null) { + baseLayout.get()?.let { + showDebugNotification(getMessageString(it.context, message, args)) + } + } } fun getMessageString(context: Context, message: Int, args: List?): String { @@ -74,17 +92,37 @@ private fun handleNotificationAction(action: Action) { if (action is Debug && !BuildConfig.DEBUG) return when (action) { - is NotificationAction -> notificationsHandler?.showNotification(action.messageResource) + is NotificationAction -> { + notificationsHandler?.showNotification(action.messageResource) + } is ToastNotificationAction -> notificationsHandler?.showToastNotification(action.messageResource) is ErrorAction -> { - when (action.error) { - is MultiMessageError -> { - val multiError = action.error as MultiMessageError - notificationsHandler?.showNotification(multiError.assembleErrors(), multiError.builder) + when (action) { + is Debug -> { + val args = (action.error as? ArgError)?.args ?: listOf() + when (action) { + is DebugNotification -> { + notificationsHandler?.showNotification(action.error.messageResource, args) + } + is DebugToastNotification -> { + notificationsHandler?.showToastNotification(action.error.messageResource, args) + } + is DebugErrorAction -> { + notificationsHandler?.showDebugErrorNotification(action.error.messageResource, args) + } + } } else -> { - val args = (action.error as? ArgError)?.args ?: listOf() - notificationsHandler?.showNotification(action.error.messageResource, args) + when (action.error) { + is MultiMessageError -> { + val multiError = action.error as MultiMessageError + notificationsHandler?.showNotification(multiError.assembleErrors(), multiError.builder) + } + else -> { + val args = (action.error as? ArgError)?.args ?: listOf() + notificationsHandler?.showNotification(action.error.messageResource, args) + } + } } } } diff --git a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt index 6221e35faa..0c38cd5a46 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt @@ -43,7 +43,8 @@ sealed class TapError( object AssetAccountNotCreated : TapError(R.string.send_error_no_account_xlm) } - sealed class WalletManagerUpdate { + sealed class WalletManager { + object CreationError: CustomError("Can't create wallet manager") class NoAccountError(amountToCreateAccount: String): CustomError(amountToCreateAccount) class InternalError(message: String): CustomError(message) object BlockchainIsUnreachable: TapError(R.string.wallet_balance_blockchain_unreachable) diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 598c125413..c461b5f57b 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -4,9 +4,9 @@ import com.tangem.blockchain.blockchains.solana.RentProvider import com.tangem.blockchain.common.* import com.tangem.common.services.Result import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.TapWorkarounds.isTestCard -import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.extensions.withMainContext import com.tangem.tap.common.ThrottlerWithValues import com.tangem.tap.common.extensions.dispatchOnMain @@ -19,7 +19,6 @@ import com.tangem.tap.domain.configurable.config.ConfigManager import com.tangem.tap.domain.extensions.isMultiwalletAllowed import com.tangem.tap.domain.extensions.makePrimaryWalletManager import com.tangem.tap.domain.extensions.makeWalletManagersForApp -import com.tangem.tap.domain.tokens.CardCurrencies import com.tangem.tap.domain.tokens.BlockchainNetwork import com.tangem.tap.features.demo.isDemoCard import com.tangem.tap.features.wallet.models.PendingTransactionType @@ -61,12 +60,12 @@ class TapWalletManager { } is Result.Failure -> { when (result.error) { - is TapError.WalletManagerUpdate.NoAccountError -> { + is TapError.WalletManager.NoAccountError -> { dispatchOnMain( WalletAction.LoadWallet.NoAccount( walletManager.wallet, blockchainNetwork, - (result.error as TapError.WalletManagerUpdate.NoAccountError).customMessage + (result.error as TapError.WalletManager.NoAccountError).customMessage ) ) } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt index 7fefd24545..f0873595ff 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt @@ -32,7 +32,7 @@ class OnboardingManager( suspend fun loadArtworkUrl(): String { val cardInfo = cardInfo - ?: OnlineCardVerifier().getCardInfo(scanResponse.card.cardId, scanResponse.card.cardPublicKey) + ?: OnlineCardVerifier().getCardInfo(scanResponse.card.cardId, scanResponse.card.cardPublicKey) this.cardInfo = cardInfo return scanResponse.card.getOrLoadCardArtworkUrl(cardInfo) } @@ -57,11 +57,11 @@ class OnboardingManager( is Result.Failure -> { val error = (result.error as? TapError) ?: TapError.UnknownError when (error) { - is TapError.WalletManagerUpdate.NoAccountError -> OnboardingWalletBalance.error(error) - // NoInternetConnection, WalletManagerUpdate.InternalError + is TapError.WalletManager.NoAccountError -> OnboardingWalletBalance.error(error) + // NoInternetConnection, WalletManager.InternalError else -> { Timber.e(error.localizedMessage) - OnboardingWalletBalance.criticalError(TapError.WalletManagerUpdate.BlockchainIsUnreachableTryLater) + OnboardingWalletBalance.criticalError(TapError.WalletManager.BlockchainIsUnreachableTryLater) } } } @@ -93,7 +93,7 @@ data class OnboardingWalletBalance( fun balanceIsToppedUp(): Boolean = value.isPositive() || hasIncomingTransaction val amountToCreateAccount: String? - get() = if (error is TapError.WalletManagerUpdate.NoAccountError) error.customMessage else null + get() = if (error is TapError.WalletManager.NoAccountError) error.customMessage else null companion object { fun error(error: TapError): OnboardingWalletBalance = OnboardingWalletBalance( diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/AddCustomTokenFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/AddCustomTokenFragment.kt index 0c6e8bc2a1..b33f5c7220 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/AddCustomTokenFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/AddCustomTokenFragment.kt @@ -14,6 +14,7 @@ import com.google.accompanist.appcompattheme.AppCompatTheme import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState import com.tangem.domain.redux.domainStore import com.tangem.tap.features.BaseStoreFragment +import com.tangem.tap.features.addBackPressHandler import com.tangem.tap.features.tokens.addCustomToken.compose.AddCustomTokenScreen import com.tangem.wallet.R import org.rekotlin.StoreSubscriber @@ -57,6 +58,6 @@ class AddCustomTokenFragment : BaseStoreFragment(R.layout.view_compose_fragment) } } -// addBackPressHandler(this) + addBackPressHandler(this) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/DomainErrorConverter.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/DomainErrorConverter.kt index 89c205afee..db6b5aa462 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/DomainErrorConverter.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/DomainErrorConverter.kt @@ -36,13 +36,15 @@ private class AddCustomTokenConverter( AddCustomTokenError.InvalidContractAddress -> R.string.custom_token_creation_error_invalid_contract_address AddCustomTokenError.NetworkIsNotSelected -> R.string.custom_token_creation_error_network_not_selected AddCustomTokenError.InvalidDerivationPath -> R.string.custom_token_creation_error_invalid_derivation_path - AddCustomTokenError.InvalidDecimalsCount -> R.string.custom_token_creation_error_wrong_decimals - AddCustomTokenError.FieldIsEmpty -> R.string.custom_token_creation_error_empty_fields + AddCustomTokenError.InvalidDecimalsCount -> { + context.getString(R.string.custom_token_creation_error_wrong_decimals, 30) + } + AddCustomTokenError.FieldIsEmpty -> R.string.custom_token_creation_error_required_field else -> null } return when (rawMessage) { is Int -> context.getString(rawMessage) -// is String -> rawMessage + is String -> rawMessage else -> "Unknown error: ${customTokenError::class.java.simpleName}" } } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt index ad66229ed4..83641420f3 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt @@ -25,6 +25,7 @@ import com.tangem.domain.features.addCustomToken.redux.ScreenState import com.tangem.domain.features.addCustomToken.redux.ViewStates import com.tangem.domain.redux.domainStore import com.tangem.tap.common.compose.ComposeDialogManager +import com.tangem.tap.common.compose.ToggledRippleTheme import com.tangem.tap.common.compose.keyboardAsState import com.tangem.tap.features.tokens.addCustomToken.DomainErrorConverter import com.tangem.wallet.R @@ -107,7 +108,7 @@ fun Warnings(warnings: List) { Column { warnings.forEachIndexed { index, item -> val modifier = when (index) { - 0 -> Modifier.padding(16.dp, 16.dp, 16.dp, 0.dp) + 0 -> Modifier.padding(16.dp, 0.dp, 16.dp, 0.dp) warnings.lastIndex -> Modifier.padding(16.dp, 8.dp, 16.dp, 16.dp) else -> Modifier.padding(16.dp, 8.dp, 16.dp, 0.dp) } @@ -120,7 +121,7 @@ fun Warnings(warnings: List) { Text( modifier = Modifier.padding(16.dp), text = warningConverter.convertError(item), - color = colorResource(id = R.color.lightGray0), + color = colorResource(id = R.color.white), fontSize = 14.sp ) } @@ -138,36 +139,34 @@ private fun AddButton(state: MutableState) { } @Composable -fun AddCustomTokenFab( +private fun AddCustomTokenFab( modifier: Modifier = Modifier, isEnabled: Boolean = true, onClick: () -> Unit ) { - val contentColor = if (isEnabled) { - Color.White + val contentColor = Color.White + val backgroundColor = if (isEnabled) { + Color(0xFF1ACE80) } else { - colorResource(id = R.color.darkGray1) + Color(0xFFB9E6D3) } - val backgroundColor = Color(0xFF1ACE80) - ExtendedFloatingActionButton( - modifier = modifier, - icon = { - Icon( - imageVector = Icons.Filled.Add, - tint = contentColor, - contentDescription = "Add", - ) - }, - text = { - Text( - text = stringResource(id = R.string.common_add), - ) - }, - onClick = onClick, - backgroundColor = backgroundColor, - contentColor = contentColor, - ) + ToggledRippleTheme(isEnabled) { + ExtendedFloatingActionButton( + modifier = modifier, + icon = { + Icon( + imageVector = Icons.Filled.Add, + tint = contentColor, + contentDescription = "Add", + ) + }, + text = { Text(text = stringResource(id = R.string.common_add)) }, + onClick = { if (isEnabled) onClick() }, + backgroundColor = backgroundColor, + contentColor = contentColor, + ) + } } data class ScreenFieldData( diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/DebugActions.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/DebugActions.kt index 18c1a5fc2e..0db334c34b 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/DebugActions.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/DebugActions.kt @@ -68,7 +68,7 @@ private fun AllInOne() { ) // unknown ContractAddressButton( - name = "unk", + name = "unknown", address = "0x1111111111111111112111111111111111111113" ) } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/FormFieldViews.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/FormFieldViews.kt index e9207987e7..1d0ee9869e 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/FormFieldViews.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/FormFieldViews.kt @@ -29,7 +29,7 @@ fun TokenContractAddressView(screenFieldData: ScreenFieldData) { OutlinedTextFieldWidget( textFieldData = tokenField.data, labelId = R.string.custom_token_contract_address_input_title, - placeholder = "0x0000000000000000", + placeholder = "0x0000000000000000000000000000000000000000", isEnabled = screenFieldData.viewState.isEnabled, isLoading = screenFieldData.viewState.isLoading, error = screenFieldData.error, @@ -71,7 +71,7 @@ fun TokenNetworkView(screenFieldData: ScreenFieldData, state: AddCustomTokenStat itemList = networkField.itemList, selectedItem = networkField.data, isEnabled = screenFieldData.viewState.isEnabled, - textFieldConverter = { state.convertBlockchainName(it, notSelected) }, + textFieldConverter = { state.blockchainToName(it) ?: notSelected }, ) { domainStore.dispatch(AddCustomTokenAction.OnTokenNetworkChanged(Field.Data(it))) } SpacerH8() } @@ -123,11 +123,11 @@ fun TokenDerivationPathView(screenFieldData: ScreenFieldData, state: AddCustomTo itemList = networkField.itemList, selectedItem = networkField.data, isEnabled = screenFieldData.viewState.isEnabled, - textFieldConverter = { state.convertBlockchainName(it, notSelected) }, + textFieldConverter = { state.blockchainToName(it) ?: notSelected }, dropdownItemView = { blockchain -> - val derivationPathLabel = state.convertDerivationPathLabel(blockchain, notSelected) - val blockchainName = state.convertBlockchainName(blockchain, notSelected) - TitleSubtitle(derivationPathLabel, blockchainName) + val derivationPathName = state.blockchainToName(blockchain, true) ?: notSelected + val blockchainName = state.blockchainToName(blockchain) ?: notSelected + TitleSubtitle(derivationPathName, blockchainName) } ) { domainStore.dispatch(AddCustomTokenAction.OnTokenDerivationPathChanged(Field.Data(it))) } SpacerH8() diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt index 4ba47f905f..eb09ad2eb1 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt @@ -13,12 +13,12 @@ import com.tangem.domain.common.KeyWalletPublicKey import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.TapWorkarounds.isTestCard -import com.tangem.domain.features.addCustomToken.CompleteData +import com.tangem.domain.features.addCustomToken.CustomCurrency import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction -import com.tangem.domain.features.addCustomToken.redux.AddedCurrencies import com.tangem.domain.redux.domainStore import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.tap.* +import com.tangem.tap.common.extensions.dispatchDebugErrorNotification import com.tangem.tap.common.extensions.dispatchErrorNotification import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.AppState @@ -28,19 +28,16 @@ import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.makeWalletManagerForApp import com.tangem.tap.domain.tokens.BlockchainNetwork +import com.tangem.tap.features.wallet.redux.Currency import com.tangem.tap.features.wallet.redux.WalletAction -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.tap.tangemSdkManager import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Middleware -import timber.log.Timber class TokensMiddleware { - val tokensMiddleware: Middleware = { dispatch, state -> + val tokensMiddleware: Middleware = { _, _ -> { next -> { action -> when (action) { @@ -58,8 +55,10 @@ class TokensMiddleware { val isTestcard = scanResponse?.card?.isTestCard ?: false scope.launch { - val currencies = async { currenciesRepository.getSupportedTokens(isTestcard) - .filter(action.supportedBlockchains?.toSet()) } + val currencies = async { + currenciesRepository.getSupportedTokens(isTestcard) + .filter(action.supportedBlockchains?.toSet()) + } val delay = async { delay(600) } delay.await() store.dispatchOnMain(TokensAction.LoadCurrencies.Success(currencies.await())) @@ -69,6 +68,7 @@ class TokensMiddleware { private fun handleSaveChanges(action: TokensAction.SaveChanges) { val scanResponse = store.state.globalState.scanResponse ?: return + //TODO: bad things happens. val currentTokens = store.state.tokensState.addedWallets.toTokens() val currentBlockchains = store.state.tokensState.addedWallets.toBlockchains( store.state.tokensState.derivationStyle @@ -83,44 +83,42 @@ class TokensMiddleware { removeCurrenciesIfNeeded(blockchainsToRemove, tokensToRemove) if (tokensToAdd.isEmpty() && blockchainsToAdd.isEmpty()) { + store.dispatchDebugErrorNotification("Nothing to save") store.dispatch(NavigationAction.PopBackTo()) return } + val derivationStyle = scanResponse.card.derivationStyle + val currencyList = blockchainsToAdd.map { + Currency.Blockchain(it, it.derivationPath(derivationStyle)?.rawPath) + } + tokensToAdd.map { + Currency.Token(it.token, it.blockchain, it.blockchain.derivationPath(derivationStyle)?.rawPath) + } if (scanResponse.supportsHdWallet()) { - deriveMissingBlockchains(scanResponse, blockchainsToAdd, tokensToAdd) + deriveMissingBlockchains(scanResponse, currencyList) { + submitAdd(it, currencyList) + store.dispatchOnMain(NavigationAction.PopBackTo()) + } } else { - submitAdd(blockchainsToAdd, tokensToAdd, scanResponse) - store.dispatch(NavigationAction.PopBackTo()) + submitAdd(scanResponse, currencyList) + store.dispatchOnMain(NavigationAction.PopBackTo()) } } - private fun handleAddingCustomToken(action: TokensAction.PrepareAndNavigateToAddCustomToken) { - val tokensState = store.state.tokensState - val addedTokensList = tokensState.addedTokens.map { - DomainWrapped.TokenWithBlockchain(it.token.copy(), it.blockchain) - } - val addedBlockchains = tokensState.addedBlockchains.map { it } - val addedCurrencies = AddedCurrencies(addedTokensList, addedBlockchains) - domainStore.dispatch(AddCustomTokenAction.Init.SetAddedCurrencies(addedCurrencies)) - - val callback = fun(data: CompleteData) { - Timber.e("Yoooohhhoooo") - } - domainStore.dispatch(AddCustomTokenAction.Init.SetOnAddTokenCallback(callback)) - store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken)) - } - private fun deriveMissingBlockchains( scanResponse: ScanResponse, - blockchains: List, - tokens: List + currencyList: List, + onSuccess: (ScanResponse) -> Unit, ) { val derivationDataList = listOfNotNull( - getDerivations(EllipticCurve.Secp256k1, scanResponse, blockchains, tokens), - getDerivations(EllipticCurve.Ed25519, scanResponse, blockchains, tokens) + getDerivations(EllipticCurve.Secp256k1, scanResponse, currencyList), + getDerivations(EllipticCurve.Ed25519, scanResponse, currencyList) ) - val derivations = derivationDataList.map { it.derivations }.toMap() + val derivations = derivationDataList.associate { it.derivations } + if (derivations.isEmpty()) { + onSuccess(scanResponse) + return + } scope.launch { val result = tangemSdkManager.derivePublicKeys( @@ -145,10 +143,8 @@ class TokensMiddleware { derivedKeys = updatedDerivedKeys ) store.dispatchOnMain(GlobalAction.SaveScanNoteResponse(updatedScanResponse)) - submitAdd(blockchains, tokens, updatedScanResponse) - delay(DELAY_SDK_DIALOG_CLOSE) - store.dispatchOnMain(NavigationAction.PopBackTo()) + onSuccess(updatedScanResponse) } is CompletionResult.Failure -> { store.dispatchErrorNotification(TapError.CustomError("Error adding tokens")) @@ -160,20 +156,31 @@ class TokensMiddleware { private fun getDerivations( curve: EllipticCurve, scanResponse: ScanResponse, - blockchains: List, - tokens: List + currencyList: List, ): DerivationData? { val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null - val derivationPathsCandidates = (blockchains + tokens.map { it.blockchain }).distinct() - .mapNotNull { it.derivationPath(scanResponse.card.derivationStyle) } + val manageTokensCandidates = currencyList.map { it.blockchain }.distinct().filter { + it.getSupportedCurves().contains(curve) + }.mapNotNull { + it.derivationPath(scanResponse.card.derivationStyle) + } + + val customTokensCandidates = currencyList.filter { + it.blockchain.getSupportedCurves().contains(curve) + }.mapNotNull { it.derivationPath }.map { DerivationPath(it) } + + val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct() + if (bothCandidates.isEmpty()) return null val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey() val alreadyDerivedKeys: ExtendedPublicKeysMap = scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap()) val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList() - val toDerive = derivationPathsCandidates.filterNot { alreadyDerivedPaths.contains(it) } + val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) } + if (toDerive.isEmpty()) return null + return DerivationData( derivations = mapKeyOfWalletPublicKey to toDerive, alreadyDerivedKeys = alreadyDerivedKeys, @@ -188,20 +195,42 @@ class TokensMiddleware { ) private fun submitAdd( - blockchains: List, tokens: List, scanResponse: ScanResponse, + scanResponse: ScanResponse, + currencyList: List, ) { val factory = store.state.globalState.tapWalletManager.walletManagerFactory + val derivationStyle = scanResponse.card.derivationStyle - (blockchains.mapNotNull { - val walletManager = factory.makeWalletManagerForApp( - scanResponse, it, - scanResponse.card.derivationStyle?.let { DerivationParams.Default(it) } - ) ?: return@mapNotNull null - WalletAction.MultiWallet.AddBlockchain(BlockchainNetwork.fromWalletManager(walletManager), walletManager) - } + tokens.map { - val blockchainNetwork = BlockchainNetwork(it.blockchain, scanResponse.card) - WalletAction.MultiWallet.AddToken(it.token, blockchainNetwork) - }).forEach { store.dispatchOnMain(it) } + val addActions = currencyList.mapNotNull { currency -> + when (currency) { + is Currency.Blockchain -> { + val derivationPath = currency.derivationPath?.let { DerivationPath(it) } + + val derivationParams = derivationStyle?.let { + when (derivationPath) { + null -> DerivationParams.Default(derivationStyle) + else -> DerivationParams.Custom(derivationPath) + } + } + + val walletManager = factory.makeWalletManagerForApp( + scanResponse = scanResponse, + blockchain = currency.blockchain, + derivationParams = derivationParams + ) ?: return@mapNotNull null + val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager) + WalletAction.MultiWallet.AddBlockchain(blockchainNetwork, walletManager) + } + is Currency.Token -> { + val rawDerivationPath = currency.derivationPath + ?: currency.blockchain.derivationPath(derivationStyle)?.rawPath + + val blockchainNetwork = BlockchainNetwork(currency.blockchain, rawDerivationPath, emptyList()) + WalletAction.MultiWallet.AddToken(currency.token, blockchainNetwork) + } + } + } + addActions.forEach { store.dispatchOnMain(it) } } private fun removeCurrenciesIfNeeded(blockchains: List, tokens: List) { @@ -221,4 +250,45 @@ class TokensMiddleware { } } + private fun isNeedToDerive(scanResponse: ScanResponse, currency: Currency): Boolean { + return currency.derivationPath?.let { + !scanResponse.hasDerivation(currency.blockchain, it) + } ?: false + } + + private fun handleAddingCustomToken(action: TokensAction.PrepareAndNavigateToAddCustomToken) { + val onAddCustomToken = fun(customCurrency: CustomCurrency) { + val scanResponse = store.state.globalState.scanResponse ?: return + + fun submitAndPopBack(scanResponse: ScanResponse, currencyList: List) { + submitAdd(scanResponse, currencyList) + // pop from the AddCustomTokenScreen + store.dispatchOnMain(NavigationAction.PopBackTo()) + store.dispatchOnMain(NavigationAction.PopBackTo()) + } + + val currency = Currency.fromCustomCurrency(customCurrency) + val isNeedToDerive = isNeedToDerive(scanResponse, currency) + val currencyList = listOf(currency) + if (isNeedToDerive) { + deriveMissingBlockchains(scanResponse, currencyList) { + submitAndPopBack(it, currencyList) + } + } else { + submitAndPopBack(scanResponse, currencyList) + } + } + + val addedCurrencies = store.state.walletState.wallets.map { walletStore -> + walletStore.walletsData.map { walletData -> walletData.currency } + }.flatten().map { + when (it) { + is Currency.Blockchain -> DomainWrapped.Currency.Blockchain(it.blockchain, it.derivationPath) + is Currency.Token -> DomainWrapped.Currency.Token(it.token, it.blockchain, it.derivationPath) + } + } + domainStore.dispatch(AddCustomTokenAction.Init.SetAddedCurrencies(addedCurrencies)) + domainStore.dispatch(AddCustomTokenAction.Init.SetOnAddTokenCallback(onAddCustomToken)) + store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken)) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt index abecd0f54a..6c592e7152 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt @@ -6,6 +6,7 @@ import com.tangem.blockchain.common.address.AddressType import com.tangem.blockchain.extensions.isAboveZero import com.tangem.common.extensions.isZero import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.domain.features.addCustomToken.CustomCurrency import com.tangem.tap.common.entities.Button import com.tangem.tap.common.extensions.toQrCode import com.tangem.tap.common.redux.StateDialog @@ -17,6 +18,7 @@ import com.tangem.tap.domain.extensions.sellIsAllowed import com.tangem.tap.domain.extensions.toSendableAmounts import com.tangem.tap.domain.tokens.BlockchainNetwork import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState +import com.tangem.tap.features.tokens.redux.TokenWithBlockchain import com.tangem.tap.features.wallet.models.PendingTransaction import com.tangem.tap.features.wallet.models.toPendingTransactions import com.tangem.tap.features.wallet.models.toPendingTransactionsForToken @@ -439,6 +441,28 @@ sealed interface Currency { ) } } + + fun fromCustomCurrency(customCurrency: CustomCurrency): Currency { + return when (customCurrency) { + is CustomCurrency.CustomBlockchain -> Blockchain( + blockchain = customCurrency.network, + derivationPath = customCurrency.derivationPath?.rawPath + ) + is CustomCurrency.CustomToken -> Token( + token = customCurrency.token, + blockchain = customCurrency.network, + derivationPath = customCurrency.derivationPath?.rawPath, + ) + } + } + + fun fromTokenWithBlockchain(tokenWithBlockchain: TokenWithBlockchain): Token { + return Currency.Token( + token = tokenWithBlockchain.token, + blockchain = tokenWithBlockchain.blockchain, + derivationPath = null + ) + } } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt index 5a0dc52f6c..61c5e422be 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -235,7 +235,8 @@ class MultiWalletMiddleware { token, blockchainNetwork.blockchain, blockchainNetwork.derivationPath ) })) - walletManager.addTokens(tokens) + if (tokens.isNotEmpty()) walletManager.addTokens(tokens) + currenciesRepository.saveUpdatedCurrency( cardId = scanResponse.card.cardId, blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 02169fe239..9a0a6881fd 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -202,6 +202,7 @@ Значок токена Добавить токен Адрес контракта + Обязательное поле Пожалуйста, выберите сеть Пожалуйста, заполните все поля Количество знаков после запятой должно быть корректным числом не больше %d diff --git a/app/src/main/res/values/strings_untranslated.xml b/app/src/main/res/values/strings_untranslated.xml index be969d2aae..649081be00 100644 --- a/app/src/main/res/values/strings_untranslated.xml +++ b/app/src/main/res/values/strings_untranslated.xml @@ -84,6 +84,7 @@ Add Token Contract address + Required field Please select the network Please fill in all the fields Decimal number must be a valid integer, no higher than %d diff --git a/domain/src/main/java/com/tangem/domain/DomainStateDialog.kt b/domain/src/main/java/com/tangem/domain/DomainDialog.kt similarity index 82% rename from domain/src/main/java/com/tangem/domain/DomainStateDialog.kt rename to domain/src/main/java/com/tangem/domain/DomainDialog.kt index a3c7aa9da2..e95abd7626 100644 --- a/domain/src/main/java/com/tangem/domain/DomainStateDialog.kt +++ b/domain/src/main/java/com/tangem/domain/DomainDialog.kt @@ -6,16 +6,14 @@ import com.tangem.network.api.tangemTech.Coins /** [REDACTED_AUTHOR] */ -interface DomainStateDialog +sealed interface DomainDialog { -sealed class DomainDialog : DomainStateDialog { - - data class DialogError(val error: DomainError) : DomainDialog() + data class DialogError(val error: DomainError) : DomainDialog data class SelectTokenDialog( val items: List, val networkIdConverter: (String) -> String, val onSelect: (Coins.CheckAddressResponse.Token.Contract) -> Unit, val onClose: VoidCallback = {} - ) : DomainDialog() + ) : DomainDialog } \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/DomainWrapped.kt b/domain/src/main/java/com/tangem/domain/DomainWrapped.kt index 41ab1c8569..3872eeaab1 100644 --- a/domain/src/main/java/com/tangem/domain/DomainWrapped.kt +++ b/domain/src/main/java/com/tangem/domain/DomainWrapped.kt @@ -1,7 +1,6 @@ package com.tangem.domain -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token +import com.tangem.blockchain.common.DerivationStyle /** [REDACTED_AUTHOR] @@ -11,8 +10,30 @@ import com.tangem.blockchain.common.Token // to appropriate parts of module sealed interface DomainWrapped { - data class TokenWithBlockchain( - val token: Token, - val blockchain: Blockchain - ) + // Mirror reflection ot the com.tangem.tap.features.wallet.redux.Currency + sealed interface Currency { + val blockchain: com.tangem.blockchain.common.Blockchain + val currencySymbol: String + val derivationPath: String? + + data class Token( + val token: com.tangem.blockchain.common.Token, + override val blockchain: com.tangem.blockchain.common.Blockchain, + override val derivationPath: String? + ) : Currency { + override val currencySymbol = token.symbol + } + + data class Blockchain( + override val blockchain: com.tangem.blockchain.common.Blockchain, + override val derivationPath: String? + ) : Currency { + override val currencySymbol: String = blockchain.currency + } + + fun isCustomCurrency(derivationStyle: DerivationStyle?): Boolean { + if (derivationPath == null || derivationStyle == null) return false + return derivationPath != blockchain.derivationPath(derivationStyle)?.rawPath + } + } } \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/ScanResponse.kt b/domain/src/main/java/com/tangem/domain/common/ScanResponse.kt index b41eb4f116..35ec0dbe8b 100644 --- a/domain/src/main/java/com/tangem/domain/common/ScanResponse.kt +++ b/domain/src/main/java/com/tangem/domain/common/ScanResponse.kt @@ -3,9 +3,13 @@ package com.tangem.domain.common import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.common.card.Card +import com.tangem.common.card.EllipticCurve import com.tangem.common.card.WalletData import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.extensions.toMapKey +import com.tangem.common.hdWallet.DerivationPath import com.tangem.domain.common.TapWorkarounds.getTangemNoteBlockchain +import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.operations.CommandResponse import com.tangem.operations.backup.PrimaryCard import com.tangem.operations.derivation.ExtendedPublicKeysMap @@ -48,6 +52,33 @@ data class ScanResponse( fun twinsIsTwinned(): Boolean = card.isTangemTwins() && walletData != null && secondTwinPublicKey != null + + fun hasDerivation(blockchain: Blockchain, rawDerivationPath: String): Boolean { + return hasDerivation(blockchain, DerivationPath(rawDerivationPath)) + } + + fun hasDerivation(blockchain: Blockchain, derivationPath: DerivationPath): Boolean { + val isTestnet = card.isTestCard || blockchain.isTestnet() + return when { + Blockchain.secp256k1Blockchains(isTestnet).contains(blockchain) -> { + hasDerivation(EllipticCurve.Secp256k1, derivationPath) + } + Blockchain.ed25519OnlyBlockchains(isTestnet).contains(blockchain) -> { + hasDerivation(EllipticCurve.Ed25519, derivationPath) + } + else -> false + } + } + + fun hasDerivation(curve: EllipticCurve, derivationPath: DerivationPath): Boolean { + val foundWallet = card.wallets.firstOrNull { it.curve == curve } + ?: return false + + val extendedPublicKeysMap = derivedKeys[foundWallet.publicKey.toMapKey()] ?: return false + + val extendedPublicKey = extendedPublicKeysMap[derivationPath] + return extendedPublicKey != null + } } enum class ProductType { diff --git a/domain/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt b/domain/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt index 83ba44c309..712fa36bd3 100644 --- a/domain/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt +++ b/domain/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt @@ -26,7 +26,7 @@ class StringIsNotEmptyValidator : CustomTokenValidator() { class TokenContractAddressValidator : CustomTokenValidator() { override fun validate(data: String?): AddCustomTokenError? { - if (data == null || data.isEmpty()) return null + if (data == null || data.isEmpty()) return AddCustomTokenError.FieldIsEmpty return if (EthereumAddressService().validate(data)) { null diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/CompleteData.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/CompleteData.kt deleted file mode 100644 index 90b11472d2..0000000000 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/CompleteData.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.tangem.domain.features.addCustomToken - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token -import com.tangem.domain.common.form.BaseFieldDataConverter -import com.tangem.domain.common.form.FieldId - -/** -[REDACTED_AUTHOR] - */ -enum class CompleteDataType { - Blockchain, Token -} - -sealed class CompleteData() { - - class CustomBlockchain( - val network: Blockchain, - val derivationPath: String? - ) : CompleteData() { - - class Converter : BaseFieldDataConverter() { - override fun getConvertedData(): CustomBlockchain { - val network = collectedData[CustomTokenFieldId.Network] as Blockchain - val derivationPath = collectedData[CustomTokenFieldId.DerivationPath] as? String - return CustomBlockchain(network, derivationPath) - } - - override fun getIdToCollect(): List = listOf(CustomTokenFieldId.Network, CustomTokenFieldId.DerivationPath) - } - } - - class CustomToken( - val token: Token, - val network: Blockchain, - val derivationPath: String?, - ) : CompleteData() { - - class Converter(val tokenId: String?) : BaseFieldDataConverter() { - - override fun getConvertedData(): CustomToken { - val token = Token( - name = collectedData[CustomTokenFieldId.Name] as String, - symbol = collectedData[CustomTokenFieldId.Symbol] as String, - contractAddress = collectedData[CustomTokenFieldId.ContractAddress] as String, - decimals = (collectedData[CustomTokenFieldId.Decimals] as String).toInt(), - id = tokenId, - ) - return CustomToken( - token, - collectedData[CustomTokenFieldId.Network] as Blockchain, - collectedData[CustomTokenFieldId.DerivationPath] as? String, - ) - } - - override fun getIdToCollect(): List = CustomTokenFieldId.values().toList() - } - } -} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/CustomCurrency.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/CustomCurrency.kt new file mode 100644 index 0000000000..67f62551eb --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/CustomCurrency.kt @@ -0,0 +1,83 @@ +package com.tangem.domain.features.addCustomToken + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.DerivationStyle +import com.tangem.blockchain.common.Token +import com.tangem.common.hdWallet.DerivationPath +import com.tangem.domain.common.form.BaseFieldDataConverter +import com.tangem.domain.common.form.FieldId +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState + +/** +[REDACTED_AUTHOR] + */ +enum class CompleteDataType { + Blockchain, Token +} + +sealed class CustomCurrency( + val network: Blockchain, + val derivationPath: DerivationPath?, +) { + + class CustomBlockchain( + network: Blockchain, + derivationPath: DerivationPath? + ) : CustomCurrency(network, derivationPath) { + + class Converter( + private val derivationStyle: DerivationStyle? + ) : BaseFieldDataConverter() { + override fun getConvertedData(): CustomBlockchain { + val mainNetwork = collectedData[CustomTokenFieldId.Network] as Blockchain + val derivationPathNetwork = collectedData[CustomTokenFieldId.DerivationPath] as Blockchain + val derivationPath = AddCustomTokenState.getDerivationPath( + mainNetwork, + derivationPathNetwork, + derivationStyle + ) + return CustomBlockchain(mainNetwork, derivationPath) + } + + override fun getIdToCollect(): List = listOf(CustomTokenFieldId.Network, CustomTokenFieldId.DerivationPath) + } + } + + class CustomToken( + val token: Token, + network: Blockchain, + derivationPath: DerivationPath? + ) : CustomCurrency(network, derivationPath) { + + class Converter( + private val tokenId: String?, + private val derivationStyle: DerivationStyle? + ) : BaseFieldDataConverter() { + + override fun getConvertedData(): CustomToken { + val mainNetwork = collectedData[CustomTokenFieldId.Network] as Blockchain + val derivationPathNetwork = collectedData[CustomTokenFieldId.DerivationPath] as Blockchain + val derivationPath = AddCustomTokenState.getDerivationPath( + mainNetwork, + derivationPathNetwork, + derivationStyle + ) + + val token = Token( + name = collectedData[CustomTokenFieldId.Name] as String, + symbol = collectedData[CustomTokenFieldId.Symbol] as String, + contractAddress = collectedData[CustomTokenFieldId.ContractAddress] as String, + decimals = (collectedData[CustomTokenFieldId.Decimals] as String).toInt(), + id = tokenId, + ) + return CustomToken( + token, + collectedData[CustomTokenFieldId.Network] as Blockchain, + derivationPath, + ) + } + + override fun getIdToCollect(): List = CustomTokenFieldId.values().toList() + } + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt index 0b541fb309..dc832d367a 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt @@ -2,13 +2,13 @@ package com.tangem.domain.features.addCustomToken.redux import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.DerivationStyle +import com.tangem.domain.DomainWrapped import com.tangem.domain.common.form.Field import com.tangem.domain.common.form.FieldId import com.tangem.domain.features.addCustomToken.AddCustomTokenError import com.tangem.domain.features.addCustomToken.AddCustomTokenWarning -import com.tangem.domain.features.addCustomToken.CompleteData +import com.tangem.domain.features.addCustomToken.CustomCurrency import com.tangem.domain.features.addCustomToken.CustomTokenFieldId -import com.tangem.network.api.tangemTech.Coins import org.rekotlin.Action /** @@ -16,9 +16,8 @@ import org.rekotlin.Action */ sealed class AddCustomTokenAction : Action { sealed class Init : AddCustomTokenAction() { - data class SetAddedCurrencies(val addedCurrencies: AddedCurrencies) : AddCustomTokenAction() - - data class SetOnAddTokenCallback(val callback: (CompleteData) -> Unit) : AddCustomTokenAction() + data class SetAddedCurrencies(val addedCurrencies: List) : AddCustomTokenAction() + data class SetOnAddTokenCallback(val callback: (CustomCurrency) -> Unit) : AddCustomTokenAction() } object OnCreate : AddCustomTokenAction() { @@ -36,22 +35,16 @@ sealed class AddCustomTokenAction : Action { data class OnTokenDecimalsChanged(val tokenDecimals: Field.Data) : AddCustomTokenAction() object OnAddCustomTokenClicked : AddCustomTokenAction() + data class SetFoundTokenId(val id: String?) : AddCustomTokenAction() + // form fields data class UpdateForm(val state: AddCustomTokenState) : AddCustomTokenAction() - object ClearTokenFields : AddCustomTokenAction() - - data class FillTokenFields( - val token: Coins.CheckAddressResponse.Token, - val contract: Coins.CheckAddressResponse.Token.Contract, - ) : AddCustomTokenAction() sealed class FieldError : AddCustomTokenAction() { data class Add(val id: CustomTokenFieldId, val error: AddCustomTokenError) : FieldError() data class Remove(val id: CustomTokenFieldId) : FieldError() } - data class SetTokenId(val id: String) : AddCustomTokenAction() - // warnings sealed class Warning : AddCustomTokenAction() { data class Add(val warnings: Set) : Warning() diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt index 3e0ed80316..9529e88021 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt @@ -2,11 +2,13 @@ package com.tangem.domain.features.addCustomToken.redux import android.webkit.ValueCallback import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.DerivationStyle import com.tangem.common.extensions.guard import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result import com.tangem.domain.DomainDialog import com.tangem.domain.DomainException +import com.tangem.domain.DomainWrapped import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.toNetworkId @@ -21,7 +23,7 @@ import com.tangem.domain.redux.domainStore import com.tangem.domain.redux.global.DomainGlobalAction import com.tangem.network.api.tangemTech.Coins import com.tangem.network.api.tangemTech.TangemTechService -import kotlinx.coroutines.cancel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Action @@ -49,111 +51,114 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT if (action !is AddCustomTokenAction) return when (action) { - is Init.SetAddedCurrencies -> {} - is Init.SetOnAddTokenCallback -> {} is OnCreate -> { - hubState.addedCurrencies.guard { + hubState.appSavedCurrencies.guard { return throwUnAppropriateInitialization("addedTokens") } } - is OnDestroy -> hubScope.cancel() + is OnDestroy -> cancelAll() is OnTokenContractAddressChanged -> { - dispatchOnMain( - Screen.UpdateAddButton( - ViewStates.AddButton(!hubState.allFieldsIsEmpty()) - ) - ) - val contractAddress = action.contractAddress.value - val validator: TokenContractAddressValidator = hubState.getValidator(ContractAddress) - val error = validator.validate(contractAddress) - addOrRemoveError(ContractAddress, error) + val address = action.contractAddress.value - if (error != null || contractAddress.isEmpty()) { - dispatchOnMain(unlockTokenFields()) - return + when (val error = ContractAddress.validateValue(address)) { + null -> { + ContractAddress.removeError() + unlockTokenFields() + } + AddCustomTokenError.FieldIsEmpty -> { + ContractAddress.removeError() + return + } + AddCustomTokenError.InvalidContractAddress -> { + ContractAddress.addError(error) + unlockTokenFields() + return + } + else -> {} } - if (!action.contractAddress.isUserInput) return - manageTokenChanges(requestInfoAboutContractAddress(contractAddress)) + if (!action.contractAddress.isUserInput) return + manageFoundTokenChanges(requestInfoAboutToken(address)) + } + is OnTokenNameChanged -> { + updateAddButton() + } + is OnTokenSymbolChanged -> { + updateAddButton() + } + is OnTokenDecimalsChanged -> { + updateAddButton() } is OnTokenNetworkChanged -> { if (!action.blockchainNetwork.isUserInput) return - val contractAddress = hubState.getField(ContractAddress).data.value - manageTokenChanges(requestInfoAboutContractAddress(contractAddress)) + val contractAddress = ContractAddress.getFieldValue() + val error = ContractAddress.validateValue(contractAddress) + if (error == null && contractAddress.isNotEmpty()) { + // token branch + manageFoundTokenChanges(requestInfoAboutToken(contractAddress)) + } else { + // blockchain branch + val isAlreadyAdded = isBlockchainPersistIntoAppSavedTokensList( + selectedNetwork = action.blockchainNetwork.value + ) + updateWarningAlreadyAdded(isAlreadyAdded) + updateAddButton() + } } - is OnTokenNameChanged -> { - val validator: TokenNameValidator = hubState.getValidator(Name) - addOrRemoveError(Name, validator.validate(action.tokenName.value)) - } - is OnTokenSymbolChanged -> { - val validator: TokenSymbolValidator = hubState.getValidator(Symbol) - addOrRemoveError(Symbol, validator.validate(action.tokenSymbol.value)) - } - is OnTokenDecimalsChanged -> { - val validator: TokenDecimalsValidator = hubState.getValidator(Decimals) - addOrRemoveError(Decimals, validator.validate(action.tokenDecimals.value)) - } -// is OnTokenDerivationPathChanged -> { -// val validator: TokenDerivationPathValidator = getValidator(DerivationPath, hubState) -// addOrRemoveError(DerivationPath, validator.validate(action.value.value)) -// } - is ClearTokenFields -> { - val nameField = hubState.getField(Name) - val symbolField = hubState.getField(Symbol) - val decimalsField = hubState.getField(Decimals) - - nameField.data = Field.Data("", false) - symbolField.data = Field.Data("", false) - decimalsField.data = Field.Data("", false) - - dispatchOnMain(UpdateForm(hubState)) - } - is FillTokenFields -> { - val networkField = hubState.getField(Network) - val nameField = hubState.getField(Name) - val symbolField = hubState.getField(Symbol) - val decimalsField = hubState.getField(Decimals) - - val token = action.token - val contract = action.contract - val blockchain = Blockchain.fromNetworkId(contract.networkId) ?: Blockchain.Unknown - networkField.data = Field.Data(blockchain, false) - nameField.data = Field.Data(token.name, false) - symbolField.data = Field.Data(token.symbol, false) - decimalsField.data = Field.Data(contract.decimalCount.toString(), false) - - dispatchOnMain(UpdateForm(hubState)) + is OnTokenDerivationPathChanged -> { + val isAlreadyAdded = if (ContractAddress.isFilled()) { + // token branch + isTokenPersistIntoAppSavedTokensList( + selectedDerivation = action.blockchainDerivationPath.value + ) + } else { + // blockchain branch + isBlockchainPersistIntoAppSavedTokensList( + selectedDerivation = action.blockchainDerivationPath.value + ) + } + updateWarningAlreadyAdded(isAlreadyAdded) + updateAddButton() } is OnAddCustomTokenClicked -> { -// if (hubState.allFieldsIsEmpty()) { - dispatchOnMain( - DomainGlobalAction.ShowDialog(DomainDialog.DialogError( - AddCustomTokenError.FieldIsEmpty - ))) - return -// } - when { - !hubState.customTokensFieldsIsEmpty() && !hubState.networkIsEmpty() -> { - hubState.getCompleteData(CompleteDataType.Token) + val state = hubState + val completeData = when { + state.tokensFieldsIsFilled() && state.networkIsSelected() -> { + state.gatherUserToken() + } + !state.tokensFieldsIsFilled() && state.networkIsSelected() -> { + state.gatherBlockchain() + } + else -> null + } + + if (completeData == null) { + // normally it can't be, because the AddButton must be blocked + } else { + hubScope.launch(Dispatchers.Main) { + state.onTokenAddCallback?.invoke(completeData) } -// !hubState.customTokensFieldsIsEmpty() && -> { -// } } -// if (true) { -// dispatchOnMain(NavigationAction.PopBackTo()) -// hubState.onTokenAddCallback?.invoke() -// } } else -> {} } } - private suspend fun requestInfoAboutContractAddress( + private suspend fun updateWarningAlreadyAdded(isInAppSavedList: Boolean) { + if (isInAppSavedList) { + AddCustomTokenWarning.TokenAlreadyAdded.add() + } else { + AddCustomTokenWarning.TokenAlreadyAdded.remove() + } + } + + private suspend fun requestInfoAboutToken( contractAddress: String, ): List { val tangemTechServiceManager = requireNotNull(hubState.tangemTechServiceManager) dispatchOnMain(Screen.UpdateTokenFields(listOf(ContractAddress to ViewStates.TokenField(isLoading = true)))) + val field = hubState.getField(Network) val selectedNetworkId: String? = field.data.value.let { if (it == Blockchain.Unknown) null else it @@ -176,118 +181,331 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT return result } - private suspend fun manageTokenChanges(foundTokens: List) { - val toAddWarnings = mutableSetOf() - val toRemoveWarnings = mutableSetOf() + private suspend fun manageFoundTokenChanges(foundTokens: List) { + if (foundTokens.isEmpty()) { + // token not found - it's completely custom + AddCustomTokenWarning.TokenAlreadyAdded.remove() + AddCustomTokenWarning.PotentialScamToken.add() + dispatchOnMain(SetFoundTokenId(null)) + clearTokenFields() + unlockTokenFields() + updateAddButton() + return + } + + // foundToken - contains all info about the token + val foundToken = foundTokens[0] + dispatchOnMain(SetFoundTokenId(foundToken.id)) when { - foundTokens.isEmpty() -> { - toAddWarnings.add(AddCustomTokenWarning.PotentialScamToken) - toRemoveWarnings.add(AddCustomTokenWarning.TokenAlreadyAdded) - dispatchOnMain(ClearTokenFields) - dispatchOnMain(unlockTokenFields()) + foundToken.contracts.isEmpty() -> { + Timber.e("Unexpected state -> throw to FB") + } + foundToken.contracts.size == 1 -> { + // token with single contract address + val singleTokenContract = foundToken.contracts[0] + fillTokenFields(foundToken, singleTokenContract) + + val isInAppSavedTokens = isTokenPersistIntoAppSavedTokensList() + if (isInAppSavedTokens) { + lockTokenFields() + lockAddButton() + AddCustomTokenWarning.PotentialScamToken.replace(AddCustomTokenWarning.TokenAlreadyAdded) + } else { + // not in the saved tokens list + if (singleTokenContract.active) { + lockTokenFields() + unlockAddButton() + if (hubState.derivationPathIsSelected()) { + AddCustomTokenWarning.PotentialScamToken.add() + } else { + AddCustomTokenWarning.TokenAlreadyAdded.remove() + AddCustomTokenWarning.PotentialScamToken.remove() + } + } else { + unlockAddButton() + AddCustomTokenWarning.PotentialScamToken.add() + } + } } else -> { - val token = foundTokens[0] - val contracts = token.contracts - when { - contracts.isEmpty() -> { - // TODO: refactoring: - Timber.e("Unexpected state -> throw to FB") - } - contracts.size == 1 -> { - val contract = contracts[0] - val isPersistIntoTheAppAddedTokenList = isPersistIntoTheAppAddedTokenList(token, contract) + AddCustomTokenWarning.PotentialScamToken.replace(AddCustomTokenWarning.TokenAlreadyAdded) - if (isPersistIntoTheAppAddedTokenList) { - toAddWarnings.add(AddCustomTokenWarning.TokenAlreadyAdded) - toRemoveWarnings.add(AddCustomTokenWarning.PotentialScamToken) - - dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(false))) - dispatchOnMain(lockTokenFields()) - } else { - toRemoveWarnings.add(AddCustomTokenWarning.TokenAlreadyAdded) - dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(true))) - - val isStandardDerivation = true - val tokenContract = token.contracts[0] - if (tokenContract.active && isStandardDerivation) { - toRemoveWarnings.add(AddCustomTokenWarning.PotentialScamToken) - dispatchOnMain(FillTokenFields(token, contract)) - dispatchOnMain(lockTokenFields()) - } else { - toAddWarnings.add(AddCustomTokenWarning.PotentialScamToken) - dispatchOnMain(ClearTokenFields) - dispatchOnMain(unlockTokenFields()) - } + val dialog = DomainDialog.SelectTokenDialog( + items = foundToken.contracts, + networkIdConverter = { networkId -> + val blockchain = Blockchain.fromNetworkId(networkId) + if (blockchain == null || blockchain == Blockchain.Unknown) { + throw DomainException.SelectTokeNetworkException(networkId) } + hubState.blockchainToName(blockchain) ?: "" + }, + onSelect = { selectedContract -> + hubScope.launch { + // find how to connect to the upper coroutineContext and dispatch through them + fillTokenFields(foundToken, selectedContract) + lockTokenFields() + unlockAddButton() + } + }, + ) + dispatchOnMain(DomainGlobalAction.ShowDialog(dialog)) + } + } + } + + private suspend fun replaceWarnings( + warningsAdd: MutableSet = mutableSetOf(), + warningsRemove: MutableSet = mutableSetOf(), + ) { + if (warningsAdd.isNotEmpty() || warningsRemove.isNotEmpty()) { + dispatchOnMain(Warning.Replace(warningsRemove.toSet(), warningsAdd.toSet())) + } + } + + private suspend fun updateAddButton() { + val state = hubState + if (state.warnings.contains(AddCustomTokenWarning.TokenAlreadyAdded)) { + lockAddButton() + return + } + when { + // token + state.tokensOneFieldsIsFilled() -> { + lockAddButton() + } + // token + state.tokensFieldsIsFilled() && state.networkIsSelected() -> { + unlockAddButton() + } + // blockchain + else -> { + if (state.networkIsSelected()) { + val alreadyAdded = isBlockchainPersistIntoAppSavedTokensList() + if (alreadyAdded) { + lockAddButton() + } else { + unlockAddButton() } - else -> { - val dialog = DomainDialog.SelectTokenDialog( - items = contracts, - networkIdConverter = { networkId -> - val blockchain = Blockchain.fromNetworkId(networkId) - if (blockchain == null || blockchain == Blockchain.Unknown) { - throw DomainException.SelectTokeNetworkException(networkId) - } - hubState.convertBlockchainName(blockchain, "") - }, - onSelect = { selectedContract -> - hubScope.launch { - // find how to connect to the upper coroutineContext and dispatch through them - dispatchOnMain(FillTokenFields(token, selectedContract)) - dispatchOnMain(lockTokenFields()) - } - }, - ) - dispatchOnMain(DomainGlobalAction.ShowDialog(dialog)) + } else { + lockAddButton() + } + } + } + } + + private suspend fun lockAddButton() { + dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(false))) + } + + private suspend fun unlockAddButton() { + dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(true))) + } + + /** + * These are helper functions. + */ + private fun isTokenPersistIntoAppSavedTokensList( + tokenId: String? = hubState.tokenId, + tokenContractAddress: String = ContractAddress.getFieldValue(), + tokenNetworkId: String = Network.getFieldValue().toNetworkId(), + selectedDerivation: Blockchain = DerivationPath.getFieldValue() + ): Boolean { + val savedCurrencies = hubState.appSavedCurrencies ?: return false + + val derivationPath = getDerivationPathFromSelectedBlockchain(selectedDerivation) + savedCurrencies.forEach { wrappedCurrency -> + when (wrappedCurrency) { + is DomainWrapped.Currency.Blockchain -> {} + is DomainWrapped.Currency.Token -> { + val sameId = tokenId == wrappedCurrency.token.id + val sameAddress = tokenContractAddress == wrappedCurrency.token.contractAddress + val sameBlockchain = Blockchain.fromNetworkId(tokenNetworkId) == wrappedCurrency.blockchain + val sameDerivationPath = derivationPath?.rawPath == wrappedCurrency.derivationPath + if (sameId && sameAddress && sameBlockchain && sameDerivationPath) { + return true } } } } - - if (toAddWarnings.isNotEmpty() || toRemoveWarnings.isNotEmpty()) { - dispatchOnMain(Warning.Replace(toRemoveWarnings.toSet(), toAddWarnings.toSet())) - } + return false } - private fun isPersistIntoTheAppAddedTokenList( - token: Coins.CheckAddressResponse.Token, - contract: Coins.CheckAddressResponse.Token.Contract - ): Boolean = false - - private suspend fun addOrRemoveError(id: CustomTokenFieldId, error: AddCustomTokenError?) { - if (error == null) { - dispatchOnMain(FieldError.Remove(id)) - } else { - dispatchOnMain(FieldError.Add(id, error)) - } - } - - private fun lockTokenFields(): Action { + private fun isBlockchainPersistIntoAppSavedTokensList( + selectedNetwork: Blockchain = Network.getFieldValue(), + selectedDerivation: Blockchain = DerivationPath.getFieldValue() + ): Boolean { val state = hubState - return Screen.UpdateTokenFields(listOf( + val savedCurrencies = state.appSavedCurrencies ?: return false + + val derivationPath = getDerivationPathFromSelectedBlockchain(selectedDerivation) + savedCurrencies.forEach { wrappedCurrency -> + when (wrappedCurrency) { + is DomainWrapped.Currency.Blockchain -> { + val isSameBlockchain = selectedNetwork == wrappedCurrency.blockchain + val isSameDerivationPath = derivationPath?.rawPath == wrappedCurrency.derivationPath + if (isSameBlockchain && isSameDerivationPath) return true + } + is DomainWrapped.Currency.Token -> {} + } + } + return false + } + + private fun getDerivationPathFromSelectedBlockchain( + selectedDerivationBlockchain: Blockchain + ): com.tangem.common.hdWallet.DerivationPath? = AddCustomTokenState.getDerivationPath( + mainNetwork = Network.getFieldValue(), + derivationNetwork = selectedDerivationBlockchain, + derivationStyle = hubState.cardDerivationStyle + ) + + private suspend fun CustomTokenFieldId.addError(error: AddCustomTokenError) { + dispatchOnMain(FieldError.Add(this, error)) + } + + private suspend fun CustomTokenFieldId.removeError() { + dispatchOnMain(FieldError.Remove(this)) + } + + private inline fun CustomTokenFieldId.getField(): T { + val state = hubState + val value = when (this) { + ContractAddress -> state.getField(this) + Network -> state.getField(this) + Name -> state.getField(this) + Symbol -> state.getField(this) + Decimals -> state.getField(this) + DerivationPath -> state.getField(this) + } + return value as T + } + + private inline fun CustomTokenFieldId.getFieldValue(): T { + val value = when (this) { + ContractAddress -> getField().data.value + Network -> getField().data.value + Name -> getField().data.value + Symbol -> getField().data.value + Decimals -> getField().data.value + DerivationPath -> getField().data.value + } + return value as T + } + + private fun CustomTokenFieldId.setFieldValue(fieldData: Field.Data<*>) { + when (this) { + ContractAddress -> getField().data = fieldData as Field.Data + Network -> getField().data = fieldData as Field.Data + Name -> getField().data = fieldData as Field.Data + Symbol -> getField().data = fieldData as Field.Data + Decimals -> getField().data = fieldData as Field.Data + DerivationPath -> getField().data = fieldData as Field.Data + } + } + + private fun CustomTokenFieldId.validateValue(value: Any): AddCustomTokenError? { + val state = hubState + val contractAddressValidator: TokenContractAddressValidator = state.getValidator(ContractAddress) + val nameValidator: TokenNameValidator = state.getValidator(Name) + val symbolValidator: TokenSymbolValidator = state.getValidator(Symbol) + val decimalsValidator: TokenDecimalsValidator = state.getValidator(Decimals) + val networkValidator: TokenNetworkValidator = state.getValidator(Network) + return when (this) { + ContractAddress -> contractAddressValidator.validate(value as String) + Network, DerivationPath -> networkValidator.validate(value as Blockchain) + Name -> nameValidator.validate(value as String) + Symbol -> symbolValidator.validate(value as String) + Decimals -> decimalsValidator.validate(value as String) + } + } + + private fun CustomTokenFieldId.isFilled(): Boolean { + return when (this) { + ContractAddress -> getFieldValue().isNotEmpty() + Network -> getFieldValue() != Blockchain.Unknown + Name -> getFieldValue().isNotEmpty() + Symbol -> getFieldValue().isNotEmpty() + Decimals -> getFieldValue().isNotEmpty() + DerivationPath -> getFieldValue() != Blockchain.Unknown + } + } + + /** + * The field is being validated. + * If there is an error, then it adds it to the field. + */ + private suspend fun CustomTokenFieldId.validateAndUpdateError(value: Any): AddCustomTokenError? { + val error = this.validateValue(value) + when (error) { + null -> this.removeError() + else -> this.addError(error) + } + return error + } + + private suspend fun fillTokenFields( + token: Coins.CheckAddressResponse.Token, + contract: Coins.CheckAddressResponse.Token.Contract, + ) { + val blockchain = Blockchain.fromNetworkId(contract.networkId) ?: Blockchain.Unknown + Network.setFieldValue(Field.Data(blockchain, false)) + Name.setFieldValue(Field.Data(token.name, false)) + Symbol.setFieldValue(Field.Data(token.symbol, false)) + Decimals.setFieldValue(Field.Data(contract.decimalCount.toString(), false)) + dispatchOnMain(UpdateForm(hubState)) + } + + private suspend fun clearTokenFields() { + Name.setFieldValue(Field.Data("", false)) + Symbol.setFieldValue(Field.Data("", false)) + Decimals.setFieldValue(Field.Data("", false)) + dispatchOnMain(UpdateForm(hubState)) + } + + private suspend fun lockTokenFields() { + val state = hubState + val action = Screen.UpdateTokenFields(listOf( Network to state.screenState.network.copy(isEnabled = false), Name to state.screenState.name.copy(isEnabled = false), Symbol to state.screenState.symbol.copy(isEnabled = false), Decimals to state.screenState.decimals.copy(isEnabled = false), )) + dispatchOnMain(action) } - private fun unlockTokenFields(): Action { + private suspend fun unlockTokenFields() { val state = hubState - return Screen.UpdateTokenFields(listOf( + val action = Screen.UpdateTokenFields(listOf( Network to state.screenState.network.copy(isEnabled = true), Name to state.screenState.name.copy(isEnabled = true), Symbol to state.screenState.symbol.copy(isEnabled = true), Decimals to state.screenState.decimals.copy(isEnabled = true), )) + dispatchOnMain(action) } + private suspend fun AddCustomTokenWarning.add() { + dispatchOnMain(Warning.Add(setOf(this))) + } + + private suspend fun AddCustomTokenWarning.remove() { + dispatchOnMain(Warning.Remove(setOf(this))) + } + + private suspend fun AddCustomTokenWarning.replace(to: AddCustomTokenWarning) { + dispatchOnMain(Warning.Replace(setOf(this), setOf(to))) + } + +// private suspend fun AddCustomTokenWarning.replace(replace: Boolean, to: AddCustomTokenWarning) { +// if (replace) dispatchOnMain(Warning.Replace(setOf(this), setOf(to))) +// } + + override fun reduceAction(action: Action, state: AddCustomTokenState): AddCustomTokenState { return when (action) { is Init.SetAddedCurrencies -> { - state.copy(addedCurrencies = action.addedCurrencies) + state.copy(appSavedCurrencies = action.addedCurrencies) } is Init.SetOnAddTokenCallback -> { state.copy(onTokenAddCallback = action.callback) @@ -296,9 +514,16 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT val card = requireNotNull(globalState.scanResponse?.card) val tangemTechServiceManager = TangemTechServiceManager(TangemTechService()) tangemTechServiceManager.attachAuthKey(card.cardPublicKey.toHexString()) + + var derivationPathState = state.screenState.derivationPath + derivationPathState = when (card.derivationStyle) { + DerivationStyle.LEGACY -> derivationPathState.copy(isVisible = true) + null, DerivationStyle.NEW -> derivationPathState.copy(isVisible = false) + } state.copy( - derivationStyle = card.derivationStyle, - tangemTechServiceManager = tangemTechServiceManager + cardDerivationStyle = card.derivationStyle, + tangemTechServiceManager = tangemTechServiceManager, + screenState = state.screenState.copy(derivationPath = derivationPathState) ) } is OnDestroy -> state.reset() @@ -343,7 +568,7 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT val newMap = state.formErrors.toMutableMap().apply { remove(action.id) } state.copy(formErrors = newMap) } - is SetTokenId -> { + is SetFoundTokenId -> { state.copy(tokenId = action.id) } is Warning.Add -> { diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt index 13acabb71e..568a36ba8b 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt @@ -2,15 +2,16 @@ package com.tangem.domain.features.addCustomToken.redux import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.DerivationStyle +import com.tangem.domain.DomainWrapped import com.tangem.domain.common.form.* import com.tangem.domain.features.addCustomToken.* import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.* import org.rekotlin.StateType data class AddCustomTokenState( - val addedCurrencies: AddedCurrencies? = null, - val onTokenAddCallback: ((CompleteData) -> Unit)? = null, - val derivationStyle: DerivationStyle? = null, + val appSavedCurrencies: List? = null, + val onTokenAddCallback: ((CustomCurrency) -> Unit)? = null, + val cardDerivationStyle: DerivationStyle? = null, val form: Form = Form(createFormFields()), val formValidators: Map> = createFormValidators(), val formErrors: Map = emptyMap(), @@ -28,30 +29,73 @@ data class AddCustomTokenState( fun hasError(id: FieldId): Boolean = formErrors[id] != null - fun getCompleteData(type: CompleteDataType): CompleteData = when (type) { - CompleteDataType.Token -> getToken() - CompleteDataType.Blockchain -> getBlockchain() - } - inline fun visitDataConverter(converter: FieldDataConverter): T { form.visitDataConverter(converter) return converter.getConvertedData() } - fun convertBlockchainName(blockchain: Blockchain, unknown: String): String = when (blockchain) { - Blockchain.Unknown -> unknown - else -> blockchain.fullName + fun blockchainToName(blockchain: Blockchain, isDerivationPath: Boolean = false): String? { + return when { + isDerivationPath -> blockchain.derivationPath(cardDerivationStyle)?.rawPath + else -> { + when (blockchain) { + Blockchain.Unknown -> null + else -> blockchain.fullName + } + } + } } - fun convertDerivationPathLabel(blockchain: Blockchain, unknown: String): String { - return blockchain.derivationPath(derivationStyle)?.rawPath ?: unknown + // except network + fun tokensFieldsIsFilled(): Boolean { + val idsToCheck = listOf(ContractAddress, Name, Symbol, Decimals) + val fieldsToCheck = form.fieldList.filter { idsToCheck.contains(it.id) } + val validator = StringIsNotEmptyValidator() + fieldsToCheck.forEach { field -> + val error = validator.validate(field.data.value?.toString()) + if (error != null) return false + } + return true + } + + // except network + fun tokensOneFieldsIsFilled(): Boolean { + val idsToCheck = listOf(ContractAddress, Name, Symbol, Decimals) + val fieldsToCheck = form.fieldList.filter { idsToCheck.contains(it.id) } + val validator = StringIsEmptyValidator() + val errorsList = fieldsToCheck.mapNotNull { field -> + validator.validate(field.data.value?.toString()) + } + return errorsList.size == 1 + } + + fun networkIsSelected(): Boolean { + val network = getField(Network) + return network.data.value != Blockchain.Unknown + } + + fun derivationPathIsSelected(): Boolean { + val network = getField(DerivationPath) + return network.data.value != Blockchain.Unknown + } + + fun gatherUserToken(): CustomCurrency.CustomToken? = try { + getToken() + } catch (ex: Exception) { + null + } + + fun gatherBlockchain(): CustomCurrency.CustomBlockchain? = try { + getBlockchain() + } catch (ex: Exception) { + null } fun reset(): AddCustomTokenState { return this.copy( - addedCurrencies = null, + appSavedCurrencies = null, onTokenAddCallback = null, - derivationStyle = null, + cardDerivationStyle = null, form = Form(createFormFields()), formErrors = emptyMap(), tokenId = null, @@ -61,38 +105,33 @@ data class AddCustomTokenState( ) } - fun networkIsEmpty(): Boolean { - val network = getField(Network) - return network.data.value != Blockchain.Unknown - } - - fun customTokensFieldsIsEmpty(): Boolean { - val idsToCheck = listOf(ContractAddress, Name, Symbol, Decimals) - val fieldsToCheck = form.fieldList.filter { idsToCheck.contains(it.id) } - val validator = StringIsEmptyValidator() -// val errors = mutableMapOf<>() - fieldsToCheck.forEach { field -> - val error = validator.validate(field.data.value?.toString()) - if (error != null) return true - } - return false - } - - fun allFieldsIsEmpty(): Boolean = networkIsEmpty() && customTokensFieldsIsEmpty() - - private fun getToken(): CompleteData.CustomToken { - return CompleteData.CustomToken.Converter(tokenId) + private fun getToken(): CustomCurrency.CustomToken { + return CustomCurrency.CustomToken.Converter(tokenId, cardDerivationStyle) .apply { visitDataConverter(this) } .getConvertedData() } - private fun getBlockchain(): CompleteData.CustomBlockchain { - return CompleteData.CustomBlockchain.Converter() + private fun getBlockchain(): CustomCurrency.CustomBlockchain { + return CustomCurrency.CustomBlockchain.Converter(cardDerivationStyle) .apply { visitDataConverter(this) } .getConvertedData() } companion object { + + /** + * If an user select derivation path (derivationNetwork) as Blockchain.Unknown, + * then we should use a blockchain from the mainNetwork to determine a DerivationPath + */ + fun getDerivationPath( + mainNetwork: Blockchain, + derivationNetwork: Blockchain, + derivationStyle: DerivationStyle? + ): com.tangem.common.hdWallet.DerivationPath? = when (derivationNetwork) { + Blockchain.Unknown -> mainNetwork + else -> derivationNetwork + }.derivationPath(derivationStyle) + private fun createFormFields(): List> { return listOf( TokenField(ContractAddress), @@ -138,11 +177,11 @@ data class AddCustomTokenState( return ScreenState( contractAddressField = ViewStates.TokenField(), network = ViewStates.TokenField(), - name = ViewStates.TokenField(), - symbol = ViewStates.TokenField(), - decimals = ViewStates.TokenField(), + name = ViewStates.TokenField(isEnabled = false), + symbol = ViewStates.TokenField(isEnabled = false), + decimals = ViewStates.TokenField(isEnabled = false), derivationPath = ViewStates.TokenField(), - addButton = ViewStates.AddButton() + addButton = ViewStates.AddButton(isEnabled = false) ) } } diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt index 1d9bc38a7a..3dba21a577 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt @@ -1,8 +1,5 @@ package com.tangem.domain.features.addCustomToken.redux -import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.DomainWrapped - /** [REDACTED_AUTHOR] */ @@ -27,9 +24,4 @@ sealed class ViewStates { data class AddButton( val isEnabled: Boolean = true ) : ViewStates() -} - -data class AddedCurrencies( - val addedTokens: List, - val addedBlockchains: List -) \ No newline at end of file +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt b/domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt index 9de3cedcf3..1c8bd9314c 100644 --- a/domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt +++ b/domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt @@ -97,6 +97,10 @@ internal abstract class BaseStoreHub( } } + protected fun cancelAll() { + actionsAndJobs.forEach { (_, job) -> job.cancel() } + } + protected abstract suspend fun handleAction(action: Action, storeState: DomainState, cancel: ValueCallback) protected abstract fun reduceAction(action: Action, state: State): State diff --git a/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt b/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt index 1ce8b77035..a01a16315a 100644 --- a/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt +++ b/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt @@ -1,6 +1,6 @@ package com.tangem.domain.redux.global -import com.tangem.domain.DomainStateDialog +import com.tangem.domain.DomainDialog import com.tangem.domain.common.ScanResponse import org.rekotlin.Action @@ -10,5 +10,5 @@ import org.rekotlin.Action //TODO: refactoring: is alias for the GlobalAction sealed class DomainGlobalAction : Action { data class SetScanResponse(val scanResponse: ScanResponse?) : DomainGlobalAction() - data class ShowDialog(val stateDialog: DomainStateDialog?) : DomainGlobalAction() + data class ShowDialog(val stateDialog: DomainDialog?) : DomainGlobalAction() } \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt b/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt index b629778ccc..9bf5d7c15e 100644 --- a/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt +++ b/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt @@ -1,6 +1,6 @@ package com.tangem.domain.redux.global -import com.tangem.domain.DomainStateDialog +import com.tangem.domain.DomainDialog import com.tangem.domain.common.ScanResponse /** @@ -9,5 +9,5 @@ import com.tangem.domain.common.ScanResponse //TODO: refactoring: is alias for the GlobalState data class DomainGlobalState( val scanResponse: ScanResponse? = null, - val dialog: DomainStateDialog? = null, + val dialog: DomainDialog? = null, )